@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
@@ -126,7 +126,862 @@ def _biz_rule(action):
126
126
  }
127
127
 
128
128
 
129
+ class RuleEventMergeTest(unittest.TestCase):
130
+ def test_creation_and_value_update_names_do_not_select_after_events(self):
131
+ self.assertEqual(5, rule_contract.select_action_events(
132
+ "TakeBaseProAction", ["创建", "值更新"],
133
+ ))
134
+ self.assertEqual(5, rule_contract.select_action_events(
135
+ "TakeBaseProAction", ["create", "update"],
136
+ ))
137
+ self.assertEqual(256, rule_contract.select_action_events(
138
+ "TakeBaseProAction", ["创建后"],
139
+ ))
140
+
141
+ def test_semantic_events_compile_for_business_and_interface_services(self):
142
+ biz = rule_contract.normalize_biz_rule_action({
143
+ "actionType": "FormulaAction", "expression": "amount = qty * price",
144
+ "events": ["AfterCreate", "FieldChange"],
145
+ })
146
+ self.assertEqual(257, biz["RET"])
147
+ self.assertNotIn("events", biz)
148
+ form = rule_contract.normalize_form_rule_action({
149
+ "actionType": "HideFieldAction", "fields": ["price"],
150
+ "events": ["AfterCreate", "FieldChange"],
151
+ })
152
+ self.assertEqual(257, form["RET"])
153
+ self.assertNotIn("events", form)
154
+ with self.assertRaisesRegex(ValueError, "RULE_EVENT_BASE_RET_REQUIRED"):
155
+ rule_contract.normalize_biz_rule_action({
156
+ "actionType": "FormulaAction", "expression": "amount = qty * price",
157
+ "addEvents": ["AfterCopy"],
158
+ })
159
+
160
+ @patch.object(cmd_rules, "do_ensure_conditional_clear_fields")
161
+ def test_batch_forwards_named_events(self, writer):
162
+ item = cmd_rules._validate_rule_set_request({"rules": [{
163
+ "command": "ensureConditionalClearFields",
164
+ "args": {"conditionField": "数量", "operator": "greater-than", "value": "2",
165
+ "targets": ["单价"], "addEvents": ["AfterCopy"]},
166
+ }]})[0]
167
+ cmd_rules._execute_rule_set_item({}, "token", "demo", item)
168
+ self.assertEqual(["AfterCopy"], writer.call_args.args[-2])
169
+
170
+ def test_batch_accepts_exact_events_for_every_rule_service(self):
171
+ samples = {
172
+ "ensureRuleFunctionFormula": {"target": "金额", "call": {"functionId": "round", "args": []}},
173
+ "ensureConditionalControlState": {"conditionField": "状态", "operator": "equals", "value": "A", "targetControl": "按钮", "behavior": "hide"},
174
+ "ensureConditionalClearFields": {"conditionField": "数量", "operator": "greater-than", "value": "10", "targets": ["单价"]},
175
+ "ensureConditionalFieldChange": {"conditionField": "物料", "operator": "not-empty", "target": "单价"},
176
+ "ensureConditionalCopyFieldValue": {"conditionField": "状态", "operator": "equals", "value": "A", "source": "来源", "target": "目标"},
177
+ "ensureConditionalCalculation": {"conditionField": "状态", "operator": "equals", "value": "A", "target": "金额", "left": "数量", "calculation": "multiply", "right": "单价"},
178
+ "ensureConditionalCarryReferenceProperty": {"sourceReference": "客户", "sourceProperty": "名称", "target": "客户名"},
179
+ "ensureQuantityConversion": {"sourceQty": "数量", "targetQty": "换算数量"},
180
+ }
181
+ for command, args in samples.items():
182
+ with self.subTest(command=command):
183
+ item = cmd_rules._validate_rule_set_request({"rules": [{
184
+ "command": command, "args": {**args, "events": ["AfterCreate", "FieldChange"]},
185
+ }]})[0]
186
+ self.assertEqual(["AfterCreate", "FieldChange"], item["args"]["events"])
187
+ client = cmd_rules._validate_rule_set_request({"rules": [{
188
+ "command": "ensureConditionalClientAppearance",
189
+ "args": {"request": {"clientMount": "list", "conditionField": "状态",
190
+ "operator": "equals", "value": "A",
191
+ "effects": [{"kind": "rowStyle", "background": "#FFFFFF"}],
192
+ "events": ["AfterCreate", "FieldChange"]}},
193
+ }]})[0]
194
+ self.assertEqual(["AfterCreate", "FieldChange"], client["args"]["request"]["events"])
195
+
196
+ def test_batch_forwards_timings_to_formula_copy_and_control_services(self):
197
+ for command, writer, args in (
198
+ ("ensureConditionalCalculation", "do_ensure_conditional_calculation",
199
+ {"conditionField": "状态", "operator": "equals", "value": "A",
200
+ "target": "金额", "left": "数量", "calculation": "multiply", "right": "单价"}),
201
+ ("ensureConditionalCopyFieldValue", "do_ensure_conditional_copy_field_value",
202
+ {"conditionField": "状态", "operator": "equals", "value": "A",
203
+ "source": "来源", "target": "目标"}),
204
+ ("ensureConditionalControlState", "do_ensure_conditional_control_state",
205
+ {"conditionField": "状态", "operator": "equals", "value": "A",
206
+ "targetControl": "按钮", "behavior": "hide"}),
207
+ ):
208
+ with self.subTest(command=command), patch.object(cmd_rules, writer) as write:
209
+ item = cmd_rules._validate_rule_set_request({"rules": [{
210
+ "command": command, "args": {**args, "events": ["AfterCopy"]},
211
+ }]})[0]
212
+ cmd_rules._execute_rule_set_item({}, "token", "demo", item)
213
+ self.assertEqual(["AfterCopy"], write.call_args.args[-1])
214
+
215
+ def test_existing_generic_service_adds_timing_without_replacing_actions(self):
216
+ rule = {
217
+ "_Type_": "BizRule", "Id": "rule-1", "Description": "计算金额",
218
+ "TrueActions": [{"Id": "action-1", "ActionType": "FormulaAction",
219
+ "Expression": "amount = qty * price", "RET": 253}],
220
+ "FalseActions": [],
221
+ }
222
+ with patch.object(cmd_rules, "do_update_rule_verified", return_value={
223
+ "status": True, "data": {},
224
+ }) as write, patch.object(cmd_rules, "do_load_full_metadata", return_value={
225
+ "status": True, "data": {"Rules": [{
226
+ **rule, "TrueActions": [{**rule["TrueActions"][0],
227
+ "Id": "new-action-id", "RET": 1277}],
228
+ }]},
229
+ }):
230
+ result = cmd_rules._reconcile_existing_rule_events(
231
+ {}, "token", "demo", rule, "bizRule", add_events=["AfterCopy"],
232
+ )
233
+ self.assertTrue(result["verification"]["preservationVerified"])
234
+ self.assertEqual(1277, write.call_args.args[-1]["trueActions"][0]["RET"])
235
+ self.assertEqual(253, rule["TrueActions"][0]["RET"])
236
+
237
+ def test_existing_generic_service_refuses_implicit_timing_replacement(self):
238
+ rule = {"Id": "rule-1", "TrueActions": [{"ActionType": "FormulaAction", "RET": 253}],
239
+ "FalseActions": []}
240
+ result = cmd_rules._reconcile_existing_rule_events(
241
+ {}, "token", "demo", rule, "bizRule", events=["AfterCopy"],
242
+ )
243
+ self.assertEqual("RULE_EVENT_REPLACE_REQUIRES_RULE_ID", result["errorCode"])
244
+ self.assertFalse(result["writeAttempted"])
245
+
246
+
247
+ class ReferenceCardinalityTest(unittest.TestCase):
248
+ def test_department_entry_is_rejected_before_any_write_in_both_carry_routes(self):
249
+ def api(_cfg, _token, endpoint, payload, method="GET"):
250
+ if endpoint == "getFormSchema":
251
+ return {"status": True, "data": {"formNumber": "bos_user"}}
252
+ if endpoint == "getEntityFields":
253
+ return {"status": True, "data": {
254
+ "headerFields": {"headerKey": "bos_user", "fields": []},
255
+ "entryFields": {"entryentity": {"entryKey": "entryentity", "fields": [
256
+ {"key": "dpt", "name": "部门", "type": "OrgField"},
257
+ ]}},
258
+ }}
259
+ raise AssertionError(endpoint)
260
+
261
+ def elements(*args, **kwargs):
262
+ return {"status": True, "data": (
263
+ [{"key": "demo", "name": "单据头", "id": "header-id", "type": "BillEntity"}]
264
+ if kwargs["category"] == "entity" else [
265
+ {"key": "applicant", "name": "申请人", "type": "UserField", "parentKey": "demo"},
266
+ {"key": "dept", "name": "申请部门", "type": "OrgField", "parentKey": "demo"},
267
+ ])}
268
+
269
+ for declared in ([], [{"name": "dpt"}]):
270
+ for batch in (False, True):
271
+ with self.subTest(declared=declared, batch=batch), patch.object(
272
+ cmd_rules, "_api_call", side_effect=api,
273
+ ), patch.object(cmd_rules, "do_list_elements", side_effect=elements), patch.object(
274
+ cmd_rules, "do_get_properties", return_value={"status": True, "data": {
275
+ "baseEntityId": "user-id", "refProps": declared,
276
+ }},
277
+ ), patch.object(cmd_rules, "do_set_ref_props") as ref_write, patch.object(
278
+ cmd_rules, "do_add_rule_verified",
279
+ ) as rule_write:
280
+ with self.assertRaisesRegex(ValueError, "CARRY_REFERENCE_COLLECTION_UNSUPPORTED.*entryentity"):
281
+ if batch:
282
+ cmd_rules.do_ensure_carry_reference_mappings({}, "token", "demo", {"mappings": [{
283
+ "sourceReference": "申请人", "sourceProperty": "dpt", "target": "申请部门",
284
+ }]})
285
+ else:
286
+ cmd_rules.do_ensure_conditional_carry_reference_property(
287
+ {}, "token", "demo", None, None, None, None, "申请人", "dpt", "申请部门",
288
+ )
289
+ ref_write.assert_not_called()
290
+ rule_write.assert_not_called()
291
+
292
+
293
+ class MultiMappingRuleTest(unittest.TestCase):
294
+ def test_mapping_cli_accepts_condition_flags_alongside_input(self):
295
+ from types import SimpleNamespace
296
+ from cosmic_api import _load_mapping_request
297
+
298
+ args = SimpleNamespace(
299
+ input_file='{"mappings":[{"sourceReference":"物料","sourceProperty":"分类","target":"原分类"}]}',
300
+ entity=None, condition_field="是否启用带出", operator="equals",
301
+ value="true", events=["create", "update"], add_events=None,
302
+ )
303
+ request = _load_mapping_request(args)
304
+ self.assertEqual("是否启用带出", request["conditionField"])
305
+ self.assertEqual(["create", "update"], request["events"])
306
+ args.input_file = '{"mappings":[],"conditionField":"另一个条件"}'
307
+ with self.assertRaisesRegex(ValueError, "conflicts"):
308
+ _load_mapping_request(args)
309
+
310
+ @patch.object(cmd_rules, "do_add_rule_verified")
311
+ @patch.object(cmd_rules, "do_load_full_metadata")
312
+ @patch.object(cmd_rules, "do_set_ref_props")
313
+ @patch.object(cmd_rules, "_reference_entity_field_candidates")
314
+ @patch.object(cmd_rules, "do_get_properties")
315
+ @patch.object(cmd_rules, "do_list_elements")
316
+ def test_same_reference_multiple_missing_properties_are_prepared_once(
317
+ self, list_elements, get_properties, reference_fields, set_ref_props,
318
+ load_full, add_verified,
319
+ ):
320
+ list_elements.side_effect = lambda *args, **kwargs: {
321
+ "status": True,
322
+ "data": ([{"key": "demo", "name": "验证单", "id": "entity-id", "type": "BillEntity"}]
323
+ if kwargs["category"] == "entity" else [
324
+ {"key": key, "name": key, "parentKey": "demo", "type": "TextField"}
325
+ for key in ("material", "old_group", "new_group", "old_name", "new_name")
326
+ ]),
327
+ }
328
+ get_properties.return_value = {"status": True, "data": {
329
+ "refProps": "[]", "baseEntityId": "material-id",
330
+ }}
331
+ reference_fields.return_value = [
332
+ {"key": "groupid", "name": "分类"},
333
+ {"key": "name", "name": "名称"},
334
+ ]
335
+ set_ref_props.return_value = {"status": True, "verification": {"verified": True}}
336
+ load_full.return_value = {"status": True, "data": {"entitymeta": {"Items": []}}}
337
+ add_verified.return_value = {"status": True, "data": {"ruleId": "rule-1"},
338
+ "verification": {"verified": True}}
339
+ request = {"events": ["create", "update"], "mappings": [
340
+ {"sourceReference": "material", "sourceProperty": "分类", "target": "old_group"},
341
+ {"sourceReference": "material", "sourceProperty": "分类", "target": "new_group"},
342
+ {"sourceReference": "material", "sourceProperty": "名称", "target": "old_name"},
343
+ {"sourceReference": "material", "sourceProperty": "名称", "target": "new_name"},
344
+ ]}
345
+ result = cmd_rules.do_ensure_carry_reference_mappings({}, "token", "demo", request)
346
+ self.assertTrue(result["status"])
347
+ reference_fields.assert_called_once_with({}, "token", "material-id")
348
+ get_properties.assert_called_once_with({}, "token", "demo", "material", ["RefProps", "BaseEntityId"])
349
+ self.assertEqual(4, len(add_verified.call_args.args[3]["trueActions"][0]["expression"]))
350
+ self.assertEqual(5, add_verified.call_args.args[3]["trueActions"][0]["ret"])
351
+ set_ref_props.assert_called_once_with(
352
+ {}, "token", "demo", "material", [{"name": "groupid"}, {"name": "name"}],
353
+ )
354
+
355
+ set_ref_props.reset_mock()
356
+ add_verified.reset_mock()
357
+ request["mappings"][2]["sourceProperty"] = "核心名称"
358
+ with self.assertRaisesRegex(ValueError, "CONDITIONAL_CARRY_SOURCE_PROPERTY_UNAVAILABLE"):
359
+ cmd_rules.do_ensure_carry_reference_mappings({}, "token", "demo", request)
360
+ set_ref_props.assert_not_called()
361
+ add_verified.assert_not_called()
362
+
363
+ def test_raw_carry_action_keeps_multiple_mapping_rows(self):
364
+ action = rule_contract.normalize_biz_rule_action({
365
+ "actionType": "TakeBaseProAction",
366
+ "expression": [
367
+ {"targetField": "old_category", "srcField": "material.category"},
368
+ {"targetField": "new_category", "srcField": "material.category"},
369
+ ],
370
+ "fieldName": [
371
+ {"targetFieldName": "原分类", "srcFieldName": "物料.分类"},
372
+ {"targetFieldName": "新分类", "srcFieldName": "物料.分类"},
373
+ ],
374
+ })
375
+ self.assertEqual(2, len(action["Expression"]))
376
+ self.assertEqual(2, len(action["FieldName"]))
377
+ with self.assertRaisesRegex(ValueError, "LENGTH_MISMATCH"):
378
+ rule_contract.normalize_biz_rule_action({
379
+ "actionType": "TakeBaseProAction",
380
+ "expression": [
381
+ {"targetField": "old_category", "srcField": "material.category"},
382
+ {"targetField": "new_category", "srcField": "material.category"},
383
+ ],
384
+ "fieldName": [{"targetFieldName": "原分类", "srcFieldName": "物料.分类"}],
385
+ })
386
+
387
+ @patch.object(cmd_rules, "do_list_elements")
388
+ def test_copy_scope_checks_every_mapping_row(self, list_elements):
389
+ list_elements.return_value = {"status": True, "data": [
390
+ {"key": "source", "parentKey": "uhyl_demo"},
391
+ {"key": "target", "parentKey": "uhyl_demo"},
392
+ ]}
393
+ requested = _biz_rule({
394
+ "actionType": "SetEqutionAction",
395
+ "expression": [{"targetField": "target", "srcField": "source"},
396
+ {"targetField": "missing_target", "srcField": "source"}],
397
+ "fieldName": [{"targetFieldName": "目标(target)", "srcFieldName": "来源(source)"},
398
+ {"targetFieldName": "缺失(missing_target)", "srcFieldName": "来源(source)"}],
399
+ })
400
+ with self.assertRaisesRegex(ValueError, "RULE_SET_EQUATION_FIELD_UNRESOLVED.*missing_target"):
401
+ cmd_rules._validate_set_equation_action_scope({}, "token", "demo", requested)
402
+ with self.assertRaisesRegex(ValueError, "DUPLICATE_TARGET"):
403
+ rule_contract.normalize_biz_rule_action({
404
+ "actionType": "SetEqutionAction",
405
+ "expression": [
406
+ {"targetField": "target", "srcField": "first"},
407
+ {"targetField": "target", "srcField": "second"},
408
+ ],
409
+ "fieldName": [
410
+ {"targetFieldName": "目标一", "srcFieldName": "来源一"},
411
+ {"targetFieldName": "目标二", "srcFieldName": "来源二"},
412
+ ],
413
+ })
414
+
415
+ @patch.object(cmd_rules, "do_add_rule_verified")
416
+ @patch.object(cmd_rules, "do_load_full_metadata")
417
+ @patch.object(cmd_rules, "do_list_elements")
418
+ def test_semantic_copy_writes_one_service_with_two_mapping_rows(
419
+ self, list_elements, load_full, add_verified,
420
+ ):
421
+ list_elements.side_effect = lambda *args, **kwargs: {
422
+ "status": True,
423
+ "data": ([{"key": "demo", "name": "主实体", "id": "entity-id", "type": "MainEntity"}]
424
+ if kwargs["category"] == "entity" else [
425
+ {"key": key, "name": key, "parentKey": "demo", "type": "TextField"}
426
+ for key in ("old_value", "new_value", "old_target", "new_target")
427
+ ]),
428
+ }
429
+ load_full.return_value = {"status": True, "data": {"entitymeta": {"Items": []}}}
430
+ add_verified.return_value = {"status": True, "data": {"ruleId": "copy-rule"},
431
+ "verification": {"verified": True}}
432
+ request = {"mappings": [
433
+ {"source": "old_value", "target": "old_target"},
434
+ {"source": "new_value", "target": "new_target"},
435
+ ]}
436
+ result = cmd_rules.do_ensure_copy_field_mappings({}, "token", "demo", request)
437
+ self.assertTrue(result["writeAttempted"])
438
+ action = add_verified.call_args.args[3]["trueActions"][0]
439
+ self.assertEqual("SetEqutionAction", action["actionType"])
440
+ self.assertEqual(2, len(action["expression"]))
441
+ self.assertEqual(2, len(action["fieldName"]))
442
+ payload = add_verified.call_args.args[3]
443
+ false_result = cmd_rules.do_ensure_copy_field_mappings(
444
+ {}, "token", "demo", {**request, "branch": "false"},
445
+ )
446
+ self.assertTrue(false_result["writeAttempted"])
447
+ false_payload = add_verified.call_args.args[3]
448
+ self.assertEqual([], false_payload["trueActions"])
449
+ self.assertEqual(1, len(false_payload["falseActions"]))
450
+ cmd_rules._validate_rule_write_input(false_payload, command="addRule")
451
+ compiled = cmd_rules.normalize_rule_config(payload)
452
+ split = {"_Type_": "BizRule", "Id": "copy-rule", "ItemId": "entity-id",
453
+ "PreCondition": payload["preCondition"], "TriggerField": payload["triggerField"],
454
+ "TrueActions": [
455
+ {**compiled["trueActions"][0], "Expression": [mapping],
456
+ "FieldName": [compiled["trueActions"][0]["FieldName"][index]]}
457
+ for index, mapping in enumerate(compiled["trueActions"][0]["Expression"])
458
+ ], "FalseActions": []}
459
+ load_full.return_value = {"status": True, "data": {"entitymeta": {"Items": [split]}}}
460
+ with patch.object(cmd_rules, "do_update_rule_verified", return_value={
461
+ "status": True, "data": {}, "verification": {"verified": True},
462
+ }) as update:
463
+ consolidated = cmd_rules.do_ensure_copy_field_mappings(
464
+ {}, "token", "demo", {**request, "ruleId": "copy-rule"},
465
+ )
466
+ self.assertTrue(consolidated["writeAttempted"])
467
+ self.assertEqual(1, len(update.call_args.args[3]["trueActions"]))
468
+ self.assertEqual(2, len(update.call_args.args[3]["trueActions"][0]["expression"]))
469
+ false_compiled = cmd_rules.normalize_rule_config(false_payload)
470
+ false_split = {**split, "TrueActions": [], "FalseActions": [
471
+ {**false_compiled["falseActions"][0], "Expression": [mapping],
472
+ "FieldName": [false_compiled["falseActions"][0]["FieldName"][index]]}
473
+ for index, mapping in enumerate(false_compiled["falseActions"][0]["Expression"])
474
+ ]}
475
+ load_full.return_value = {"status": True, "data": {"entitymeta": {"Items": [false_split]}}}
476
+ with patch.object(cmd_rules, "do_update_rule_verified", return_value={
477
+ "status": True, "data": {}, "verification": {"verified": True},
478
+ }) as update:
479
+ cmd_rules.do_ensure_copy_field_mappings(
480
+ {}, "token", "demo", {**request, "branch": "false", "ruleId": "copy-rule"},
481
+ )
482
+ self.assertEqual(1, len(update.call_args.args[3]["falseActions"]))
483
+ self.assertEqual(2, len(update.call_args.args[3]["falseActions"][0]["expression"]))
484
+ load_full.return_value = {"status": True, "data": {"entitymeta": {"Items": [
485
+ {**split, "TrueActions": [split["TrueActions"][0]]},
486
+ ]}}}
487
+ overlap = cmd_rules.do_ensure_copy_field_mappings({}, "token", "demo", request)
488
+ self.assertEqual("COPY_MAPPINGS_TARGET_OVERLAP", overlap["errorCode"])
489
+ with self.assertRaisesRegex(ValueError, "COPY_MAPPINGS_TARGET_CONFLICT"):
490
+ cmd_rules.do_ensure_copy_field_mappings({}, "token", "demo", {
491
+ "mappings": [{"source": "old_value", "target": "old_target"},
492
+ {"source": "new_value", "target": "old_target"}],
493
+ })
494
+
495
+ def test_rule_set_forwards_mapping_arrays_as_one_service_request(self):
496
+ for command, writer, mappings in (
497
+ ("ensureConditionalCarryReferenceProperty", "do_ensure_carry_reference_mappings",
498
+ [{"sourceReference": "物料", "sourceProperty": "分类", "target": "原分类"},
499
+ {"sourceReference": "物料", "sourceProperty": "分类", "target": "新分类"}]),
500
+ ("ensureConditionalCopyFieldValue", "do_ensure_copy_field_mappings",
501
+ [{"source": "旧值", "target": "原值"},
502
+ {"source": "新值", "target": "变更后值"}]),
503
+ ):
504
+ with self.subTest(command=command), patch.object(cmd_rules, writer) as write:
505
+ item = cmd_rules._validate_rule_set_request({"rules": [{
506
+ "command": command, "args": {"mappings": mappings},
507
+ }]})[0]
508
+ cmd_rules._execute_rule_set_item({}, "token", "demo", item)
509
+ self.assertEqual(mappings, write.call_args.args[-1]["mappings"])
510
+
511
+ @patch.object(cmd_rules, "do_add_rule_verified")
512
+ @patch.object(cmd_rules, "do_load_full_metadata")
513
+ @patch.object(cmd_rules, "do_get_properties")
514
+ @patch.object(cmd_rules, "do_list_elements")
515
+ @patch.object(cmd_rules, "_reference_entity_field_candidates", return_value=[{
516
+ "key": "linkman", "name": "联系人", "type": "TextField", "referenceCollection": False,
517
+ }, {"key": "number", "name": "编码", "type": "TextField", "referenceCollection": False},
518
+ {"key": "category", "name": "分类", "type": "BasedataField", "referenceCollection": False}])
519
+ def test_semantic_carry_writes_one_service_with_two_mapping_rows(
520
+ self, reference_fields, list_elements, get_properties, load_full, add_verified,
521
+ ):
522
+ list_elements.side_effect = lambda *args, **kwargs: {
523
+ "status": True,
524
+ "data": ([{"key": "demo", "name": "主实体", "id": "entity-id", "type": "MainEntity"}]
525
+ if kwargs["category"] == "entity" else [
526
+ {"key": key, "name": key, "parentKey": "demo", "type": "TextField"}
527
+ for key in ("material", "item", "old_category", "new_category")
528
+ ]),
529
+ }
530
+ get_properties.return_value = {"status": True, "data": {"refProps": '[{"name":"category"}]', "baseEntityId": "reference-id"}}
531
+ load_full.return_value = {"status": True, "data": {"entitymeta": {"Items": []}}}
532
+ add_verified.return_value = {"status": True, "data": {"ruleId": "rule-1"},
533
+ "verification": {"verified": True}}
534
+ request = {"mappings": [
535
+ {"sourceReference": "material", "sourceProperty": "category", "target": "old_category"},
536
+ {"sourceReference": "item", "sourceProperty": "category", "target": "new_category"},
537
+ ]}
538
+ created = cmd_rules.do_ensure_carry_reference_mappings({}, "token", "demo", request)
539
+ self.assertTrue(created["writeAttempted"])
540
+ payload = add_verified.call_args.args[3]
541
+ self.assertEqual(1, len(payload["trueActions"]))
542
+ self.assertEqual(2, len(payload["trueActions"][0]["expression"]))
543
+ self.assertEqual(2, len(payload["trueActions"][0]["fieldName"]))
544
+ self.assertEqual("material(material);item(item)", payload["triggerField"])
545
+ false_result = cmd_rules.do_ensure_carry_reference_mappings(
546
+ {}, "token", "demo", {**request, "branch": "false"},
547
+ )
548
+ self.assertTrue(false_result["writeAttempted"])
549
+ false_payload = add_verified.call_args.args[3]
550
+ self.assertEqual([], false_payload["trueActions"])
551
+ self.assertEqual(2, len(false_payload["falseActions"][0]["expression"]))
552
+ cmd_rules._validate_rule_write_input(false_payload, command="addRule")
553
+
554
+ compiled = cmd_rules.normalize_rule_config(payload)
555
+ stored = {"_Type_": "BizRule", "Id": "rule-1", "ItemId": "entity-id",
556
+ "PreCondition": payload["preCondition"], "TriggerField": payload["triggerField"],
557
+ "TrueActions": compiled["trueActions"], "FalseActions": []}
558
+ load_full.return_value = {"status": True, "data": {"entitymeta": {"Items": [stored]}}}
559
+ add_verified.reset_mock()
560
+ repeated = cmd_rules.do_ensure_carry_reference_mappings({}, "token", "demo", request)
561
+ self.assertFalse(repeated["writeAttempted"])
562
+ add_verified.assert_not_called()
563
+
564
+ stored["TrueActions"] = [
565
+ {**compiled["trueActions"][0], "Expression": [mapping],
566
+ "FieldName": [compiled["trueActions"][0]["FieldName"][index]]}
567
+ for index, mapping in enumerate(compiled["trueActions"][0]["Expression"])
568
+ ]
569
+ load_full.return_value = {"status": True, "data": {"entitymeta": {"Items": [stored]}}}
570
+ blocked = cmd_rules.do_ensure_carry_reference_mappings({}, "token", "demo", request)
571
+ self.assertEqual("CARRY_MAPPINGS_CONSOLIDATION_REQUIRES_RULE_ID", blocked["errorCode"])
572
+ with patch.object(cmd_rules, "do_update_rule_verified", return_value={
573
+ "status": True, "data": {}, "verification": {"verified": True},
574
+ }) as update:
575
+ consolidated = cmd_rules.do_ensure_carry_reference_mappings(
576
+ {}, "token", "demo", {**request, "ruleId": "rule-1"},
577
+ )
578
+ self.assertTrue(consolidated["writeAttempted"])
579
+ self.assertEqual(1, len(update.call_args.args[3]["trueActions"]))
580
+ self.assertEqual(2, len(update.call_args.args[3]["trueActions"][0]["expression"]))
581
+ load_full.return_value = {"status": True, "data": {"entitymeta": {"Items": [
582
+ {**stored, "TrueActions": [stored["TrueActions"][0]]},
583
+ ]}}}
584
+ overlap = cmd_rules.do_ensure_carry_reference_mappings({}, "token", "demo", request)
585
+ self.assertEqual("CARRY_MAPPINGS_TARGET_OVERLAP", overlap["errorCode"])
586
+
587
+ @patch.object(cmd_rules, "do_ensure_conditional_field_change")
588
+ def test_batch_forwards_field_change_request(self, writer):
589
+ item = cmd_rules._validate_rule_set_request({"rules": [{
590
+ "command": "ensureConditionalFieldChange",
591
+ "args": {"conditionField": "物料", "operator": "not-empty",
592
+ "target": "单价", "addEvents": ["AfterCopy"]},
593
+ }]})[0]
594
+ cmd_rules._execute_rule_set_item({}, "token", "demo", item)
595
+ self.assertEqual(["AfterCopy"], writer.call_args.args[-2])
596
+ exact = cmd_rules._validate_rule_set_request({"rules": [{
597
+ "command": "ensureConditionalFieldChange",
598
+ "args": {"conditionField": "物料", "operator": "not-empty",
599
+ "target": "单价", "events": ["FieldChanged", "AfterCopy"]},
600
+ }]})[0]
601
+ cmd_rules._execute_rule_set_item({}, "token", "demo", exact)
602
+ self.assertEqual(["FieldChanged", "AfterCopy"], writer.call_args.args[-1])
603
+ with self.assertRaisesRegex(ValueError, "RULE_EVENTS_INVALID"):
604
+ cmd_rules._validate_rule_set_request({"rules": [{
605
+ "command": "ensureConditionalFieldChange",
606
+ "args": {"conditionField": "物料", "operator": "not-empty",
607
+ "target": "单价", "addEvents": "AfterCopy"},
608
+ }]})
609
+ with self.assertRaisesRegex(ValueError, "mixes events and addEvents"):
610
+ cmd_rules._validate_rule_set_request({"rules": [{
611
+ "command": "ensureConditionalFieldChange",
612
+ "args": {"conditionField": "物料", "operator": "not-empty",
613
+ "target": "单价", "events": ["FieldChanged"],
614
+ "addEvents": ["AfterCopy"]},
615
+ }]})
616
+ self.assertEqual(2, writer.call_count)
617
+
618
+ @patch.object(cmd_rules, "do_add_rule_verified")
619
+ def test_related_field_change_condition_requires_rule_selection(self, add_verified):
620
+ self.rules.append({
621
+ "_Type_": "BizRule", "Id": "other-rule", "ItemId": "entity-id",
622
+ "PreCondition": "qty <> null", "TriggerField": "数量(qty)",
623
+ "TrueActions": [{"ActionType": "CallFieldChangeAction",
624
+ "Expression": "price", "RET": 773}],
625
+ "FalseActions": [],
626
+ })
627
+ result = cmd_rules.do_ensure_conditional_field_change(
628
+ {}, "token", "demo", None, "数量", "greater-than", "2",
629
+ "单价", ["AfterCopy"],
630
+ )
631
+ self.assertEqual("RULE_CONDITION_UPDATE_REQUIRES_RULE_ID", result["errorCode"])
632
+ self.assertEqual("other-rule", result["data"]["candidates"][0]["ruleId"])
633
+ add_verified.assert_not_called()
634
+
635
+ @patch.object(cmd_rules, "_api_call")
636
+ def test_tagged_field_change_rule_is_renamed_in_place(self, api_call):
637
+ description = "数量 greater-than 2时调用单价字段值更新服务"
638
+ digest = hashlib.sha1("demo:demo:qty > 2:price".encode("utf-8")).hexdigest()[:12]
639
+ action = {"ActionType": "CallFieldChangeAction", "Expression": "price",
640
+ "RET": 773, "Id": "action-id", "IsvFlag": "owner", "Seq": 1}
641
+ self.rules.append({
642
+ "_Type_": "BizRule", "Id": "rule-id", "ItemId": "entity-id",
643
+ "Description": f"{description} [CFC_{digest}]",
644
+ "PreDescription": description, "PreCondition": "qty > 2",
645
+ "TriggerField": "数量(qty)", "Enabled": True,
646
+ "TrueActions": [action], "FalseActions": [],
647
+ })
648
+ def write(_cfg, _token, command, payload, **kwargs):
649
+ self.assertEqual("updateRule", command)
650
+ self.rules[0]["Description"] = payload["description"]
651
+ return {"status": True, "data": {"ruleId": "rule-id"}}
652
+ api_call.side_effect = write
653
+
654
+ result = cmd_rules.do_ensure_conditional_field_change(
655
+ {}, "token", "demo", None, "数量", "greater-than", "2", "单价",
656
+ )
657
+ self.assertTrue(result["verification"]["preservationVerified"])
658
+ self.assertEqual(description, self.rules[0]["Description"])
659
+ self.assertEqual("rule-id", self.rules[0]["Id"])
660
+ self.assertEqual("action-id", self.rules[0]["TrueActions"][0]["Id"])
661
+ api_call.assert_called_once()
662
+
663
+ def setUp(self):
664
+ self.rules = []
665
+ self.context = {
666
+ "entityKey": "demo", "entityId": "entity-id",
667
+ "expression": "qty > 2", "conditionName": "数量",
668
+ "conditionKey": "qty", "conditionEntityKey": "demo",
669
+ "targetKey": "price", "targetName": "单价",
670
+ "operator": "greater-than", "requestedValue": "2",
671
+ "legacyBooleanExpression": None, "legacyEmptyExpression": None,
672
+ "fieldRows": [{"key": "price", "name": "单价", "parentKey": "demo"}],
673
+ }
674
+ self.context_patch = patch.object(
675
+ cmd_rules, "_resolve_conditional_context", return_value=self.context,
676
+ )
677
+ self.context_patch.start()
678
+ self.addCleanup(self.context_patch.stop)
679
+ reader = patch.object(cmd_rules, "do_load_full_metadata", side_effect=lambda *a: {
680
+ "status": True, "data": {"entitymeta": {"Items": copy.deepcopy(self.rules)}},
681
+ })
682
+ reader.start()
683
+ self.addCleanup(reader.stop)
684
+
685
+ def create(self, events=None):
686
+ def add(_cfg, _token, _form, requested):
687
+ compiled = rule_contract.normalize_rule_config(requested)
688
+ self.rules.append({
689
+ "_Type_": "BizRule", "Id": "rule-id", "ISV": "original-isv",
690
+ "Description": requested["description"],
691
+ "PreDescription": requested["preDescription"],
692
+ "PreCondition": requested["preCondition"],
693
+ "TriggerField": requested["triggerField"], "Enabled": True,
694
+ "TrueActions": compiled["trueActions"], "FalseActions": [],
695
+ })
696
+ self.rules[0]["TrueActions"][0].update({"Id": "action-id", "ISV": "owner"})
697
+ return {"status": True, "data": {"ruleId": "rule-id"},
698
+ "verification": {"verified": True}}
699
+ with patch.object(cmd_rules, "do_add_rule_verified", side_effect=add):
700
+ return self.run_clear(events)
701
+
702
+ def run_clear(self, events=None):
703
+ return cmd_rules.do_ensure_conditional_clear_fields(
704
+ {}, "token", "demo", None, "数量", "greater-than", "2", ["单价"], events,
705
+ )
706
+
707
+ def create_form_rule(self, events=None, exact_events=None):
708
+ def add(_cfg, _token, _form, requested, **kwargs):
709
+ compiled = rule_contract.normalize_rule_config(requested)
710
+ self.rules.append({
711
+ "_Type_": "FormRule", "Id": "form-rule-id",
712
+ "Description": requested["description"],
713
+ "PreDescription": requested["preDescription"],
714
+ "PreCondition": requested["preCondition"],
715
+ "TriggerField": requested["triggerField"], "Enabled": True,
716
+ "TrueActions": compiled["trueActions"],
717
+ "FalseActions": compiled["falseActions"],
718
+ })
719
+ for branch in ("TrueActions", "FalseActions"):
720
+ self.rules[0][branch][0]["Id"] = branch + "-old-id"
721
+ return {"status": True, "data": {"ruleId": "form-rule-id"},
722
+ "verification": {"verified": True}}
723
+ with patch.object(cmd_rules, "do_add_rule_verified", side_effect=add):
724
+ return self.run_form_rule(events, exact_events)
725
+
726
+ def run_form_rule(self, events=None, exact_events=None):
727
+ return cmd_rules.do_ensure_conditional_field_state(
728
+ {}, "token", "demo", None, "数量", "greater-than", "2",
729
+ "单价", "hide", events, exact_events,
730
+ )
731
+
732
+ def test_form_rule_add_event_compiles_both_branches_in_one_write(self):
733
+ self.create_form_rule(["复制后"])
734
+ self.assertEqual(1275, self.rules[0]["TrueActions"][0]["RET"])
735
+ self.assertEqual(1275, self.rules[0]["FalseActions"][0]["RET"])
736
+
737
+ def test_new_form_rule_exact_events_match_requested_timings(self):
738
+ self.create_form_rule(exact_events=["加载", "字段值变化", "复制后"])
739
+ self.assertEqual(1027, self.rules[0]["TrueActions"][0]["RET"])
740
+ self.assertEqual(1027, self.rules[0]["FalseActions"][0]["RET"])
741
+
742
+ def test_existing_form_rule_appends_event_with_one_update(self):
743
+ self.create_form_rule()
744
+ original = copy.deepcopy(self.rules[0])
745
+ def write(_cfg, _token, command, payload, **kwargs):
746
+ self.assertEqual("updateRule", command)
747
+ self.assertEqual("form-rule-id", payload["ruleId"])
748
+ for branch in ("TrueActions", "FalseActions"):
749
+ self.rules[0][branch] = copy.deepcopy(payload[branch[0].lower() + branch[1:]])
750
+ self.rules[0][branch][0]["Id"] = branch + "-new-id"
751
+ return {"status": True, "data": {"ruleId": "form-rule-id"}}
752
+ with patch.object(cmd_rules, "_api_call", side_effect=write) as writer:
753
+ result = self.run_form_rule(["AfterCopy"])
754
+ self.assertTrue(result["verification"]["preservationVerified"])
755
+ self.assertEqual(1, writer.call_count)
756
+ self.assertFalse(self.run_form_rule(["AfterCopy"])["writeAttempted"])
757
+ self.assertEqual(1, writer.call_count)
758
+ self.assertEqual(original["Id"], self.rules[0]["Id"])
759
+ for branch in ("TrueActions", "FalseActions"):
760
+ self.assertEqual(1275, self.rules[0][branch][0]["RET"])
761
+ self.assertEqual(original[branch][0]["Fields"], self.rules[0][branch][0]["Fields"])
762
+
763
+ def test_new_clear_rule_compiles_named_after_copy(self):
764
+ self.create(["复制后"])
765
+ self.assertEqual(1277, self.rules[0]["TrueActions"][0]["RET"])
766
+ self.assertIn("数量", self.rules[0]["Description"])
767
+ self.assertTrue(self.rules[0]["Description"].endswith("时清空单价"))
768
+ self.assertNotIn("[CCF_", self.rules[0]["Description"])
769
+
770
+ def test_legacy_hash_description_is_renamed_without_recreating_rule(self):
771
+ for events, tagged in ((None, False), (["AfterCopy"], True)):
772
+ with self.subTest(events=events, tagged=tagged):
773
+ self.rules.clear()
774
+ self.create()
775
+ readable = self.rules[0]["Description"]
776
+ digest = hashlib.sha1("demo:demo:qty > 2:price".encode("utf-8")).hexdigest()[:12]
777
+ self.rules[0]["Description"] = (
778
+ f"{readable} [CCF_{digest}]" if tagged
779
+ else f"ConditionalClearFields_{digest}"
780
+ )
781
+ original_action = copy.deepcopy(self.rules[0]["TrueActions"][0])
782
+
783
+ def write(_cfg, _token, command, payload, **kwargs):
784
+ self.assertEqual("updateRule", command)
785
+ self.assertEqual("rule-id", payload["ruleId"])
786
+ if "description" in payload:
787
+ self.rules[0]["Description"] = payload["description"]
788
+ if "trueActions" in payload:
789
+ self.rules[0]["TrueActions"] = copy.deepcopy(payload["trueActions"])
790
+ return {"status": True, "data": {"ruleId": "rule-id"}}
791
+
792
+ with patch.object(cmd_rules, "_api_call", side_effect=write) as writer:
793
+ result = self.run_clear(events)
794
+ self.assertTrue(result["status"])
795
+ self.assertTrue(result["verification"]["preservationVerified"])
796
+ self.assertEqual(1, writer.call_count)
797
+ self.assertFalse(self.run_clear(events)["writeAttempted"])
798
+ self.assertEqual(1, writer.call_count)
799
+ self.assertEqual(readable, self.rules[0]["Description"])
800
+ self.assertEqual("rule-id", self.rules[0]["Id"])
801
+ self.assertEqual(original_action["Id"], self.rules[0]["TrueActions"][0]["Id"])
802
+ self.assertEqual(
803
+ 1277 if events else original_action["RET"],
804
+ self.rules[0]["TrueActions"][0]["RET"],
805
+ )
806
+
807
+ def test_existing_rule_appends_event_preserving_full_rule_and_is_idempotent(self):
808
+ self.create()
809
+ self.rules[0]["TrueActions"][0]["RET"] = 1021
810
+ original = copy.deepcopy(self.rules[0])
811
+
812
+ def write(_cfg, _token, command, payload, **kwargs):
813
+ self.assertEqual("updateRule", command)
814
+ self.assertEqual("rule-id", payload["ruleId"])
815
+ self.rules[0]["TrueActions"] = copy.deepcopy(payload["trueActions"])
816
+ self.rules[0]["TrueActions"][0]["Id"] = "platform-new-action-id"
817
+ return {"status": True, "data": {"ruleId": "rule-id"}}
818
+
819
+ with patch.object(cmd_rules, "_api_call", side_effect=write) as writer:
820
+ result = self.run_clear(["AfterCopy"])
821
+ self.assertTrue(result["status"])
822
+ self.assertTrue(result["verification"]["preservationVerified"])
823
+ expected = copy.deepcopy(original)
824
+ expected["TrueActions"][0]["RET"] = 2045
825
+ expected["TrueActions"][0]["Id"] = "platform-new-action-id"
826
+ self.assertEqual(expected, self.rules[0])
827
+ self.assertEqual(1, writer.call_count)
828
+ self.assertFalse(self.run_clear(["复制后"])["writeAttempted"])
829
+ self.assertFalse(self.run_clear()["writeAttempted"])
830
+ self.assertEqual(1, writer.call_count)
831
+
832
+ def test_readback_rejects_lost_event_or_unrelated_rule_changes(self):
833
+ for corruption in ("event", "condition"):
834
+ with self.subTest(corruption=corruption):
835
+ self.rules.clear()
836
+ self.create()
837
+
838
+ def write(_cfg, _token, command, payload, **kwargs):
839
+ self.rules[0]["TrueActions"] = copy.deepcopy(payload["trueActions"])
840
+ if corruption == "event":
841
+ self.rules[0]["TrueActions"][0]["RET"] = 253
842
+ else:
843
+ self.rules[0]["PreCondition"] = "qty > 100"
844
+ return {"status": True, "data": {"ruleId": "rule-id"}}
845
+
846
+ with patch.object(cmd_rules, "_api_call", side_effect=write) as writer:
847
+ result = self.run_clear(["AfterCopy"])
848
+ self.assertFalse(result["status"])
849
+ self.assertFalse(result["verification"]["verified"])
850
+ self.assertEqual(1, writer.call_count)
851
+
852
+
129
853
  class RuleWriteClosureTest(unittest.TestCase):
