@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
@@ -0,0 +1,171 @@
1
+ """Durable per-artifact progress inside the existing runBatch result contract."""
2
+ import copy
3
+ import csv
4
+ import hashlib
5
+ import json
6
+ import os
7
+ from pathlib import Path
8
+ import sys
9
+ import threading
10
+ import subprocess
11
+ import time
12
+
13
+ from .execution_errors import classify_execution_error
14
+ from execution_binding import execution_binding
15
+
16
+
17
+ def _atomic_json(path, value):
18
+ temporary = path.with_name(path.name + ".tmp")
19
+ temporary.write_text(json.dumps(value, ensure_ascii=False, indent=2), encoding="utf-8")
20
+ os.replace(temporary, path)
21
+
22
+
23
+ def _path(value):
24
+ return os.path.normcase(os.path.realpath(value))
25
+
26
+
27
+ def _targets(artifact):
28
+ return sorted(str(row.get("entityKey") or "").strip() for row in artifact.get("entities", [])
29
+ if isinstance(row, dict) and not row.get("parentEntityId"))
30
+
31
+
32
+ class BuildReceipts:
33
+ def __init__(self, app_root, app_id, environment, artifacts, *, resume_result=None, options=None, reconcile_only=False):
34
+ self.root = Path(app_root).resolve()
35
+ self.output = self.root / "_runtime" / "output"
36
+ self.output.mkdir(parents=True, exist_ok=True)
37
+ self.lock = threading.Lock()
38
+ self.artifacts = artifacts
39
+ self.options = options or []
40
+ self.identity = {"appRoot": str(self.root), "bizAppId": str(app_id), "environment": environment, "executionOptions": self.options}
41
+ self.previous = {}
42
+ if resume_result:
43
+ prior = json.loads(Path(resume_result).read_text(encoding="utf-8-sig"))
44
+ if reconcile_only and not prior.get("executionFinished"):
45
+ pid = prior.get("processId")
46
+ if not isinstance(pid, int):
47
+ raise ValueError("BATCH_WRITE_RECONCILIATION_REQUIRED: legacy receipt has no process boundary")
48
+ if os.name == "nt":
49
+ tasks = subprocess.check_output(["tasklist", "/FI", f"PID eq {pid}", "/FO", "CSV", "/NH"], text=True)
50
+ active = any(len(row) > 1 and row[1] == str(pid) for row in csv.reader(tasks.splitlines()))
51
+ else:
52
+ try:
53
+ os.kill(pid, 0)
54
+ active = True
55
+ except ProcessLookupError:
56
+ active = False
57
+ if active:
58
+ raise ValueError("BATCH_WRITE_RECONCILIATION_REQUIRED: original execution is still active")
59
+ if prior.get("batchIdentity") != self.identity:
60
+ raise ValueError("BATCH_INPUT_IDENTITY_CHANGED: resume requires the same application and environment")
61
+ if prior.get("executionBinding") and prior["executionBinding"] != execution_binding() and not reconcile_only:
62
+ raise ValueError("SKILL_REVISION_MISMATCH: reconcile the old receipt with the current bound source first")
63
+ self.previous = {_path(row["artifactPath"]): row for row in prior.get("results", [])}
64
+ if set(self.previous) != {_path(p) for p in artifacts}:
65
+ raise ValueError("BATCH_INPUT_ARTIFACTS_CHANGED: resume with the original paths; correct only unwritten inputs in place")
66
+ self.receipt = {"processId": os.getpid(), "executionBinding": execution_binding(), "batchIdentity": self.identity, "appRoot": str(self.root),
67
+ "artifactPaths": list(artifacts), "results": [],
68
+ "resultPath": str(self.output / f"runBatch.{time.time_ns()}.json")}
69
+ self.result_path = Path(self.receipt["resultPath"])
70
+ seen = set()
71
+ for source, artifact in artifacts.items():
72
+ targets = _targets(artifact)
73
+ if (not targets or any(not target for target in targets)
74
+ or len({number.casefold() for number in targets}) != len(targets)
75
+ or seen.intersection(number.casefold() for number in targets)):
76
+ raise ValueError("BATCH_INPUT_TARGET_CONFLICT: each artifact needs unique exact root entity keys")
77
+ seen.update(number.casefold() for number in targets)
78
+ revision = hashlib.sha256(Path(source).read_bytes()).hexdigest()
79
+ prior = self.previous.get(_path(source))
80
+ if prior and (prior.get("ok") or prior.get("writeAttempted") is not False):
81
+ if not reconcile_only and (not prior.get("ok") or (prior.get("verification") or {}).get("status") != "pass"):
82
+ raise ValueError("BATCH_WRITE_RECONCILIATION_REQUIRED: inspect retained/unknown targets from " + str(resume_result))
83
+ if prior.get("inputRevision") != revision:
84
+ raise ValueError("BATCH_INPUT_SAVED_TARGET_CHANGED: use an existing-form writer for the affected object and references")
85
+ row = copy.deepcopy(prior) if prior and (prior.get("ok") or reconcile_only) else {
86
+ "artifact": Path(source).name, "artifactPath": source, "inputRevision": revision,
87
+ "targetNumbers": targets, "ok": False, "phase": "notStarted",
88
+ "writeAttempted": False, "elapsed_sec": 0.0,
89
+ }
90
+ row["reusedReceipt"] = bool(prior and prior.get("ok"))
91
+ self.receipt["results"].append(row)
92
+ self._save()
93
+
94
+ def _save(self):
95
+ rows = self.receipt["results"]
96
+ self.receipt["success"] = sum(row.get("ok") is True for row in rows)
97
+ self.receipt["failed"] = len(rows) - self.receipt["success"]
98
+ self.receipt["total"] = len(rows)
99
+ self.receipt["resume"] = {"python": sys.executable, "entry": execution_binding()["cosmicApi"], "command": "runBatch",
100
+ "arguments": ["--stdout", "runBatch", "--app-root", str(self.root),
101
+ "--resume-result", str(self.result_path), "--verify",
102
+ *self.options, "--artifacts", *self.artifacts]}
103
+ unresolved = [row for row in rows if row.get("writeAttempted") is not False
104
+ and (not row.get("ok") or (row.get("verification") or {}).get("status") != "pass")]
105
+ self.receipt["resume"]["ready"] = not unresolved
106
+ self.receipt["reconcile"] = {"python": sys.executable, "entry": execution_binding()["cosmicApi"],
107
+ "arguments": [*self.receipt["resume"]["arguments"], "--reconcile-only"],
108
+ "requiresOriginalExecutionFinished": True, "platformEffect": "read"}
109
+ self.receipt["reconciliation"] = [{
110
+ "python": sys.executable, "entry": execution_binding()["cosmicApi"],
111
+ "artifactPath": row["artifactPath"],
112
+ "arguments": ["--stdout", "queryForms", "--app-root", str(self.root), "--keyword", number],
113
+ "instruction": "Read exact identity first; an arbitrary same-number object is not this batch's success receipt",
114
+ } for row in unresolved for number in row["targetNumbers"]]
115
+ self.receipt["notStartedArtifacts"] = [row["artifactPath"] for row in rows if row.get("writeAttempted") is False and not row.get("ok")]
116
+ _atomic_json(self.result_path, self.receipt)
117
+
118
+ def current(self, source):
119
+ return next(copy.deepcopy(row) for row in self.receipt["results"] if _path(row["artifactPath"]) == _path(source))
120
+
121
+ def reused(self, source):
122
+ return next((copy.deepcopy(row) for row in self.receipt["results"]
123
+ if _path(row["artifactPath"]) == _path(source) and row.get("reusedReceipt")), None)
124
+
125
+ def start(self, source):
126
+ """Claim exact target identities before dispatch; interrupted claims stay unknown."""
127
+ with self.lock:
128
+ row = next(row for row in self.receipt["results"] if _path(row["artifactPath"]) == _path(source))
129
+ claims = []
130
+ for number in row["targetNumbers"]:
131
+ key = hashlib.sha256(json.dumps([{key: value for key, value in self.identity.items() if key != "executionOptions"}, number.casefold()], sort_keys=True).encode()).hexdigest()
132
+ claim = self.output / f"runBatch.target.{key}.json"
133
+ if claim.exists():
134
+ owner = json.loads(claim.read_text(encoding="utf-8"))
135
+ raise ValueError("BATCH_WRITE_RECONCILIATION_REQUIRED: previous write receipt " + owner["resultPath"])
136
+ claims.append(claim)
137
+ row.update(phase="writeUnknown", writeAttempted=None,
138
+ claimPaths=[str(p) for p in claims], claimOwner=str(self.result_path))
139
+ self._save()
140
+ # O_EXCL protects independent processes as well as threads.
141
+ acquired = []
142
+ try:
143
+ for claim in claims:
144
+ with claim.open("x", encoding="utf-8") as handle:
145
+ json.dump({"resultPath": str(self.result_path), "artifactPath": source}, handle)
146
+ acquired.append(claim)
147
+ except FileExistsError:
148
+ for claim in acquired:
149
+ claim.unlink()
150
+ raise ValueError("BATCH_WRITE_RECONCILIATION_REQUIRED: concurrent creation owns this target")
151
+
152
+ def finish(self, source, result):
153
+ with self.lock:
154
+ row = next(row for row in self.receipt["results"] if _path(row["artifactPath"]) == _path(source))
155
+ row.update(result)
156
+ row["artifactPath"] = source
157
+ if row.get("writeAttempted") is False:
158
+ for claim in row.get("claimPaths", []):
159
+ path = Path(claim)
160
+ if path.is_file() and json.loads(path.read_text(encoding="utf-8")).get("resultPath") == row.get("claimOwner"):
161
+ path.unlink()
162
+ row.pop("claimPaths", None)
163
+ row.pop("claimOwner", None)
164
+ if row.get("ok"):
165
+ row.pop("errorCategory", None)
166
+ row.pop("capabilityLimitConfirmed", None)
167
+ if not row.get("ok"):
168
+ row["errorCategory"] = classify_execution_error(row)
169
+ row["capabilityLimitConfirmed"] = row["errorCategory"] == "capability_limit"
170
+ self._save()
171
+ return copy.deepcopy(row)
@@ -1280,13 +1280,32 @@ def do_add_unit(cfg, token, biz_app_id=None, biz_app_number=None,
1280
1280
  else:
1281
1281
  raise ValueError("addUnit 需要 --bizAppId 或 --bizAppNumber 之一")
1282
1282
 
1283
+ if not str(number or "").strip():
1284
+ return {
1285
+ "status": False, "errorCode": "FUNCTIONAL_GROUP_CODE_REQUIRED",
1286
+ "message": "addUnit requires a non-empty number; preserve the requested functional group code.",
1287
+ "writeAttempted": False,
1288
+ }
1283
1289
  payload["name"] = name
1284
- if number is not None:
1285
- payload["number"] = number
1286
- if description is not None:
1287
- payload["description"] = description
1288
- if seq is not None:
1289
- payload["seq"] = seq
1290
+ payload["number"] = number
1291
+ payload["description"] = name if description is None else description
1292
+ if seq is None:
1293
+ if parent_unit_id not in (None, ""):
1294
+ return {"status": False, "errorCode": "FUNCTIONAL_GROUP_SEQ_UNAVAILABLE", "message": "A nested group needs an explicit sibling --seq", "writeAttempted": False}
1295
+ try:
1296
+ existing = do_list_units(cfg, token, biz_app_id=biz_app_id, biz_app_number=biz_app_number)
1297
+ except Exception as exc:
1298
+ return {"status": False, "errorCode": "FUNCTIONAL_GROUP_SEQ_UNAVAILABLE", "message": f"Cannot choose the next group sequence: {exc}", "writeAttempted": False}
1299
+ units = (existing.get("data") or {}).get("units") if isinstance(existing, dict) else None
1300
+ if not isinstance(units, list) or not _is_api_success(existing):
1301
+ return {"status": False, "errorCode": "FUNCTIONAL_GROUP_SEQ_UNAVAILABLE", "message": "Cannot read existing groups to choose the next sequence", "writeAttempted": False}
1302
+ known = [int(unit["seq"]) if isinstance(unit, dict) and str(unit.get("seq") if unit.get("seq") is not None else "").isdigit() else None for unit in units]
1303
+ if any(value is None for value in known):
1304
+ return {"status": False, "errorCode": "FUNCTIONAL_GROUP_SEQ_UNAVAILABLE", "message": "Existing group sequence is unavailable; pass an explicit --seq", "writeAttempted": False}
1305
+ seq = max(known, default=0) + 1
1306
+ if not isinstance(seq, int) or not 1 <= seq < 1000:
1307
+ return {"status": False, "errorCode": "FUNCTIONAL_GROUP_SEQ_INVALID", "message": "Group sequence must be an integer from 1 to 999", "writeAttempted": False}
1308
+ payload["seq"] = seq
1290
1309
  if parent_unit_id is not None:
1291
1310
  payload["parentUnitId"] = parent_unit_id
1292
1311
  if is_leaf is not None:
@@ -27,6 +27,14 @@ _PARENT_TYPES = {
27
27
  }
28
28
 
29
29
 
30
+ class ControlInputError(ValueError):
31
+ def __init__(self, missing_fields):
32
+ super().__init__(
33
+ "CONTROL_INPUT_INVALID: required fields missing: " + ", ".join(missing_fields)
34
+ )
35
+ self.recovery = {"missingFields": missing_fields}
36
+
37
+
30
38
  @lru_cache(maxsize=1)
31
39
  def _control_contract():
32
40
  with (_CONTRACT or contract_path("control-authoring.json")).open("r", encoding="utf-8-sig") as stream:
@@ -132,7 +140,22 @@ def validate_control_batch_input(intent):
132
140
  unknown = sorted(set(intent) - {"parent", "panel", "controls"})
133
141
  if unknown:
134
142
  raise ValueError(f"CONTROL_INPUT_INVALID: unknown top-level keys {unknown}")
143
+ controls = intent.get("controls")
144
+ missing_fields = (["controls"] if controls is None else []) + [
145
+ f"controls[{index}].{field}"
146
+ for index, item in enumerate(controls if isinstance(controls, list) else [])
147
+ if isinstance(item, dict)
148
+ for field in ("type", "name") if not str(item.get(field) or "").strip()
149
+ ]
135
150
  panel_input = intent.get("panel")
151
+ if isinstance(panel_input, dict) and not str(panel_input.get("name") or "").strip():
152
+ missing_fields.append("panel.name")
153
+ if missing_fields:
154
+ raise ControlInputError(missing_fields)
155
+ if not isinstance(controls, list) or not controls:
156
+ raise ValueError("CONTROL_INPUT_INVALID: controls must be a non-empty array")
157
+ if len(controls) > 50:
158
+ raise ValueError("CONTROL_BATCH_TOO_LARGE: at most 50 controls per call")
136
159
  if panel_input is not None and intent.get("parent"):
137
160
  raise ValueError(
138
161
  "CONTROL_INPUT_INVALID: use panel.parent when panel is declared"
@@ -148,10 +171,6 @@ def validate_control_batch_input(intent):
148
171
  )
149
172
  panel_name = str(panel_input.get("name") or "").strip()
150
173
  panel_parent = str(panel_input.get("parent") or "").strip()
151
- if not panel_name or not panel_parent:
152
- raise ValueError(
153
- "CONTROL_INPUT_INVALID: panel.name and panel.parent are required"
154
- )
155
174
  panel_key = str(panel_input.get("key") or "").strip() or (
156
175
  "panel_" + hashlib.sha1(panel_name.encode("utf-8")).hexdigest()[:12]
157
176
  )
@@ -172,11 +191,6 @@ def validate_control_batch_input(intent):
172
191
  "controlDefaults": presentation,
173
192
  }
