@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
@@ -1,23 +1,20 @@
1
- """A plugin type is defined in three places that nothing checked against each other.
2
-
3
- `references/type-registry.json` is the authority — "downstream docs and prompts should resolve
4
- per-type behavior from this registry". `cosmic-coding-impl`'s mapping table resolves the same
5
- classSuffix and guide independently, and each guide's `## Skeleton Template` states which type it
6
- serves. Every drift below reached delivery: `report-plugin` carried one suffix in the table and
7
- another in the registry, the registry sent the three mobile types to a guide with no skeleton at
8
- all, four guides declared a type name the registry does not have, and form-plugin.md used
9
- `EventObject` in five places while its import block never named it.
10
-
11
- Every check is narrow on purpose. A guess at what counts as a drift reports a finding nobody can
12
- fix, and a false finding here blocks delivery while a missed one costs one compile round.
13
- """
1
+ """The type registry drives coding evidence and each guide's supported skeletons."""
14
2
 
15
3
  import json
4
+ import os
16
5
  import re
6
+ import shutil
7
+ import subprocess
8
+ import sys
17
9
  from pathlib import Path
18
10
 
11
+ import pytest
12
+
19
13
 
20
14
  ROOT = Path(__file__).resolve().parents[2]
15
+ # Target SDK JARs (bos and trd) joined with the path separator. The compile check below runs only
16
+ # when it is set; javac comes from JAVA_HOME when set, otherwise PATH, and must accept -source 8.
17
+ SDK_CLASSPATH = os.environ.get("COSMIC_SDK_PLUGIN_CLASSPATH", "")
21
18
  # This file is byte-identical in both skill trees, so it must work from either: the locale tree has
22
19
  # no nested locale tree. From the English root both are checked, from the zh root only that one.
23
20
  TREES = [ROOT] + [tree for tree in [ROOT / ".locales" / "zh"] if tree.is_dir()]
@@ -48,8 +45,6 @@ _DECLARED_TYPES_RE = re.compile(
48
45
  r"(?:Applicable to|Applies to|适用于|适用)\s*[::]\s*((?:`[\w-]+`(?:\s*/\s*)?)+)")
49
46
  _BACKTICKED_RE = re.compile(r"`([\w-]+)`")
50
47
  _STATED_SUFFIX_RE = re.compile(r"(?:suffix follows|类名后缀按)\s*`([A-Za-z{][\w{}]*)`")
51
- _TABLE_ROW_RE = re.compile(r"^\|\s*`([\w-]+)`\s*\|\s*`([A-Za-z]\w*)`\s*\|\s*(.+?)\s*\|\s*$",
52
- re.MULTILINE)
53
48
 
54
49
 
55
50
  def _registry(tree):
@@ -59,17 +54,10 @@ def _registry(tree):
59
54
 
60
55
 
61
56
  def _guides(tree):
62
- """Every distinct guide a type points at. `service` publishes none by design — the mapping
63
- table records "no guide; every required API needs exact documentation" for it."""
57
+ """Every distinct guide a type points at; service has no guide."""
64
58
  return sorted({entry["guide"] for entry in _registry(tree).values() if entry.get("guide")})
65
59
 
66
60
 
67
- def _mapping_table(tree):
68
- """type -> (classSuffix, guide) as cosmic-coding-impl's own table declares them."""
69
- text = (tree / "skills" / "cosmic-coding-impl" / "SKILL.md").read_text(encoding="utf-8")
70
- return {name: (suffix, guide.strip("`")) for name, suffix, guide in _TABLE_ROW_RE.findall(text)}
71
-
72
-
73
61
  def _skeletons(tree, guide):