854
+ @patch.object(cmd_rules, "do_add_rule_verified")
855
+ @patch.object(cmd_rules, "do_load_full_metadata")
856
+ @patch.object(cmd_rules, "do_list_elements")
857
+ def test_conditional_field_change_compiles_nonempty_and_copy_timing(
858
+ self, list_elements, load_full, add_verified,
859
+ ) -> None:
860
+ list_elements.side_effect = lambda *args, **kwargs: {
861
+ "status": True,
862
+ "data": ([{
863
+ "key": "demo", "name": "测试单", "id": "entity-id", "type": "BillEntity",
864
+ }] if kwargs["category"] == "entity" else [
865
+ {"key": "material", "name": "物料", "parentKey": "demo", "type": "MaterielField"},
866
+ {"key": "price", "name": "单价", "parentKey": "demo", "type": "PriceField"},
867
+ ]),
868
+ }
869
+ load_full.return_value = {"status": True, "data": {"entitymeta": {"Items": []}}}
870
+ add_verified.return_value = {
871
+ "status": True, "data": {"ruleId": "rule-id"},
872
+ "verification": {"verified": True},
873
+ "runtimeVerification": {"verified": False},
874
+ }
875
+ result = cmd_rules.do_ensure_conditional_field_change(
876
+ {}, "token", "demo", None, "物料", "not-empty", None,
877
+ "单价", events=["字段值变化", "复制后"],
878
+ )
879
+ requested = add_verified.call_args.args[3]
880
+ self.assertEqual("(material <> null and material <> '')", requested["preCondition"])
881
+ self.assertEqual("物料(material)", requested["triggerField"])
882
+ self.assertEqual("price", requested["trueActions"][0]["expression"])
883
+ self.assertEqual(1025, requested["trueActions"][0]["ret"])
884
+ self.assertEqual("物料不为空时调用单价字段值更新服务", requested["description"])
885
+ self.assertTrue(result["writeAttempted"])
886
+ self.assertFalse(result["runtimeVerification"]["verified"])
887
+
888
+ @patch.object(cmd_rules, "_api_call")
889
+ @patch.object(cmd_rules, "do_add_rule_verified")
890
+ @patch.object(cmd_rules, "do_load_full_metadata")
891
+ @patch.object(cmd_rules, "do_list_elements")
892
+ def test_conditional_field_change_adopts_existing_rule_and_adds_timing_once(
893
+ self, list_elements, load_full, add_verified, api_call,
894
+ ) -> None:
895
+ list_elements.side_effect = lambda *args, **kwargs: {
896
+ "status": True,
897
+ "data": ([{"key": "demo", "name": "测试单", "id": "entity-id", "type": "BillEntity"}]
898
+ if kwargs["category"] == "entity" else [
899
+ {"key": "material", "name": "物料", "parentKey": "demo", "type": "MaterielField"},
900
+ {"key": "price", "name": "单价", "parentKey": "demo", "type": "PriceField"},
901
+ ]),
902
+ }
903
+ stored = []
904
+ load_full.side_effect = lambda *args: {
905
+ "status": True, "data": {"entitymeta": {"Items": copy.deepcopy(stored)}},
906
+ }
907
+ def add(_cfg, _token, _form, requested):
908
+ action = rule_contract.normalize_rule_config(requested)["trueActions"][0]
909
+ action.update({"Id": "original-action", "IsvFlag": "original"})
910
+ stored.append({
911
+ "_Type_": "BizRule", "Id": "existing-rule", "ItemId": "entity-id",
912
+ "Description": "原有规则名称", "PreDescription": requested["preDescription"],
913
+ "PreCondition": requested["preCondition"],
914
+ "TriggerField": requested["triggerField"], "Enabled": True,
915
+ "TrueActions": [action], "FalseActions": [],
916
+ })
917
+ return {"status": True, "data": {"ruleId": "existing-rule"},
918
+ "verification": {"verified": True},
919
+ "runtimeVerification": {"verified": False}}
920
+ add_verified.side_effect = add
921
+ def write(_cfg, _token, command, payload, **kwargs):
922
+ self.assertEqual("updateRule", command)
923
+ self.assertEqual("existing-rule", payload["ruleId"])
924
+ stored[0]["TrueActions"] = copy.deepcopy(payload["trueActions"])
925
+ stored[0]["TrueActions"][0]["Id"] = "replacement-action"
926
+ return {"status": True, "data": {"ruleId": "existing-rule"}}
927
+ api_call.side_effect = write
928
+
929
+ cmd_rules.do_ensure_conditional_field_change(
930
+ {}, "token", "demo", None, "物料", "not-empty", None, "单价",
931
+ )
932
+ result = cmd_rules.do_ensure_conditional_field_change(
933
+ {}, "token", "demo", None, "物料", "not-empty", None,
934
+ "单价", ["AfterCopy"],
935
+ )
936
+ self.assertTrue(result["verification"]["preservationVerified"])
937
+ self.assertEqual("existing-rule", result["data"]["ruleId"])
938
+ self.assertEqual("原有规则名称", stored[0]["Description"])
939
+ self.assertEqual(1797, stored[0]["TrueActions"][0]["RET"])
940
+ self.assertEqual(1, api_call.call_count)
941
+ repeated = cmd_rules.do_ensure_conditional_field_change(
942
+ {}, "token", "demo", None, "物料", "not-empty", None,
943
+ "单价", ["复制后"],
944
+ )
945
+ self.assertFalse(repeated["writeAttempted"])
946
+ self.assertFalse(repeated["runtimeVerification"]["verified"])
947
+ self.assertEqual(1, api_call.call_count)
948
+ self.assertEqual(1, add_verified.call_count)
949
+
950
+ @patch.object(cmd_rules, "do_load_full_metadata")
951
+ @patch.object(cmd_rules, "_api_call")
952
+ def test_generated_business_rule_rename_preserves_actions_and_identity(
953
+ self, api_call, load_full,
954
+ ) -> None:
955
+ stored = {
956
+ "_Type_": "BizRule", "Id": "rule-id", "IsvFlag": "original",
957
+ "Description": "QuantityConversion_123456789abc",
958
+ "PreDescription": "数量变化时换算基本数量",
959
+ "PreCondition": "qty = qty", "TriggerField": "数量(qty)",
960
+ "TrueActions": [{
961
+ "ActionType": "FillQtyAction", "Expression": "[]", "RET": 249,
962
+ "Id": "action-id", "IsvFlag": "owner",
963
+ }], "FalseActions": [],
964
+ }
965
+ before = copy.deepcopy(stored)
966
+ load_full.side_effect = lambda *args: {
967
+ "status": True, "data": {"entitymeta": {"Items": [copy.deepcopy(stored)]}},
968
+ }
969
+ def write(_cfg, _token, _command, payload, **kwargs):
970
+ stored["Description"] = payload["description"]
971
+ return {"status": True, "data": {"ruleId": "rule-id"}}
972
+ api_call.side_effect = write
973
+
974
+ result = cmd_rules._upgrade_generated_rule_description(
975
+ {}, "token", "demo", before,
976
+ before["Description"], "数量变化时换算基本数量",
977
+ )
978
+
979
+ self.assertTrue(result["verification"]["preservationVerified"])
980
+ self.assertTrue(result["data"]["changed"])
981
+ self.assertEqual(before["Id"], stored["Id"])
982
+ self.assertEqual(before["IsvFlag"], stored["IsvFlag"])
983
+ self.assertEqual(before["TrueActions"], stored["TrueActions"])
984
+
130
985
  @patch.object(cmd_rules, "do_delete_rule_verified")