174
193
  default_parent = panel["key"] if panel else str(intent.get("parent") or "").strip()
175
- controls = intent.get("controls")
176
- if not isinstance(controls, list) or not controls:
177
- raise ValueError("CONTROL_INPUT_INVALID: controls must be a non-empty array")
178
- if len(controls) > 50:
179
- raise ValueError("CONTROL_BATCH_TOO_LARGE: at most 50 controls per call")
180
194
 
181
195
  contract = _control_contract()
182
196
  normalized = []
@@ -191,8 +205,6 @@ def validate_control_batch_input(intent):
191
205
  )
192
206
  control_type = _resolve_control_type(item.get("type"), contract)
193
207
  name = str(item.get("name") or "").strip()
194
- if not name:
195
- raise ValueError(f"CONTROL_INPUT_INVALID: controls[{index}].name is required")
196
208
  key = str(item.get("key") or "").strip() or (
197
209
  "ctl_" + hashlib.sha1(
198
210
  f"{control_type}:{name}".encode("utf-8")
@@ -207,11 +219,6 @@ def validate_control_batch_input(intent):
207
219
  raise ValueError(f"CONTROL_KEY_DUPLICATE: {key}")
208
220
  seen.add(key.casefold())
209
221
  parent = str(item.get("parent") or default_parent).strip()
210
- if not parent:
211
- raise ValueError(
212
- f"CONTROL_PARENT_REQUIRED: controls[{index}] requires parent "
213
- "or one top-level parent"
214
- )
215
222
  if panel and parent != panel["key"]:
216
223
  raise ValueError(
217
224
  f"CONTROL_PANEL_CHILD_PARENT_CONFLICT: controls[{index}] "
@@ -257,6 +264,29 @@ def validate_control_batch_input(intent):
257
264
  return {"panel": panel, "controls": normalized}
258
265
 
259
266
 
267
+ def control_input_schema():
268
+ """Publish the request shape and type properties from the validator's contract."""
269
+ contract = _control_contract()
270
+ return {
271
+ **contract["inputSchema"],
272
+ "x-commonProperties": {
273
+ name: {key: value for key, value in declaration.items() if key != "readProperty"}
274
+ for name, declaration in contract["commonProperties"].items()
275
+ },
276
+ "x-controlTypes": {
277
+ control_type: {
278
+ "aliases": spec.get("aliases") or [],
279
+ "properties": {
280
+ name: {key: value for key, value in declaration.items() if key != "readProperty"}
281
+ for name, declaration in _property_contract(control_type, contract).items()
282
+ if name not in contract["commonProperties"]
283
+ },
284
+ }
285
+ for control_type, spec in contract["releasedTypes"].items()
286
+ },
287
+ }
288
+
289
+
260
290
  def control_catalog():
261
291
  """Return the released/specialized/deferred screenshot-control surface."""
262
292
  contract = _control_contract()
@@ -291,8 +321,10 @@ def _layout_index(root):
291
321
  return nodes
292
322
 
293
323
 
294
- def _resolve_parent(selector, nodes):
295
- wanted = str(selector or "").strip()
324
+ def _resolve_parent(selector, nodes, root_key=""):
325
+ wanted = str(selector or root_key or "").strip()
326
+ if not wanted:
327
+ raise ValueError("CONTROL_PARENT_UNRESOLVED: live form root has no key")
296
328
  if wanted in nodes:
297
329
  matches = [wanted]
298
330
  else:
@@ -347,12 +379,14 @@ def do_add_controls(cfg, token, form_id, form_number, intent):
347
379
  "CONTROL_FORM_READ_FAILED: "
348
380
  + (_extract_error_message(before) or "getFormSchema failed")
349
381
  )
350
- before_nodes = _layout_index((before.get("data") or {}).get("formLayout"))
382
+ before_layout = (before.get("data") or {}).get("formLayout") or {}
383
+ before_nodes = _layout_index(before_layout)
384
+ root_key = str(before_layout.get("key") or "").strip()
351
385
  panel = normalized.get("panel")
352
386
  panel_parent = None
353
387
  structural_ops = []
354
388
  if panel:
355
- panel_parent = _resolve_parent(panel["parent"], before_nodes)
389
+ panel_parent = _resolve_parent(panel["parent"], before_nodes, root_key)
356
390
  current_panel = before_nodes.get(panel["key"])
357
391
  if current_panel:
358
392
  node = current_panel.get("node") or {}
@@ -396,7 +430,7 @@ def do_add_controls(cfg, token, form_id, form_number, intent):
396
430
  existing = []
397
431
  moved = []
398
432
  for item in managed:
399
- parent = panel["key"] if panel else _resolve_parent(item["parent"], before_nodes)
433
+ parent = panel["key"] if panel else _resolve_parent(item["parent"], before_nodes, root_key)
400
434
  if not panel:
401
435
  parent_type = str(
402
436
  ((before_nodes.get(parent) or {}).get("node") or {}).get("type") or ""
@@ -813,7 +813,7 @@ def _qf_resolve_target_entity(entity_key, entry, entity_name, entity_list, form_
813
813
  }
814
814
 
815
815
  if entity_name:
816
- matched = [e for e in entity_list if e.get("entityName") == entity_name]
816
+ matched = [e for e in entity_list if entity_name in {e.get("entityName"), e.get("entityKey")}]
817
817
  if len(matched) == 1:
818
818
  e = matched[0]
819
819
  return e["entityKey"], {
@@ -1486,6 +1486,14 @@ def _combo_verify_after_modify(cfg, token, form_number, field_key,
1486
1486
  observed_options = []
1487
1487
  read = _api_call(cfg, token, "loadFullMetadata",
1488
1488
  {"formNumber": form_number}, method="GET")
1489
+ if not _is_api_success(read):
1490
+ return {
1491
+ "status": "readback_failed",
1492
+ "exit_code": QUICKFIELD_EXIT["api_error"],
1493
+ "message": "Combo options could not be read back; their saved state is unknown. "
1494
+ + (_extract_error_message(read) or "loadFullMetadata failed"),
1495
+ "cause": read,
1496
+ }
1489
1497
  if _is_api_success(read):
1490
1498
  section = ((read.get("data") or {}).get("entitymeta") or {})
1491
1499
  items = section.get("Items") or []
@@ -1525,8 +1533,7 @@ def _combo_verify_after_modify(cfg, token, form_number, field_key,
1525
1533
  "status": "modify_failed",
1526
1534
  "exit_code": QUICKFIELD_EXIT["api_error"],
1527
1535
  "message": f"modifyMeta reported success but these options are absent on read-back: {missing}. "
1528
- "That means the op located nothing - check that target carries "
1529
- "value with the field key.",
1536
+ "Check the saved target and options before retrying.",
1530
1537
  "options": observed_options,
1531
1538
  }
1532
1539
  return {"status": "ok", "exit_code": QUICKFIELD_EXIT["ok"],
@@ -1779,6 +1779,12 @@ def do_add_field(cfg, token, form_name, field_name, biz_word,
1779
1779
  # modelType, accepting the shortened spelling some deployments return. `writeAttempted`
1780
1780
  # covers the field write only: target preparation above may already have extended the form,
1781
1781
  # so report that separately rather than implying nothing happened.
1782
+ if field_type == "RefBillField" and str(editable.get("modelType") or "").strip().casefold() in {"baseformmodel", "base"}:
1783
+ return _extension_terminal(
1784
+ "REF_BILL_OWNER_UNSUPPORTED",
1785
+ "The current RefBillField writer requires a bill form. Resolve model/reference semantics; do not substitute a field type automatically.",
1786
+ "preflight", extra={"editableTarget": editable, "writeAttempted": False,
1787
+ "extensionPerformed": bool(prepared_data.get("extensionPerformed"))})
1782
1788
  if str(editable.get("modelType") or "").strip().casefold() in {"reportformmodel", "report"}:
1783
1789
  return _extension_terminal(
1784
1790
  "REPORT_FIELD_NOT_ADDABLE",
@@ -1952,6 +1958,17 @@ def do_add_field(cfg, token, form_name, field_name, biz_word,
1952
1958
  "verification": detail_verification,
1953
1959
  },
1954
1960
  }
1961
+ _BATCH_FIELD_OPTIONS = {
1962
+ "basedata": "basedata", "mulTable": "mul_table", "unitField": "unit_field",
1963
+ "flexType": "flex_type", "refBaseField": "ref_base_field",
1964
+ "refDisplayProp": "ref_display_prop", "asstTypeId": "asst_type_id", "asstType": "asst_type",
1965
+ }
1966
+
1967
+
1968
+ def _batch_field_options(field):
1969
+ return {argument: field[key] for key, argument in _BATCH_FIELD_OPTIONS.items() if key in field}
1970
+
1971
+
1955
1972
  def validate_add_fields_input(fields):
1956
1973
  if not isinstance(fields, list) or not fields:
1957
1974
  raise ValueError("FIELD_BATCH_INVALID: expected a non-empty array")
@@ -1967,8 +1984,20 @@ def validate_add_fields_input(fields):
1967
1984
  if "combo" in field or "comboJson" in field:
1968
1985
  raise ValueError(f"FIELD_BATCH_INVALID: fields[{index}] comboOptions conflicts with combo/comboJson")
1969
1986
  field["comboJson"] = field.pop("comboOptions")
1970
- if set(field) - {"name", "bizWord", "fieldType", "combo", "comboJson", "required", "panel", "before", "after", "fullLine"}:
1971
- raise ValueError(f"FIELD_BATCH_INVALID: fields[{index}] has unsupported keys")
1987
+ if "reference" in field:
1988
+ reference = field["reference"]
1989
+ if (field.get("fieldType") not in {"BasedataField", "MulBasedataField"}
1990
+ or not isinstance(reference, dict) or reference.get("mode") != "platform"
1991
+ or set(reference) - {"mode", "formNumber"}
1992
+ or not isinstance(reference.get("formNumber"), str) or not reference["formNumber"].strip()):
1993
+ raise ValueError(f"FIELD_BATCH_INVALID: fields[{index}].reference requires a platform formNumber for BasedataField/MulBasedataField")
1994
+ if "basedata" in field and field["basedata"] != reference["formNumber"]:
1995
+ raise ValueError(f"FIELD_BATCH_INVALID: fields[{index}].reference conflicts with basedata")
1996
+ field["basedata"] = reference["formNumber"]
1997
+ field.pop("reference")
1998
+ unknown = set(field) - {"name", "bizWord", "fieldType", "combo", "comboJson", "required", "panel", "before", "after", "fullLine"} - set(_BATCH_FIELD_OPTIONS)
1999
+ if unknown:
2000
+ raise ValueError(f"FIELD_BATCH_INVALID: fields[{index}] has unsupported keys: {sorted(unknown)}")
1972
2001
  if "required" in field and not isinstance(field["required"], bool):
1973
2002
  raise ValueError(f"FIELD_BATCH_INVALID: fields[{index}].required must be boolean")
1974
2003
  if "fullLine" in field and not isinstance(field["fullLine"], bool):
@@ -1989,7 +2018,8 @@ def validate_add_fields_input(fields):
1989
2018
  "comboJson/comboOptions with explicit stable values"
1990
2019
  )
1991
2020
  result = validate_add_field_input(field.get("name"), field.get("bizWord"), field.get("fieldType", "TextField"),
1992
- combo=field.get("combo"), combo_json=field.get("comboJson"))
2021
+ combo=field.get("combo"), combo_json=field.get("comboJson"),
2022
+ **_batch_field_options(field))
1993
2023
  if not result.get("status"):
1994
2024
  raise ValueError(f"FIELD_BATCH_INVALID: fields[{index}]: {result.get('message')}")
1995
2025
  if field["bizWord"] in words:
@@ -2016,7 +2046,7 @@ def do_add_fields(cfg, token, form_name, fields, *, app_root=None, project_root=
2016
2046
  field_type=field.get("fieldType", "TextField"), combo=field.get("combo"), combo_json=field.get("comboJson"),
2017
2047
  required=field.get("required", False), app_root=app_root, project_root=project_root,
2018
2048
  app_name=app_name, app_code=app_code, entry=entry, entity_name=entity_name, entity_key=entity_key,
2019
- _prepared=prepared)
2049
+ _prepared=prepared, **_batch_field_options(field))
2020
2050
  results.append(result)
2021
2051
  if not _is_api_success(result) or not (result.get("data") or {}).get("verified"):
2022
2052
  return {"status": False, "errorCode": "FIELD_BATCH_INCOMPLETE",
@@ -2062,6 +2092,32 @@ def summary_delete_dependencies(metadata, fields):
2062
2092
  return dependencies
2063
2093
 
2064
2094
 
2095
+ def rule_delete_dependencies(metadata, fields):
2096
+ """Find persisted rule references, excluding prose and unrelated layout links."""
2097
+ keys = {field["fieldKey"] for field in fields}
2098
+ patterns = {key: _re.compile(r"(?<![A-Za-z0-9_])" + _re.escape(key) + r"(?![A-Za-z0-9_])") for key in keys}
2099
+ reference_slots = {"precondition", "condition", "triggerfield", "fieldnames", "fieldname",
2100
+ "targetfield", "srcfield", "expression", "fieldkey"}
2101
+ found = []
2102
+
2103
+ def visit(value, rule=None, path=""):
2104
+ if isinstance(value, list):
2105
+ for index, child in enumerate(value):
2106
+ visit(child, rule, f"{path}/{index}")
2107
+ if not isinstance(value, dict):
2108
+ return
2109
+ if str(value.get("_Type_") or "").rsplit(".", 1)[-1] in {"BizRule", "FormRule"}:
2110
+ rule = value.get("Id") or value.get("Key") or path
2111
+ for slot, child in value.items():
2112
+ if rule is not None and slot.lower() in reference_slots and isinstance(child, str):
2113
+ for key, pattern in patterns.items():
2114
+ if pattern.search(child):
2115
+ found.append({"ruleId": rule, "property": f"{path}/{slot}", "targetField": key})
2116
+ visit(child, rule, f"{path}/{slot}")
2117
+ visit(metadata)
2118
+ return found
2119
+
2120
+
2065
2121
  def do_remove_field(cfg, token, form_name, field_name, *, app_root=None,
2066
2122
  project_root=None, app_name=None, app_code=None,
2067
2123
  entity_key=None, before_delete=None):
@@ -2155,10 +2211,11 @@ def do_remove_field(cfg, token, form_name, field_name, *, app_root=None,
2155
2211
  if not _is_api_success(metadata) or not isinstance(metadata.get("data"), dict):
2156
2212
  return _extension_terminal("FIELD_DEPENDENCY_READ_FAILED", "删除前无法校验字段依赖关系",
2157
2213
  "preflight", cause=metadata, extra={"writeAttempted": False})
2158
- dependencies = summary_delete_dependencies(metadata.get("data"), candidates)
2214
+ dependencies = (summary_delete_dependencies(metadata.get("data"), candidates)
2215
+ + rule_delete_dependencies(metadata.get("data"), candidates))
2159
2216
  if dependencies:
2160
2217
  return _extension_terminal("FIELD_DELETE_DEPENDENCY_BLOCKED",
2161
- "仍有字段引用本次待删除的字段。请先厘清预期的业务关系再删除任何字段;不要清空该关系,也不要创建临时目标来绕过。",
2218
+ "Surviving fields or rules reference the requested deletion. Resolve the intended business relationship before deleting any fields; do not clear it or create a temporary target as a workaround.",
2162
2219
  "preflight", extra={"writeAttempted": False, "dependencies": dependencies, "fields": candidates,
2163
2220
  "editableTarget": editable})
2164
2221
  field = candidates[0]
@@ -31,17 +31,73 @@ def do_query_forms_by_app(cfg, token, app_number=None, cloud_number=None, keywor
31
31
  return _api_call(cfg, token, "queryFormsByApp", payload, method="GET")
32
32
  # ─── queryPublicTemplates / queryBizTemplates ────────────────────────────────
33
33
 
34
+ _TEMPLATE_MODEL_ALIASES = {
35
+ "FormModel_Bill": "BillFormModel",
36
+ "FormModel_Base": "BaseFormModel",
37
+ }
38
+
39
+
40
+ def _template_model_type(value):
41
+ return _TEMPLATE_MODEL_ALIASES.get(value, value)
42
+
43
+ def normalize_template_caption(value):
44
+ text = str(value or "").strip().replace("模版", "模板")
45
+ text = _re.sub(r"[\s()()]+", "", text)
46
+ return text[:-2] if text.endswith("模板") else text
47
+
48
+
49
+ def resolve_template_selector(selector, templates, model_type=None):
50
+ """Resolve an exact live template code or unique name within the model type."""
51
+ wanted = str(selector or "").strip()
52
+ model_type = _template_model_type(model_type)
53
+ rows = [row for row in templates if isinstance(row, dict)
54
+ and (not model_type or _template_model_type(row.get("modelType")) == model_type)]
55
+ coded = [row for row in rows if str(row.get("number") or "").strip() == wanted]
56
+ if len(coded) == 1:
57
+ return coded[0]["number"]
58
+ exact_names = {str(row["number"]) for row in rows if row.get("number")
59
+ and str(row.get("name") or "").strip() == wanted}
60
+ if len(exact_names) == 1:
61
+ return next(iter(exact_names))
62
+ # 模版 and 模板 are spelling variants, not different template identities.
63
+ normalize = normalize_template_caption
64
+ matched = {str(row["number"]) for row in rows
65
+ if row.get("number") and normalize(row.get("name")) == normalize(wanted)}
66
+ if len(matched) == 1:
67
+ return next(iter(matched))
68
+ if matched:
69
+ raise ValueError(f"TEMPLATE_NAME_AMBIGUOUS: {wanted}: {sorted(matched)}")
70
+ raise ValueError(f"TEMPLATE_NOT_FOUND: {wanted}")
71
+
72
+
73
+ def resolve_template_input(cfg, token, selector, *, biz_app_id=None, biz_app_number=None, model_type=None):
74
+ wanted = str(selector or "").strip()
75
+ if not wanted:
76
+ return wanted
77
+ rows = do_get_template_list(cfg, token,
78
+ model_types=[model_type] if model_type else None,
79
+ biz_app_id=biz_app_id, biz_app_number=biz_app_number)
80
+ if not rows:
81
+ return wanted
82
+ try:
83
+ return resolve_template_selector(wanted, rows, model_type)
84
+ except ValueError as exc:
85
+ if str(exc).startswith("TEMPLATE_NOT_FOUND") and _re.fullmatch(r"[A-Za-z][A-Za-z0-9_]*", wanted):
86
+ return wanted
87
+ raise
88
+
89
+
34
90
  def do_query_public_templates(cfg, token, model_type="BillFormModel",
35
91
  biz_app_number=None):
36
92
  """Query public template list."""
37
- payload = {"modelType": model_type}
93
+ payload = {"modelType": _template_model_type(model_type)}
38
94
  if biz_app_number:
39
95
  payload["bizAppNumber"] = biz_app_number
40
96
  return _api_call(cfg, token, "queryPublicTemplates", payload, method="POST")
41
97
  def do_query_biz_templates(cfg, token, model_type="BillFormModel",
42
98
  biz_app_id=None, biz_app_number=None):
43
99
  """Query business template list (app required)."""
44
- payload = {"modelType": model_type}
100
+ payload = {"modelType": _template_model_type(model_type)}
45
101
  if biz_app_id:
46
102
  payload["bizAppId"] = biz_app_id
47
103
  if biz_app_number:
@@ -73,6 +129,7 @@ def do_get_template_list(cfg, token, model_types=None, biz_app_id=None, biz_app_
73
129
  """
74
130
  if model_types is None:
75
131
  model_types = ["BillFormModel", "BaseFormModel"]
132
+ model_types = list(dict.fromkeys(_template_model_type(mt) for mt in model_types))
76
133
  all_templates: list[dict] = []
77
134
  seen_numbers: set[str] = set()
78
135
  for mt in model_types:
@@ -1488,6 +1488,14 @@ def do_configure_list(
1488
1488
  return _list_configuration_preflight_failure(
1489
1489
  "LIST_CONFIGURATION_BASELINE_FAILED",
1490
1490
  "写入列表前无法同时读取设计态和运行态元数据",
1491
+ failedReads={
1492
+ name: item
1493
+ for name, item in (
1494
+ ("listElements", field_result), ("getFormSchema", schema_before),
1495
+ ("loadFullMetadata", metadata_before), ("getEntityType", entity_type),
1496
+ )
1497
+ if not _is_api_success(item)
1498
+ },
1491
1499
  )
1492
1500
 
1493
1501
  field_rows = field_result.get("data") or []