74
62
  """The java blocks under the skeleton heading that declare a type.
75
63
 
@@ -109,26 +97,27 @@ def test_a_guide_declares_the_type_name_the_registry_uses():
109
97
  assert not findings, "guides naming a type absent from the registry: " + "; ".join(findings)
110
98
 
111
99
 
112
- def test_the_mapping_table_and_the_registry_agree_on_every_type():
113
- """Alias types stay in both places on purpose: `form_plugin` is dispatched by formtemplate and
114
- dynamic_form_emit, so dropping it from the table would break those two paths."""
100
+ def test_coding_evidence_resolves_every_registered_type_without_a_second_mapping():
115
101
  for tree in TREES:
116
- registry, table = _registry(tree), _mapping_table(tree)
117
-
118
- assert table, f"{tree.name}: the plugin mapping table parsed as empty"
119
- assert set(table) == set(registry), (
120
- f"{tree.name}: table only: {sorted(set(table) - set(registry))}, "
121
- f"registry only: {sorted(set(registry) - set(table))}"
102
+ result = subprocess.run(
103
+ [sys.executable, "-c",
104
+ "import json; from platform_api_contract import plugin_coding_evidence, TYPE_REGISTRY_PAGE; "
105
+ "entries=json.loads(TYPE_REGISTRY_PAGE.read_text())['types']; "
106
+ "print(json.dumps({e['type']:plugin_coding_evidence(e['type']) for e in entries}))"],
107
+ cwd=tree / "scripts", capture_output=True, text=True, check=True,
122
108
  )
123
- suffix_drift = {name: (suffix, registry[name]["classSuffix"])
124
- for name, (suffix, _) in table.items()
125
- if suffix != registry[name]["classSuffix"]}
126
- guide_drift = {name: (guide, registry[name]["guide"])
127
- for name, (_, guide) in table.items()
128
- if registry[name].get("guide") and not guide.endswith(registry[name]["guide"])}
129
-
130
- assert not suffix_drift, f"{tree.name}: classSuffix (table, registry): {suffix_drift}"
131
- assert not guide_drift, f"{tree.name}: guide (table, registry): {guide_drift}"
109
+ evidence = json.loads(result.stdout)
110
+ for name, entry in _registry(tree).items():
111
+ actual = evidence[name]
112
+ if entry.get("guide"):
113
+ assert Path(actual["guide"]).resolve() == (tree / entry["guide"]).resolve()
114
+ assert Path(actual["guide"]).is_file()
115
+ else:
116
+ assert actual["guide"] is None
117
+ if entry.get("baseImport"):
118
+ assert actual["baseContract"]["class"] == entry["baseImport"]
119
+ assert Path(actual["typeKnowledge"]).is_file()
120
+ assert Path(actual["standards"]).is_file()
132
121
 
133
122
 
134
123
  def test_a_guide_does_not_hardcode_a_class_suffix_that_contradicts_the_registry():
@@ -155,8 +144,7 @@ def test_a_guide_does_not_hardcode_a_class_suffix_that_contradicts_the_registry(
155
144
 
156
145
 
157
146
  def test_no_skeleton_uses_a_jdk_type_it_does_not_import():
158
- """cosmic-coding-impl requires every imported class to trace to a page read in that run, so a
159
- skeleton using a type it never imports leaves the correct import with no legal source."""
147
+ """A usable skeleton must import each non-implicit JDK type it uses."""
160
148
  findings = []
161
149
  for tree in TREES:
162
150
  for guide in _guides(tree):
@@ -183,3 +171,59 @@ def test_only_the_one_known_guide_publishes_no_skeleton():
183
171
 
184
172
  assert without == sorted(f"{tree.name}:knowledge/cosmic/platform/task-plugin.md"
185
173
  for tree in TREES), without
174
+
175
+
176
+ def _javac():
177
+ home = os.environ.get("JAVA_HOME")
178
+ candidate = Path(home) / "bin" / ("javac.exe" if os.name == "nt" else "javac") if home else None
179
+ return str(candidate) if candidate and candidate.is_file() else shutil.which("javac")
180
+
181
+
182
+ def _instantiated(entry, block):
183
+ """The skeleton as the coding step fills it for one registry type.
184
+
185
+ The placeholder table tells the coding step to substitute the registry's suffix and base class
186
+ and to import that base; package placeholders become a fixed sample package.
187
+ """
188
+ base = entry["baseImport"]
189
+ source = block.replace("{PluginSuffix}", entry["classSuffix"]).replace("{BaseClass}", base.rsplit(".", 1)[-1])
190
+ source = re.sub(r"\{\w+\}", "sample", source)
191
+ if "{BaseClass}" in block:
192
+ source = re.sub(r"^(package [^;]+;)", rf"\1\nimport {base};", source, count=1, flags=re.MULTILINE)
193
+ return source
194
+
195
+
196
+ @pytest.mark.skipif(not SDK_CLASSPATH or not _javac(), reason="target SDK classpath or javac unavailable")
197
+ def test_every_skeleton_compiles_against_the_target_sdk(tmp_path):
198
+ """The coding step copies a skeleton's imports and event signatures as verified evidence, and
199
+ the API pages cannot prove a skeleton right: list-plugin.md imported two event classes that do
200
+ not exist (`ListPackageDataEvent`, `PrepareFilterEvent`), form-plugin.md and list-plugin.md
201
+ imported `ShowType` from a package that does not exist, the shared form skeleton overrode
202
+ `afterLoadData` that only bill bases declare, and the workflow skeleton called `super` on
203
+ interface methods. Each compiled nowhere; only the target SDK shows it."""
204
+ failures = []
205
+ for tree in TREES:
206
+ seen = set()
207
+ for entry in _registry(tree).values():
208
+ base = str(entry.get("baseImport") or "")
209
+ if not entry.get("guide") or not base or "," in base or str(entry.get("baseClass", "")).startswith("@"):
210
+ continue
211
+ blocks = _skeletons(tree, entry["guide"])
212
+ if not blocks or (entry["guide"], base) in seen:
213
+ continue
214
+ seen.add((entry["guide"], base))
215
+ source = _instantiated(entry, blocks[0])
216
+ name = re.search(r"^\s*public\s+(?:(?:abstract|final)\s+)*(?:class|interface)\s+(\w+)",
217
+ source, re.MULTILINE).group(1)
218
+ work = tmp_path / f"{tree.name}-{entry['type']}"
219
+ (work / "classes").mkdir(parents=True)
220
+ (work / f"{name}.java").write_text(source, encoding="utf-8")
221
+ result = subprocess.run(
222
+ [_javac(), "-J-Duser.language=en", "-source", "8", "-target", "8", "-proc:none", "-nowarn",
223
+ "-encoding", "UTF-8", "-cp", SDK_CLASSPATH, "-d", str(work / "classes"), str(work / f"{name}.java")],
224
+ capture_output=True, text=True)
225
+ if result.returncode:
226
+ errors = re.findall(r"\.java:\d+: error: .*", result.stderr) or [result.stderr.strip()[:300]]
227
+ failures.append(f"{tree.name}:{entry['guide']} as {entry['type']}: " + "; ".join(errors[:3]))
228
+
229
+ assert not failures, "skeletons that do not compile against the target SDK:\n" + "\n".join(failures)
@@ -0,0 +1,71 @@
1
+ import hashlib
2
+ import json
3
+ import shutil
4
+ import subprocess
5
+ import sys
6
+ import zipfile
7
+ from pathlib import Path
8
+
9
+ import pytest
10
+
11
+ SCRIPT = Path(__file__).resolve().parents[1] / "inspect_platform_sdk.py"
12
+
13
+
14
+ @pytest.fixture
15
+ def sdk(tmp_path):
16
+ if not shutil.which("javac") or not shutil.which("javap"):
17
+ pytest.skip("JDK tools unavailable")
18
+ source = tmp_path / "Target.java"
19
+ source.write_text('package sample; public class Target { public String getOperationKey() { return "unit.operation"; } }')
20
+ subprocess.run(["javac", "-d", str(tmp_path), str(source)], check=True, capture_output=True)
21
+ jar = tmp_path / "sdk.jar"
22
+ with zipfile.ZipFile(jar, "w") as archive:
23
+ archive.write(tmp_path / "sample/Target.class", "sample/Target.class")
24
+ archive.writestr("META-INF/MANIFEST.MF", "Manifest-Version: 1.0\nImplementation-Version: test-1\n")
25
+ archive.writestr("sample/Target.java", source.read_text())
26
+ return jar
27
+
28
+
29
+ def query(jar, *args):
30
+ result = subprocess.run([sys.executable, str(SCRIPT), "--jar", str(jar), *args],
31
+ capture_output=True, text=True)
32
+ return result.returncode, json.loads(result.stdout)
33
+
34
+
35
+ def test_sdk_inspection_uses_real_class_declarations_and_selected_bytecode(sdk):
36
+ code, result = query(sdk, "--class", "Target", "--member", "getOperationKey")
37
+ assert code == 0
38
+ assert result["class"] == "sample.Target"
39
+ assert result["version"] == "test-1"
40
+ assert result["jarSha256"] == hashlib.sha256(sdk.read_bytes()).hexdigest()
41
+ assert result["members"][0]["declaration"] == "public java.lang.String getOperationKey();"
42
+ assert result["missingDeclaredMembers"] == []
43
+ code, result = query(sdk, "--class", "sample.Target", "--member", "getOperationKey", "--mode", "bytecode")
44
+ assert code == 0
45
+ assert any("unit.operation" in line for line in result["members"][0]["details"])
46
+ assert len(result["members"]) == 1
47
+
48
+
49
+ def test_sdk_lookup_does_not_guess_a_duplicate_version_or_an_absent_method(sdk, tmp_path):
50
+ other = tmp_path / "duplicate.jar"
51
+ shutil.copyfile(sdk, other)
52
+ code, result = query(sdk, "--jar", str(other), "--class", "sample.Target")
53
+ assert code == 2 and result["errorCode"] == "SDK_CLASS_AMBIGUOUS"
54
+ assert len(result["candidates"]) == 2
55
+ code, result = query(sdk, "--class", "sample.Target", "--member", "notDeclared")
56
+ assert code == 0
57
+ assert result["members"] == [] and result["missingDeclaredMembers"] == ["notDeclared"]
58
+ code, result = query(sdk, "--class", "Missing")
59
+ assert code == 2 and result["errorCode"] == "SDK_CLASS_NOT_FOUND"
60
+ code, result = query(sdk, "--class", "sample.Target", "--mode", "bytecode")
61
+ assert code == 2 and result["errorCode"] == "SDK_MEMBER_REQUIRED"
62
+
63
+
64
+ def test_reference_jars_are_read_by_exact_entry_without_extracting_files(sdk):
65
+ code, result = query(sdk, "--list-resources", "Target")
66
+ assert code == 0 and result["items"][0]["resource"] == "sample/Target.java"
67
+ code, result = query(sdk, "--resource", "sample/Target.java", "--limit", "1")
68
+ assert code == 0 and "public class Target" in result["content"]
69
+ assert result["resourceSha256"] and result["lineCount"] == 1
70
+ code, result = query(sdk, "--resource", "sample/Target.class")
71
+ assert code == 2 and result["errorCode"] == "SDK_REFERENCE_NOT_UNIQUE"
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env python3
2
- """Tests for kd_api.py — 业务 facade 入口。
3
-
4
- 覆盖:
5
- - _resolve_base:base_url 解析优先级 / 缺失报错
6
- - post / get:URL 拼接、body/query 构造、header 风格、timeout 透传
7
- - _request_once:HTTP 401 / body 业务码 401 → TokenExpiredError 归一化;其它 HTTPError 原样上抛
8
- - _call_with_retry:成功不刷新;TokenExpiredError 强刷重试一次;连续两次失败仍上抛
9
- - invalidate_cache:委托 kd_token_cache.invalidate(None)
2
+ """Tests for kd_api.py — business facade entry point.
3
+
4
+ Coverage:
5
+ - _resolve_base: base_url resolution priority / missing error
6
+ - post / get: URL construction, body/query building, header style, timeout passthrough
7
+ - _request_once: HTTP 401 / body business code 401 -> TokenExpiredError normalization; other HTTPError re-raised as-is
8
+ - _call_with_retry: success no refresh; TokenExpiredError force refresh retry once; consecutive two failures still re-raised
9
+ - invalidate_cache: delegates to kd_token_cache.invalidate(None)
10
10
  """