131
986
  def test_first_option_predecessor_is_removed_when_replacement_exists(
132
987
  self, delete_verified
@@ -152,6 +1007,29 @@ class RuleWriteClosureTest(unittest.TestCase):
152
1007
  )
153
1008
  self.assertTrue(result["data"]["removedFirstOptionRule"])
154
1009
 
1010
+ @patch.object(cmd_rules, "do_delete_rule_verified")
1011
+ def test_readable_clear_rule_keeps_first_option_repair_path(self, delete_verified):
1012
+ delete_verified.return_value = {
1013
+ "status": True, "data": {}, "verification": {"verified": True},
1014
+ }
1015
+ metadata = {"entitymeta": {"Items": [{
1016
+ "_Type_": "BizRule", "Id": "old-rule",
1017
+ "Description": "状态为第一项时清空备注 [CCF_123456789abc]",
1018
+ "PreDescription": "状态 = @first 时清空备注",
1019
+ "TriggerField": "状态(status)",
1020
+ }]}}
1021
+ result = cmd_rules._repair_first_option_rule(
1022
+ {}, "token", "demo", metadata, {"description": "new"},
1023
+ rule_type="BizRule", family="ConditionalClearFields_",
1024
+ family_marker=" [CCF_", entity_id="entity-id",
1025
+ trigger_field="状态(status)", markers=["清空", "备注"],
1026
+ replacement_exists=True,
1027
+ )
1028
+ delete_verified.assert_called_once_with(
1029
+ {}, "token", "demo", "bizRule", "old-rule",
1030
+ )
1031
+ self.assertTrue(result["data"]["removedFirstOptionRule"])
1032
+
155
1033
  def test_set_visible_action_is_rejected_as_a_schema_only_alias(self) -> None:
156
1034
  with self.assertRaisesRegex(
157
1035
  ValueError,
@@ -900,8 +1778,12 @@ class RuleWriteClosureTest(unittest.TestCase):
900
1778
  @patch.object(cmd_rules, "do_get_properties")
901
1779
  @patch.object(cmd_rules, "do_load_full_metadata")
902
1780
  @patch.object(cmd_rules, "do_list_elements")
1781
+ @patch.object(cmd_rules, "_reference_entity_field_candidates", return_value=[{
1782
+ "key": "linkman", "name": "联系人", "type": "TextField", "referenceCollection": False,
1783
+ }, {"key": "number", "name": "编码", "type": "TextField", "referenceCollection": False},
1784
+ {"key": "category", "name": "分类", "type": "BasedataField", "referenceCollection": False}])
903
1785
  def test_reference_carry_defaults_to_source_not_empty(
904
- self, list_elements, load_full, get_properties
1786
+ self, reference_fields, list_elements, load_full, get_properties
905
1787
  ) -> None:
906
1788
  list_elements.side_effect = lambda *args, **kwargs: {
907
1789
  "status": True,
@@ -913,7 +1795,7 @@ class RuleWriteClosureTest(unittest.TestCase):
913
1795
  ]),
914
1796
  }
915
1797
  get_properties.return_value = {
916
- "status": True, "data": {"refProps": '[{"name":"linkman"}]'},
1798
+ "status": True, "data": {"refProps": '[{"name":"linkman"}]', "baseEntityId": "reference-id"},
917
1799
  }