11
11
  from __future__ import annotations
12
12
 
@@ -26,13 +26,13 @@ import kd_api
26
26
  import kd_auth
27
27
 
28
28
 
29
- # ─── 测试替身:替换 _http_client.retry_call ──────────────────────────────
29
+ # ─── Test doubles: replace _http_client.retry_call ──────────────────────────────
30
30
 
31
31
  def _fake_retry(*behaviors):
32
- """构造一个 retry_call 替身。
32
+ """Construct a retry_call double.
33
33
 
34
- behaviors: 每次调用的 (response, exc) 二元组;超出长度时复用最后一个。
35
- 返回 (callable, calls) ——calls 记录每次调用的 (fn_name, args, kwargs)。
34
+ behaviors: (response, exc) tuple for each call; reuse the last when exceeding length.
35
+ Returns (callable, calls) — calls records (fn_name, args, kwargs) for each invocation.
36
36
  """
37
37
  calls = []
38
38
 
@@ -163,7 +163,7 @@ class GetTest(unittest.TestCase):
163
163
  self.assertEqual(args[0], "https://x/g")
164
164
 
165
165
 
166
- # ─── _request_once:401 归一化 ────────────────────────────────────────────
166
+ # ─── _request_once:401 normalization ────────────────────────────────────────────
167
167
 
168
168
  class RequestOnceAuthTest(unittest.TestCase):
169
169
  def _call(self, cfg, retry_fake):
@@ -187,7 +187,7 @@ class RequestOnceAuthTest(unittest.TestCase):
187
187
  self._call(cfg, retry)
188
188
 
189
189
  def test_body_errorcode_401_raises_token_expired(self):
190
- """HTTP 200 + body errorCode=401 也要归一化为 TokenExpiredError。"""
190
+ """HTTP 200 + body errorCode=401 should also be normalized to TokenExpiredError."""
191
191
  cfg = {"baseUrl": "https://x"}
192
192
  retry, _ = _fake_retry(({"errorCode": "401"}, None))
193
193
  with self.assertRaises(kd_auth.TokenExpiredError):
@@ -200,7 +200,7 @@ class RequestOnceAuthTest(unittest.TestCase):
200
200
  self.assertEqual(resp, {"errorCode": "500"})
201
201
 
202
202
 
203
- # ─── _call_with_retry:强刷重试 ──────────────────────────────────────────
203
+ # ─── _call_with_retry:force refresh retry ──────────────────────────────────────────
204
204
 
205
205
  class CallWithRetryTest(unittest.TestCase):
206
206
  def test_success_no_force_refresh(self):
@@ -212,14 +212,14 @@ class CallWithRetryTest(unittest.TestCase):
212
212
  resp = kd_api.post(None, "/p", cfg, {})
213
213
  self.assertEqual(resp, {"ok": True})
214
214
  self.assertEqual(m_get.call_count, 1)
215
- # 第一次:get_token(cfg, base),不带 force
215
+ # first call: get_token(cfg, base), without force
216
216
  args, kwargs = m_get.call_args
217
217
  self.assertEqual(args, (cfg, "https://x"))
218
218
  self.assertFalse(kwargs.get("force"))
219
219
 
220
220
  def test_force_refresh_on_token_expired(self):
221
221
  cfg = {"baseUrl": "https://x"}
222
- # 第一次请求抛 TokenExpiredError(body 401),第二次成功
222
+ # first request raises TokenExpiredError (body 401), second succeeds
223
223
  retry, _ = _fake_retry(
224
224
  (None, kd_auth.TokenExpiredError("expired")),
225
225
  ({"ok": True}, None),
@@ -230,7 +230,7 @@ class CallWithRetryTest(unittest.TestCase):
230
230
  resp = kd_api.post(None, "/p", cfg, {})
231
231
  self.assertEqual(resp, {"ok": True})
232
232
  self.assertEqual(m_get.call_count, 2)
233
- # 第二次必须带 force=True
233
+ # second call must have force=True
234
234
  _, kwargs = m_get.call_args
235
235
  self.assertTrue(kwargs.get("force"))
236
236
 
@@ -267,7 +267,7 @@ class InvalidateCacheTest(unittest.TestCase):
267
267
  m_inv.assert_called_once_with(None)
268
268
 
269
269
  def test_config_arg_currently_ignored(self):
270
- """当前实现忽略 config,统一 invalidate(None)。"""
270
+ """Current implementation ignores config, uniformly invalidate(None)."""
271
271
  import kd_token_cache
272
272
  with mock.patch.object(kd_token_cache, "invalidate") as m_inv:
273
273
  kd_api.invalidate_cache({"baseUrl": "https://x"})
@@ -276,3 +276,51 @@ class InvalidateCacheTest(unittest.TestCase):
276
276
 
277
277
  if __name__ == "__main__":
278
278
  unittest.main()
279
+
280
+
281
+ class WriteTransportBoundaryTest(unittest.TestCase):
282
+ def test_lost_post_response_is_not_replayed_but_get_can_retry(self):
283
+ import http.client
284
+ import socket
285
+ import threading
286
+ from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
287
+ hits = {"POST": 0, "GET": 0}
288
+
289
+ class Handler(BaseHTTPRequestHandler):
290
+ def log_message(self, *args):
291
+ pass
292
+
293
+ def do_POST(self):
294
+ self.rfile.read(int(self.headers.get("Content-Length", "0")))
295
+ hits["POST"] += 1
296
+ self.connection.shutdown(socket.SHUT_RDWR)
297
+ self.connection.close()
298
+
299
+ def do_GET(self):
300
+ hits["GET"] += 1
301
+ if hits["GET"] == 1:
302
+ self.connection.shutdown(socket.SHUT_RDWR)
303
+ self.connection.close()
304
+ return
305
+ body = b'{"status":true,"data":[]}'
306
+ self.send_response(200)
307
+ self.send_header("Content-Length", str(len(body)))
308
+ self.end_headers()
309
+ self.wfile.write(body)
310
+
311
+ server = ThreadingHTTPServer(("127.0.0.1", 0), Handler)
312
+ worker = threading.Thread(target=server.serve_forever, daemon=True)
313
+ worker.start()
314
+ try:
315
+ with mock.patch.object(_http_client, "_RETRY_DELAY", 0):
316
+ url = f"http://127.0.0.1:{server.server_port}/write"
317
+ with self.assertRaises(http.client.RemoteDisconnected):
318
+ kd_api._request_once(url, {}, "fixture", method="POST", body={}, params=None, style="kapi")
319
+ self.assertEqual(hits["POST"], 1)
320
+ result = kd_api._request_once(url, {}, "fixture", method="GET", body=None, params=None, style="kapi")
321
+ self.assertTrue(result["status"])
322
+ self.assertEqual(hits["GET"], 2)
323
+ finally:
324
+ server.shutdown()
325
+ server.server_close()
326
+ worker.join()
@@ -0,0 +1,221 @@
1
+ import json
2
+ import sys
3
+ import time
4
+ import zipfile
5
+ from pathlib import Path
6
+
7
+ import pytest
8
+
9
+ SCRIPTS = Path(__file__).resolve().parents[1]
10
+ sys.path.insert(0, str(SCRIPTS))
11
+
12
+ import kingscript_sdk_sync as sdk_sync # noqa: E402
13
+
14
+
15
+ PKG_FILE = (
16
+ "export declare type AbstractFormPlugin = $.kd.bos.form.plugin.AbstractFormPlugin;\n"
17
+ "export declare const AbstractFormPlugin: $.kd.bos.form.plugin.AbstractFormPlugin_C;\n"
18
+ "export declare type AbstractMobFormPlugin = $.kd.bos.form.plugin.AbstractMobFormPlugin;\n"
19
+ "export declare interface AfterBindDataArgs { }\n"
20
+ "export declare const FormUtil: $.kd.bos.form.plugin.FormUtil_C;\n"
21
+ "export declare const enum Color { Red }\n"
22
+ )
23
+ NESTED_PKG_FILE = (
24
+ "/// <reference types=\"../../index\" />\n"
25
+ "export declare type QFilter = $.kd.bos.orm.query.QFilter;\n"
26
+ )
27
+ AGGREGATE_FILE = (
28
+ "declare global {\n"
29
+ " namespace $ {\n"
30
+ " namespace kd.bos.form.plugin {\n"
31
+ " interface AbstractFormPlugin$ {\n"
32
+ " afterBindData?(e: any): void;\n"
33
+ " }\n"
34
+ " }\n"
35
+ " }\n"
36
+ "}\n"
37
+ )
38
+
39
+
40
+ def _make_zip(path: Path) -> Path:
41
+ with zipfile.ZipFile(path, "w") as bundle:
42
+ bundle.writestr("@cosmic/", "")
43
+ bundle.writestr("@cosmic/bos-core/", "")
44
+ bundle.writestr("@cosmic/bos-core/kd/bos/form/plugin.d.ts", PKG_FILE)
45
+ bundle.writestr("@cosmic/bos-core/kd/bos/orm/query.d.ts", NESTED_PKG_FILE)
46
+ bundle.writestr("@cosmic/bos-core/bos-form-core.d.ts", AGGREGATE_FILE)
47
+ bundle.writestr("@cosmic/bos-script/java/math.d.ts",
48
+ "export declare type BigDecimal = $.java.math.BigDecimal;\n")
49
+ return path
50
+
51
+
52
+ # ─── type index build ─────────────────────────────────────────────────────────
53
+
54
+ def test_build_type_index_extracts_all_exports_per_package_file(tmp_path):
55
+ rows, dts_count = sdk_sync.build_type_index(_make_zip(tmp_path / "sdk.zip"))
56
+ by_type = {row.split("\t")[0]: row.split("\t") for row in rows}
57
+ # MULTILINE regression guard: every export line of a package file must be
58
+ # indexed, not just the first one.
59
+ assert by_type["AbstractFormPlugin"] == [
60
+ "AbstractFormPlugin", "@cosmic/bos-core", "kd/bos/form/plugin", "kd.bos.form.plugin"]
61
+ assert by_type["AbstractMobFormPlugin"][3] == "kd.bos.form.plugin"
62
+ # interface declarations and const-only value exports are importable names
63
+ # and must be indexed too (HttpClient-style exports on real environments).
64
+ assert by_type["AfterBindDataArgs"][2] == "kd/bos/form/plugin"
65
+ assert by_type["FormUtil"][1] == "@cosmic/bos-core"
66
+ # same-name type + const pair yields exactly one row (per-file dedup)
67
+ assert sum(1 for row in rows if row.startswith("AbstractFormPlugin\t")) == 1
68
+ # `export declare const enum X` must not leak the keyword as a name
69
+ assert "Color" not in by_type and "enum" not in by_type
70
+ assert by_type["QFilter"][2] == "kd/bos/orm/query"
71
+ assert by_type["BigDecimal"] == [
72
+ "BigDecimal", "@cosmic/bos-script", "java/math", "java.math"]
73
+ assert dts_count == 4
74
+
75
+
76
+ def test_build_type_index_skips_module_root_aggregates(tmp_path):
77
+ rows, _ = sdk_sync.build_type_index(_make_zip(tmp_path / "sdk.zip"))
78
+ types = {row.split("\t")[0] for row in rows}
79
+ assert types == {"AbstractFormPlugin", "AbstractMobFormPlugin", "QFilter",
80
+ "BigDecimal", "AfterBindDataArgs", "FormUtil"}
81
+ # the aggregate declares interfaces, never `export declare` rows
82
+ assert all("AbstractFormPlugin$" not in t for t in types)
83
+
84
+
85
+ # ─── zip member safety ────────────────────────────────────────────────────────
86
+
87
+ def test_safe_member_name_rejects_traversal():
88
+ for bad in ("../evil.d.ts", "a/../../evil.d.ts", "/abs/evil.d.ts", "C:/evil.d.ts"):
89
+ with pytest.raises(RuntimeError):
90
+ sdk_sync._safe_member_name(bad)
91
+
92
+
93
+ def test_safe_member_name_normalizes_segments():
94
+ assert sdk_sync._safe_member_name("a//b/./c.d.ts") == "a/b/c.d.ts"
95
+ assert sdk_sync._safe_member_name("kd/bos/form/") == "kd/bos/form"
96
+
97
+
98
+ # ─── cache state / TTL ────────────────────────────────────────────────────────
99
+
100
+ def _write_cache_with_age(cache: Path, age_hours: float, types: int = 10) -> None:
101
+ cache.mkdir(parents=True, exist_ok=True)
102
+ (cache / "sdk-meta.json").write_text(json.dumps({
103
+ "generatedAt": int(time.time() - age_hours * 3600),
104
+ "envName": cache.name, "baseUrl": "https://unit.test/ierp",
105
+ "modules": 1, "dtsFiles": 2, "types": types,
106
+ }), encoding="utf-8")
107
+ (cache / "type-index.tsv").write_text("X\t@m/a\ta/b\ta.b\n", encoding="utf-8")
108
+
109
+
110
+ def test_cache_state_missing(tmp_path):
111
+ state = sdk_sync._cache_state(tmp_path / "none", 168)
112
+ assert state["state"] == "missing"
113
+
114
+
115
+ def test_cache_state_fresh_and_stale_boundary(tmp_path):
116
+ cache = tmp_path / "env"
117
+ _write_cache_with_age(cache, age_hours=2)
118
+ assert sdk_sync._cache_state(cache, 168)["state"] == "fresh"
119
+ assert sdk_sync._cache_state(cache, 1)["state"] == "stale"
120
+
121
+
122
+ def test_cache_state_survives_corrupt_meta(tmp_path):
123
+ cache = tmp_path / "env"
124
+ cache.mkdir(parents=True)
125
+ (cache / "sdk-meta.json").write_text("{ not json", encoding="utf-8")
126
+ assert sdk_sync._cache_state(cache, 168)["state"] == "missing"
127
+
128
+
129
+ # ─── lookup ───────────────────────────────────────────────────────────────────
130
+
131
+ def test_lookup_found_and_miss(tmp_path, capsys):
132
+ cache = tmp_path / "env"
133
+ _write_cache_with_age(cache, age_hours=0)
134
+ lines = "A\t@m/x\tx/a\tx.a\nA\t@m/y\ty/a\ty.a\nB\t@m/x\tx/b\tx.b\n"
135
+ (cache / "type-index.tsv").write_text(lines, encoding="utf-8")
136
+ monkey = pytest.MonkeyPatch()
137
+ monkey.setattr(sdk_sync, "_cache_dir", lambda env: cache)
138
+ try:
139
+ assert sdk_sync.do_lookup("env", "A") == 0
140
+ found = json.loads(capsys.readouterr().out)
141
+ assert found["status"] == "found"
142
+ assert len(found["matches"]) == 2
143
+ assert found["matches"][0]["packageDot"] == "x.a"
144
+ assert sdk_sync.do_lookup("env", "B") == 0
145
+ capsys.readouterr() # drain the intermediate output
146
+ assert sdk_sync.do_lookup("env", "Missing") == 2
147
+ miss = json.loads(capsys.readouterr().out)
148
+ assert miss["status"] == "miss"
149
+ finally:
150
+ monkey.undo()
151
+
152
+
153
+ def test_lookup_without_cache_fails(tmp_path, capsys):
154
+ monkey = pytest.MonkeyPatch()
155
+ monkey.setattr(sdk_sync, "_cache_dir", lambda env: tmp_path / "empty")
156
+ try:
157
+ assert sdk_sync.do_lookup("env", "A") == 2
158
+ result = json.loads(capsys.readouterr().out)
159
+ assert result["errorCode"] == "KINGSCRIPT_SDK_CACHE_MISSING"
160
+ finally:
161
+ monkey.undo()
162
+
163
+
164
+ # ─── ensure ───────────────────────────────────────────────────────────────────
165
+
166
+ def test_ensure_skips_sync_when_fresh(tmp_path, monkeypatch, capsys):
167
+ cache = tmp_path / "env"
168
+ _write_cache_with_age(cache, age_hours=1)
169
+ monkeypatch.setattr(sdk_sync, "_cache_dir", lambda env: cache)
170
+
171
+ def _boom(*_args, **_kwargs):
172
+ raise AssertionError("sync must not run when cache is fresh")
173
+
174
+ monkeypatch.setattr(sdk_sync, "do_sync", _boom)
175
+ assert sdk_sync.do_ensure(server={}, env={"envName": "env"}, ttl_hours=168) == 0
176
+ result = json.loads(capsys.readouterr().out)
177
+ assert result["status"] == "ready"
178
+
179
+
180
+ def test_ensure_triggers_sync_when_missing(tmp_path, monkeypatch):
181
+ monkeypatch.setattr(sdk_sync, "_cache_dir", lambda env: tmp_path / "none")
182
+ calls = []
183
+ monkeypatch.setattr(sdk_sync, "do_sync",
184
+ lambda server, env: (calls.append(env), 0)[1])
185
+ assert sdk_sync.do_ensure(server={}, env={"envName": "env"}, ttl_hours=168) == 0
186
+ assert calls == [{"envName": "env"}]
187
+
188
+
189
+ # ─── cache write ──────────────────────────────────────────────────────────────
190
+
191
+ def test_write_cache_swaps_atomically_and_reports_counts(tmp_path, monkeypatch):
192
+ zip_path = _make_zip(tmp_path / "sdk.zip")
193
+ monkeypatch.setattr(sdk_sync, "SDK_ROOT", tmp_path / "sdkroot")
194
+ cache = tmp_path / "sdkroot" / "env"
195
+ cache.parent.mkdir(parents=True)
196
+ _write_cache_with_age(cache, age_hours=999, types=1) # stale old cache
197
+ meta = sdk_sync._write_cache(cache, zip_path, {"envName": "env"},
198
+ {"baseUrl": "https://unit.test/ierp"}, "deadbeef")
199
+ assert meta["types"] == 6
200
+ assert meta["modules"] == 2
201
+ assert meta["dtsFiles"] == 4
202
+ assert meta["zipSha256"] == "deadbeef"
203
+ # old .old backup is gone, new index and dts tree are in place
204
+ assert not (tmp_path / "sdkroot" / "env.old").exists()
205
+ assert (cache / "type-index.tsv").read_text(encoding="utf-8").count("\n") == 6
206
+ assert (cache / "dts" / "@cosmic" / "bos-core" / "kd" / "bos" / "form" /
207
+ "plugin.d.ts").is_file()
208
+ # meta json is valid and carries identity
209
+ stored = json.loads((cache / "sdk-meta.json").read_text(encoding="utf-8"))
210
+ assert stored["baseUrl"] == "https://unit.test/ierp"
211
+ assert stored["envName"] == "env"
212
+
213
+
214
+ def test_write_cache_creates_from_scratch(tmp_path, monkeypatch):
215
+ zip_path = _make_zip(tmp_path / "sdk.zip")
216
+ cache = tmp_path / "sdkroot" / "fresh-env"
217
+ cache.parent.mkdir(parents=True)
218
+ meta = sdk_sync._write_cache(cache, zip_path, {"envName": "fresh-env"},
219
+ {"baseUrl": "https://unit.test/ierp"}, "abc")
220
+ assert meta["types"] == 6
221
+ assert (cache / "dts" / "@cosmic" / "bos-script" / "java" / "math.d.ts").is_file()
@@ -43,6 +43,16 @@ def test_clean_plugin_passes():
43
43
  assert "KS-EXPORT-CONTRACT" not in _warn_rules(result)
44
44
 
45
45
 
46
+ def test_kwc_controller_uses_its_native_export_binding():
47
+ source = "class CoffeeController {}\nlet kwcController = new CoffeeController(); export { kwcController };"
48
+ result = _static(source, target_type="KWC_CONTROLLER")
49
+ assert result["status"] is True
50
+ assert "KS-EXPORT-CONTRACT" not in _warn_rules(result)
51
+ wrong = _static(CLEAN_PLUGIN, target_type="KWC_CONTROLLER")
52
+ assert "KS-EXPORT-CONTRACT" in _warn_rules(wrong)
53
+ assert "kwcController" in next(x["message"] for x in wrong["warnings"] if x["rule"] == "KS-EXPORT-CONTRACT")
54
+
55
+
46
56
  def test_empty_content_is_blocked():
47
57
  result = _static(" ")
48
58
  assert result["status"] is False