918
1800
  load_full.return_value = {"status": True, "data": {"entitymeta": {"Items": []}}}
919
1801
  with patch.object(
@@ -936,8 +1818,12 @@ class RuleWriteClosureTest(unittest.TestCase):
936
1818
  @patch.object(cmd_rules, "do_get_properties")
937
1819
  @patch.object(cmd_rules, "do_load_full_metadata")
938
1820
  @patch.object(cmd_rules, "do_list_elements")
1821
+ @patch.object(cmd_rules, "_reference_entity_field_candidates", return_value=[{
1822
+ "key": "linkman", "name": "联系人", "type": "TextField", "referenceCollection": False,
1823
+ }, {"key": "number", "name": "编码", "type": "TextField", "referenceCollection": False},
1824
+ {"key": "category", "name": "分类", "type": "BasedataField", "referenceCollection": False}])
939
1825
  def test_reference_carry_repairs_legacy_isnull_condition(
940
- self, list_elements, load_full, get_properties
1826
+ self, reference_fields, list_elements, load_full, get_properties
941
1827
  ) -> None:
942
1828
  list_elements.side_effect = lambda *args, **kwargs: {
943
1829
  "status": True,
@@ -949,7 +1835,7 @@ class RuleWriteClosureTest(unittest.TestCase):
949
1835
  ]),
950
1836
  }
951
1837
  get_properties.return_value = {
952
- "status": True, "data": {"refProps": '[{"name":"linkman"}]'},
1838
+ "status": True, "data": {"refProps": '[{"name":"linkman"}]', "baseEntityId": "reference-id"},
953
1839
  }
954
1840
  legacy = "NOT (isNull(supplier) or isEmpty(supplier))"
955
1841
  identity = ":".join((
@@ -1410,6 +2296,7 @@ class RuleWriteClosureTest(unittest.TestCase):
1410
2296
  requested = add_verified.call_args.args[3]
1411
2297
  action = requested["trueActions"][0]
1412
2298
  self.assertEqual("remark:note", action["expression"])
2299
+ self.assertEqual("数量 > 2 时清空备注、说明", requested["description"])
1413
2300
  self.assertEqual(253, action["ret"])
1414
2301
  self.assertEqual([], requested["falseActions"])
1415
2302
  self.assertTrue(first["writeAttempted"])
@@ -1536,6 +2423,7 @@ class RuleWriteClosureTest(unittest.TestCase):
1536
2423
  requested = add_verified.call_args.args[3]
1537
2424
  action = requested["trueActions"][0]
1538
2425
  self.assertEqual("SetEqutionAction", action["actionType"])
2426
+ self.assertEqual(requested["preDescription"], requested["description"])
1539
2427
  self.assertEqual(
1540
2428
  [{"TargetField": "target", "SrcField": "source", "OnlyEmptyRow": True}],
1541
2429
  action["expression"],
@@ -1568,8 +2456,12 @@ class RuleWriteClosureTest(unittest.TestCase):
1568
2456
  @patch.object(cmd_rules, "do_load_full_metadata")
1569
2457
  @patch.object(cmd_rules, "do_get_properties")
1570
2458
  @patch.object(cmd_rules, "do_list_elements")
2459
+ @patch.object(cmd_rules, "_reference_entity_field_candidates", return_value=[{
2460
+ "key": "linkman", "name": "联系人", "type": "TextField", "referenceCollection": False,
2461
+ }, {"key": "number", "name": "编码", "type": "TextField", "referenceCollection": False},
2462
+ {"key": "category", "name": "分类", "type": "BasedataField", "referenceCollection": False}])
1571
2463
  def test_conditional_carry_reference_property_validates_refprops_and_adopts(
1572
- self, list_elements, get_properties, load_full, add_verified
2464
+ self, reference_fields, list_elements, get_properties, load_full, add_verified
1573
2465
  ) -> None:
1574
2466
  list_elements.side_effect = lambda *args, **kwargs: {
1575
2467
  "status": True,
@@ -1582,7 +2474,7 @@ class RuleWriteClosureTest(unittest.TestCase):
1582
2474
  ]),
1583
2475
  }
1584
2476
  get_properties.return_value = {
1585
- "status": True, "data": {"refProps": '[{"name":"number"}]'}
2477
+ "status": True, "data": {"refProps": '[{"name":"number"}]', "baseEntityId": "reference-id"}
1586
2478
  }
1587
2479
  load_full.return_value = {"status": True, "data": {"entitymeta": {"Items": []}}}
1588
2480
  add_verified.return_value = {
@@ -1596,6 +2488,7 @@ class RuleWriteClosureTest(unittest.TestCase):
1596
2488
  requested = add_verified.call_args.args[3]
1597
2489
  action = requested["trueActions"][0]
1598
2490
  self.assertEqual("TakeBaseProAction", action["actionType"])
2491
+ self.assertEqual(requested["preDescription"], requested["description"])
1599
2492
  self.assertEqual(
1600
2493
  [{"TargetField": "org_number", "SrcField": "org.number"}],
1601
2494
  action["expression"],
@@ -1622,7 +2515,7 @@ class RuleWriteClosureTest(unittest.TestCase):
1622
2515
  add_verified.assert_not_called()
1623
2516
 
1624
2517
  get_properties.return_value = {
1625
- "status": True, "data": {"refProps": '[{"name":"number"}]'}
2518
+ "status": True, "data": {"refProps": '[{"name":"number"}]', "baseEntityId": "reference-id"}
1626
2519
  }
1627
2520
  with self.assertRaisesRegex(
1628
2521
  ValueError, "CONDITIONAL_CARRY_SOURCE_PROPERTY_UNAVAILABLE.*number"
@@ -1885,17 +2778,77 @@ class RuleWriteClosureTest(unittest.TestCase):
1885
2778
  self.assertEqual(1021, written["RET"])
1886
2779
  self.assertEqual(1, written["Seq"])
1887
2780
 
1888
- @patch.object(cmd_rules, "_api_call")
1889
- def test_call_field_change_is_blocked_after_runtime_failure(self, api_call) -> None:
2781
+ @patch.object(cmd_rules, "do_list_elements")
2782
+ def test_call_field_change_resolves_target_and_preserves_runtime_boundary(
2783
+ self, list_elements,
2784
+ ) -> None:
2785
+ list_elements.return_value = {"status": True, "data": [
2786
+ {"key": "uhyl_target", "name": "单价", "parentKey": "uhyl_demo"},
2787
+ ]}
1890
2788
  requested = _biz_rule({
1891
2789
  "actionType": "CallFieldChangeAction",
1892
- "expression": "uhyl_target",
2790
+ "expression": "单价",
1893
2791
  })
1894
- with self.assertRaisesRegex(
1895
- ValueError, "RULE_ACTION_CONTRACT_BLOCKED: CallFieldChangeAction"
1896
- ):
1897
- cmd_rules.do_add_rule_verified({}, "token", "uhyl_demo", requested)
1898
- api_call.assert_not_called()
2792
+ resolved = cmd_rules._resolve_call_field_change_targets(
2793
+ {}, "token", "uhyl_demo", requested,
2794
+ )
2795
+ self.assertEqual("单价", requested["trueActions"][0]["expression"])
2796
+ self.assertEqual("uhyl_target", resolved["trueActions"][0]["expression"])
2797
+ action = rule_contract.normalize_rule_config(resolved)["trueActions"][0]
2798
+ self.assertEqual(773, action["RET"])
2799
+ self.assertEqual("uhyl_target", action["Expression"])
2800
+ self.assertEqual("CallFieldChangeAction", rule_contract.runtime_action_warnings(
2801
+ resolved,
2802
+ )[0]["actionType"])
2803
+
2804
+ @patch.object(cmd_rules, "do_list_elements")
2805
+ def test_call_field_change_rejects_ambiguous_or_foreign_target(
2806
+ self, list_elements,
2807
+ ) -> None:
2808
+ list_elements.return_value = {"status": True, "data": [
2809
+ {"key": "head_price", "name": "单价", "parentKey": "uhyl_demo"},
2810
+ {"key": "entry_price", "name": "单价", "parentKey": "entry"},
2811
+ ]}
2812
+ requested = _biz_rule({
2813
+ "actionType": "CallFieldChangeAction", "expression": "entry_price",
2814
+ })
2815
+ with self.assertRaisesRegex(ValueError, "RULE_CALL_FIELD_TARGET_UNRESOLVED"):
2816
+ cmd_rules._resolve_call_field_change_targets(
2817
+ {}, "token", "uhyl_demo", requested,
2818
+ )
2819
+ del requested["mountPoint"]
2820
+ requested["trueActions"][0]["expression"] = "单价"
2821
+ with self.assertRaisesRegex(ValueError, "RULE_CALL_FIELD_TARGET_AMBIGUOUS"):
2822
+ cmd_rules._resolve_call_field_change_targets(
2823
+ {}, "token", "uhyl_demo", requested,
2824
+ )
2825
+
2826
+ @patch.object(cmd_rules, "_ensure_rule_trigger_events", return_value=[])
2827
+ @patch.object(cmd_rules, "_attach_rule_readback")
2828
+ @patch.object(cmd_rules, "do_add_rule")
2829
+ @patch.object(cmd_rules, "do_list_elements")
2830
+ @patch.object(cmd_rules, "_resolve_rule_entity")
2831
+ def test_call_field_change_receipt_does_not_claim_runtime_verified(
2832
+ self, resolve_entity, list_elements, add_rule, readback, _trigger,
2833
+ ) -> None:
2834
+ requested = _biz_rule({
2835
+ "actionType": "CallFieldChangeAction", "expression": "单价",
2836
+ })
2837
+ resolve_entity.return_value = requested
2838
+ list_elements.return_value = {"status": True, "data": [
2839
+ {"key": "uhyl_target", "name": "单价", "parentKey": "uhyl_demo"},
2840
+ ]}
2841
+ add_rule.return_value = {"status": True, "data": {"ruleId": "rule-1"}}
2842
+ readback.return_value = {
2843
+ "status": True, "data": {"ruleId": "rule-1"},
2844
+ "verification": {"verified": True},
2845
+ }
2846
+ result = cmd_rules.do_add_rule_verified(
2847
+ {}, "token", "uhyl_demo", requested,
2848
+ )
2849
+ self.assertEqual("uhyl_target", add_rule.call_args.args[3]["trueActions"][0]["expression"])
2850
+ self.assertTrue(result["verification"]["verified"])
2851
+ self.assertFalse(result["runtimeVerification"]["verified"])
1899
2852
 
1900
2853
  def test_fill_qty_compiles_real_expression_and_ret(self) -> None:
1901
2854
  expression = '[{"distQtyField":"uhyl_base_qty","srcQtyField":"uhyl_qty"}]'
@@ -1956,11 +2909,83 @@ class RuleWriteClosureTest(unittest.TestCase):
1956
2909
  requested["triggerField"],
1957
2910
  )
1958
2911
  action = requested["trueActions"][0]
2912
+ self.assertEqual(requested["preDescription"], requested["description"])
1959
2913
  self.assertEqual(
1960
2914
  "uhyl_result = uhyl_left + uhyl_right", action["expression"]
1961
2915
  )
1962
2916
  self.assertEqual(253, action["ret"])
1963
2917
 
2918
+ @patch.object(cmd_rules, "do_add_rule_verified")
2919
+ @patch.object(cmd_rules, "do_load_full_metadata")
2920
+ @patch.object(cmd_rules, "do_list_elements")
2921
+ def test_time_difference_compiles_decimal_hours_and_empty_branch(
2922
+ self, list_elements, load_full, add_verified,
2923
+ ) -> None:
2924
+ list_elements.side_effect = [
2925
+ {"status": True, "data": [{
2926
+ "key": "uhyl_demo", "name": "演示单", "id": "entity-id",
2927
+ "type": "MainEntity",
2928
+ }]},
2929
+ {"status": True, "data": [
2930
+ {"key": "uhyl_start", "name": "开始时间", "type": "TimeField",
2931
+ "parentKey": "uhyl_demo"},
2932
+ {"key": "uhyl_end", "name": "结束时间", "type": "TimeField",
2933
+ "parentKey": "uhyl_demo"},
2934
+ {"key": "uhyl_hours", "name": "时长(小时)", "type": "DecimalField",
2935
+ "parentKey": "uhyl_demo"},
2936
+ ]},
2937
+ ]
2938
+ load_full.return_value = {"status": True, "data": {"entitymeta": {}}}
2939
+ add_verified.return_value = {
2940
+ "status": True, "data": {"ruleId": "time-rule"},
2941
+ "verification": {"verified": True},
2942
+ }
2943
+
2944
+ result = cmd_rules.do_ensure_time_difference_calculation(
2945
+ {}, "token", "uhyl_demo", "演示单", "时长(小时)",
2946
+ "开始时间", "结束时间",
2947
+ )
2948
+
2949
+ self.assertTrue(result["writeAttempted"])
2950
+ requested = add_verified.call_args.args[3]
2951
+ self.assertEqual(
2952
+ "uhyl_start >= 0 and uhyl_end >= 0", requested["preCondition"]
2953
+ )
2954
+ self.assertEqual(
2955
+ "开始时间(uhyl_start);结束时间(uhyl_end)", requested["triggerField"]
2956
+ )
2957
+ self.assertEqual(
2958
+ "uhyl_hours = (uhyl_end - uhyl_start) / 3600.0",
2959
+ requested["trueActions"][0]["expression"],
2960
+ )
2961
+ self.assertEqual(1021, requested["trueActions"][0]["ret"])
2962
+ self.assertEqual("uhyl_hours", requested["falseActions"][0]["expression"])
2963
+ self.assertEqual("hours", result["data"]["unit"])
2964
+
2965
+ @patch.object(cmd_rules, "do_list_elements")
2966
+ def test_time_difference_requires_time_operands(self, list_elements) -> None:
2967
+ list_elements.side_effect = [
2968
+ {"status": True, "data": [{
2969
+ "key": "uhyl_demo", "name": "演示单", "id": "entity-id",
2970
+ "type": "MainEntity",
2971
+ }]},
2972
+ {"status": True, "data": [
2973
+ {"key": "uhyl_start", "name": "开始时间", "type": "DecimalField",
2974
+ "parentKey": "uhyl_demo"},
2975
+ {"key": "uhyl_end", "name": "结束时间", "type": "TimeField",
2976
+ "parentKey": "uhyl_demo"},
2977
+ {"key": "uhyl_hours", "name": "时长", "type": "DecimalField",
2978
+ "parentKey": "uhyl_demo"},
2979
+ ]},
2980
+ ]
2981
+ with self.assertRaisesRegex(
2982
+ ValueError, "TIME_DIFFERENCE_TIME_FIELD_REQUIRED.*DecimalField"
2983
+ ):
2984
+ cmd_rules.do_ensure_time_difference_calculation(
2985
+ {}, "token", "uhyl_demo", "演示单", "时长",
2986
+ "开始时间", "结束时间",
2987
+ )
2988
+
1964
2989
  def test_fill_qty_rejects_guessed_legacy_mapping_keys(self) -> None:
1965
2990
  with self.assertRaisesRegex(
1966
2991
  ValueError, "RULE_FILL_QTY_MAPPING_INVALID"
@@ -2012,6 +3037,7 @@ class RuleWriteClosureTest(unittest.TestCase):
2012
3037
  self.assertTrue(result["writeAttempted"])
2013
3038
  requested = add_verified.call_args.args[3]
2014
3039
  action = requested["trueActions"][0]
3040
+ self.assertEqual("数量变化时换算基本数量", requested["description"])
2015
3041
  self.assertEqual(249, action["ret"])
2016
3042
  self.assertEqual(
2017
3043
  '[{"distQtyField":"uhyl_base_qty","srcQtyField":"uhyl_qty"}]',