@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,403 @@
1
+ #!/usr/bin/env python3
2
+ """Build or incrementally update the L0 plugin-corpus index of a Cosmic library tree.
3
+
4
+ The index records binary facts only: every JAR with its hash and manifest version, every class
5
+ with its hash, hierarchy and annotation types, declared members, and, for the corpus layers,
6
+ the members, classes and dynamic class names each class refers to. It never decompiles, never
7
+ modifies the libraries and never contacts a service.
8
+
9
+ Usage::
10
+
11
+ build_index.py --lib-root <cosmic>/mservice-cosmic/lib --workspace <corpus workspace>
12
+
13
+ Each immediate subdirectory of ``--lib-root`` that contains JARs becomes a layer named after it
14
+ (bos, biz, cus, trd for a Cosmic installation). ``--refs-layer`` selects the layers whose
15
+ references are stored (default biz and cus); the other layers keep classes and members, which
16
+ is what resolving a reference needs. A rerun reuses every JAR whose size and modification time
17
+ are unchanged, and every JAR whose content hash is unchanged.
18
+
19
+ The workspace must be outside the repository and outside anything a tested agent can read; it
20
+ holds ``l0-index.sqlite`` and ``l0-index-summary.json``.
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import argparse
26
+ import hashlib
27
+ import json
28
+ import os
29
+ import sqlite3
30
+ import sys
31
+ import time
32
+ import zipfile
33
+ from concurrent.futures import ProcessPoolExecutor
34
+ from pathlib import Path
35
+
36
+ import jvm_classfile
37
+
38
+ SCHEMA_VERSION = 1
39
+ DB_NAME = "l0-index.sqlite"
40
+ SUMMARY_NAME = "l0-index-summary.json"
41
+ DEFAULT_REFS_LAYERS = ("biz", "cus")
42
+ # References into java.* are counted per class instead of stored: the corpus questions are about
43
+ # platform and business APIs, and those rows would roughly double the table. javax.* and com.sun.*
44
+ # are kept because third-party JARs in the library tree define many of them.
45
+ JAVA_PREFIX = "java."
46
+
47
+ _TABLES = """
48
+ CREATE TABLE IF NOT EXISTS meta(key TEXT PRIMARY KEY, value TEXT);
49
+ CREATE TABLE IF NOT EXISTS strings(id INTEGER PRIMARY KEY, value TEXT NOT NULL UNIQUE);
50
+ CREATE TABLE IF NOT EXISTS jars(
51
+ id INTEGER PRIMARY KEY, path TEXT NOT NULL UNIQUE, layer TEXT NOT NULL, size INTEGER NOT NULL,
52
+ mtime_ns INTEGER NOT NULL, sha256 TEXT NOT NULL, implementation_version TEXT, bundle_version TEXT,
53
+ class_count INTEGER NOT NULL, error_count INTEGER NOT NULL, skipped_count INTEGER NOT NULL,
54
+ refs_indexed INTEGER NOT NULL, status TEXT NOT NULL, indexed_at TEXT NOT NULL);
55
+ CREATE TABLE IF NOT EXISTS classes(
56
+ id INTEGER PRIMARY KEY, jar_id INTEGER NOT NULL, name_id INTEGER NOT NULL, super_id INTEGER,
57
+ access INTEGER NOT NULL, major INTEGER NOT NULL, sha256 TEXT NOT NULL, annotations TEXT,
58
+ deprecated INTEGER NOT NULL, source_file TEXT, entry TEXT NOT NULL, java_refs INTEGER);
59
+ CREATE TABLE IF NOT EXISTS interfaces(class_id INTEGER NOT NULL, iface_id INTEGER NOT NULL);
60
+ CREATE TABLE IF NOT EXISTS members(
61
+ class_id INTEGER NOT NULL, kind TEXT NOT NULL, name_id INTEGER NOT NULL, desc_id INTEGER NOT NULL,
62
+ access INTEGER NOT NULL, annotations TEXT, deprecated INTEGER NOT NULL);
63
+ CREATE TABLE IF NOT EXISTS refs(
64
+ class_id INTEGER NOT NULL, kind TEXT NOT NULL, owner_id INTEGER NOT NULL, name_id INTEGER,
65
+ desc_id INTEGER);
66
+ CREATE TABLE IF NOT EXISTS errors(jar_id INTEGER NOT NULL, entry TEXT, message TEXT NOT NULL);
67
+ """
68
+ _INDEXES = """
69
+ CREATE INDEX IF NOT EXISTS classes_name ON classes(name_id);
70
+ CREATE INDEX IF NOT EXISTS classes_jar ON classes(jar_id);
71
+ CREATE INDEX IF NOT EXISTS interfaces_class ON interfaces(class_id);
72
+ CREATE INDEX IF NOT EXISTS members_class ON members(class_id);
73
+ CREATE INDEX IF NOT EXISTS refs_class ON refs(class_id);
74
+ CREATE INDEX IF NOT EXISTS refs_target ON refs(owner_id, name_id, desc_id);
75
+ CREATE INDEX IF NOT EXISTS errors_jar ON errors(jar_id);
76
+ """
77
+
78
+
79
+ class CorpusIndexError(RuntimeError):
80
+ """A usage or workspace problem reported to the caller as a structured error."""
81
+
82
+ def __init__(self, code: str, message: str):
83
+ super().__init__(message)
84
+ self.code = code
85
+
86
+
87
+ def _hash_file(path: str) -> str:
88
+ digest = hashlib.sha256()
89
+ with open(path, "rb") as stream:
90
+ for block in iter(lambda: stream.read(1 << 20), b""):
91
+ digest.update(block)
92
+ return digest.hexdigest()
93
+
94
+
95
+ def _manifest_versions(jar: zipfile.ZipFile) -> tuple[str | None, str | None]:
96
+ try:
97
+ text = jar.read("META-INF/MANIFEST.MF").decode("utf-8", "replace")
98
+ except KeyError:
99
+ return None, None
100
+ attributes: dict[str, str] = {}
101
+ key = None
102
+ for line in text.replace("\r\n", "\n").split("\n"):
103
+ if line.startswith(" ") and key:
104
+ attributes[key] += line[1:]
105
+ elif ":" in line:
106
+ key, _, value = line.partition(":")
107
+ attributes[key.strip()] = value.strip()
108
+ return attributes.get("Implementation-Version") or None, attributes.get("Bundle-Version") or None
109
+
110
+
111
+ def parse_jar(task: tuple[str, str | None, bool]) -> dict:
112
+ """Worker: hash one JAR and, unless its hash is unchanged, parse its classes."""
113
+ path, known_sha, with_refs = task
114
+ sha = _hash_file(path)
115
+ if known_sha == sha:
116
+ return {"path": path, "sha256": sha, "unchanged": True}
117
+ classes: list[dict] = []
118
+ errors: list[tuple[str | None, str]] = []
119
+ skipped = 0
120
+ try:
121
+ with zipfile.ZipFile(path) as jar:
122
+ implementation, bundle = _manifest_versions(jar)
123
+ for info in jar.infolist():
124
+ entry = info.filename
125
+ if not entry.endswith(".class"):
126
+ continue
127
+ # Multi-release overlays and module/package descriptors are not classes a plugin
128
+ # extends or calls; they are counted, not indexed.
129
+ if entry.startswith("META-INF/") or entry.endswith(("module-info.class", "package-info.class")):
130
+ skipped += 1
131
+ continue
132
+ data = jar.read(info)
133
+ try:
134
+ parsed = jvm_classfile.parse_class(data)
135
+ except jvm_classfile.ClassFormatError as exc:
136
+ errors.append((entry, str(exc)[:300]))
137
+ continue
138
+ parsed["entry"] = entry
139
+ parsed["sha256"] = hashlib.sha256(data).hexdigest()
140
+ if not with_refs:
141
+ parsed["memberRefs"] = parsed["classRefs"] = parsed["dynamicClassNames"] = None
142
+ classes.append(parsed)
143
+ except (zipfile.BadZipFile, OSError) as exc:
144
+ return {"path": path, "sha256": sha, "unreadable": str(exc)[:300]}
145
+ return {"path": path, "sha256": sha, "implementationVersion": implementation, "bundleVersion": bundle,
146
+ "classes": classes, "errors": errors, "skipped": skipped, "refsIndexed": with_refs}
147
+
148
+
149
+ class _Strings:
150
+ """Intern repeated names in the strings table; ids are assigned here and flushed in bulk."""
151
+
152
+ def __init__(self, db: sqlite3.Connection):
153
+ self.db = db
154
+ self.ids = {value: key for key, value in db.execute("SELECT id, value FROM strings")}
155
+ self.next_id = max(self.ids.values(), default=0) + 1
156
+ self.pending: list[tuple[int, str]] = []
157
+
158
+ def id(self, value: str | None) -> int | None:
159
+ if value is None:
160
+ return None
161
+ known = self.ids.get(value)
162
+ if known is not None:
163
+ return known
164
+ key = self.next_id
165
+ self.next_id += 1
166
+ self.ids[value] = key
167
+ self.pending.append((key, value))
168
+ return key
169
+
170
+ def flush(self) -> None:
171
+ if self.pending:
172
+ self.db.executemany("INSERT INTO strings(id, value) VALUES (?, ?)", self.pending)
173
+ self.pending = []
174
+
175
+
176
+ def resolve_layers(lib_root: str | None, explicit: list[str]) -> dict[str, Path]:
177
+ layers: dict[str, Path] = {}
178
+ if lib_root:
179
+ root = Path(lib_root).expanduser().resolve()
180
+ if not root.is_dir():
181
+ raise CorpusIndexError("CORPUS_LIB_ROOT_INVALID", f"Library root is not a directory: {root}")
182
+ for child in sorted(root.iterdir()):
183
+ if child.is_dir() and any(child.glob("*.jar")):
184
+ layers[child.name] = child
185
+ for item in explicit:
186
+ name, separator, directory = item.partition("=")
187
+ path = Path(directory).expanduser().resolve()
188
+ if not separator or not name or not path.is_dir():
189
+ raise CorpusIndexError("CORPUS_LAYER_INVALID", f"Use --layer NAME=DIRECTORY with an existing directory: {item}")
190
+ layers[name] = path
191
+ if not layers:
192
+ raise CorpusIndexError("CORPUS_LAYERS_EMPTY", "No layer directory with JARs was found")
193
+ return layers
194
+
195
+
196
+ def _protected_roots() -> list[Path]:
197
+ """The skill package and, when present, the git checkout that contains this script."""
198
+ here = Path(__file__).resolve()
199
+ roots = [here.parents[2]] # the app-build skill package, which tested agents can read
200
+ for parent in here.parents:
201
+ if (parent / ".git").exists():
202
+ roots.append(parent)
203
+ break
204
+ return roots
205
+
206
+
207
+ def _workspace(path: str) -> Path:
208
+ workspace = Path(path).expanduser().resolve()
209
+ for root in _protected_roots():
210
+ if workspace == root or root in workspace.parents:
211
+ raise CorpusIndexError("CORPUS_WORKSPACE_INSIDE_REPOSITORY",
212
+ f"The corpus workspace must be outside {root}: {workspace}")
213
+ workspace.mkdir(parents=True, exist_ok=True)
214
+ return workspace
215
+
216
+
217
+ def _connect(db_path: Path) -> tuple[sqlite3.Connection, bool]:
218
+ fresh = not db_path.exists()
219
+ db = sqlite3.connect(str(db_path))
220
+ db.execute("PRAGMA journal_mode=WAL")
221
+ db.execute("PRAGMA synchronous=NORMAL")
222
+ db.executescript(_TABLES)
223
+ version = db.execute("SELECT value FROM meta WHERE key='schemaVersion'").fetchone()
224
+ if version and int(version[0]) != SCHEMA_VERSION:
225
+ db.close()
226
+ raise CorpusIndexError("CORPUS_SCHEMA_MISMATCH",
227
+ f"{db_path} uses schema {version[0]}; move it aside to rebuild with schema {SCHEMA_VERSION}")
228
+ db.execute("INSERT OR REPLACE INTO meta(key, value) VALUES ('schemaVersion', ?)", (str(SCHEMA_VERSION),))
229
+ if not fresh:
230
+ db.executescript(_INDEXES)
231
+ return db, fresh
232
+
233
+
234
+ def _delete_jar_rows(db: sqlite3.Connection, jar_id: int) -> None:
235
+ for table in ("refs", "members", "interfaces"):
236
+ db.execute(f"DELETE FROM {table} WHERE class_id IN (SELECT id FROM classes WHERE jar_id=?)", (jar_id,))
237
+ db.execute("DELETE FROM classes WHERE jar_id=?", (jar_id,))
238
+ db.execute("DELETE FROM errors WHERE jar_id=?", (jar_id,))
239
+
240
+
241
+ def _store_result(db: sqlite3.Connection, strings: _Strings, next_class: list[int], layer: str, stat: os.stat_result,
242
+ existing: dict | None, result: dict) -> str:
243
+ now = time.strftime("%Y-%m-%dT%H:%M:%S%z")
244
+ if result.get("unchanged"):
245
+ db.execute("UPDATE jars SET size=?, mtime_ns=? WHERE id=?", (stat.st_size, stat.st_mtime_ns, existing["id"]))
246
+ return "unchanged"
247
+ if existing:
248
+ _delete_jar_rows(db, existing["id"])
249
+ jar_id = existing["id"]
250
+ else:
251
+ jar_id = db.execute("SELECT COALESCE(MAX(id), 0) + 1 FROM jars").fetchone()[0]
252
+ classes = result.get("classes") or []
253
+ errors = result.get("errors") or []
254
+ if "unreadable" in result:
255
+ errors = [(None, result["unreadable"])]
256
+ row = (jar_id, result["path"], layer, stat.st_size, stat.st_mtime_ns, result["sha256"],
257
+ result.get("implementationVersion"), result.get("bundleVersion"), len(classes), len(errors),
258
+ result.get("skipped", 0), 1 if result.get("refsIndexed") else 0,
259
+ "unreadable" if "unreadable" in result else "indexed", now)
260
+ db.execute("INSERT OR REPLACE INTO jars(id, path, layer, size, mtime_ns, sha256, implementation_version, "
261
+ "bundle_version, class_count, error_count, skipped_count, refs_indexed, status, indexed_at) "
262
+ "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", row)
263
+ class_rows, interface_rows, member_rows, ref_rows = [], [], [], []
264
+ for parsed in classes:
265
+ class_id = next_class[0]
266
+ next_class[0] += 1
267
+ member_refs = parsed["memberRefs"]
268
+ java_refs = None
269
+ if member_refs is not None:
270
+ java_refs = 0
271
+ for kind, owner, name, descriptor in member_refs:
272
+ if owner.startswith(JAVA_PREFIX):
273
+ java_refs += 1
274
+ continue
275
+ ref_rows.append((class_id, kind, strings.id(owner), strings.id(name), strings.id(descriptor)))
276
+ ref_rows.extend((class_id, "C", strings.id(name), None, None)
277
+ for name in parsed["classRefs"] if not name.startswith(JAVA_PREFIX))
278
+ ref_rows.extend((class_id, "S", strings.id(name), None, None) for name in parsed["dynamicClassNames"])
279
+ class_rows.append((class_id, jar_id, strings.id(parsed["name"]), strings.id(parsed["super"]),
280
+ parsed["access"], parsed["major"], parsed["sha256"],
281
+ " ".join(parsed["annotations"]) or None, 1 if parsed["deprecated"] else 0,
282
+ parsed["sourceFile"], parsed["entry"], java_refs))
283
+ interface_rows.extend((class_id, strings.id(name)) for name in parsed["interfaces"])
284
+ for kind, members in (("F", parsed["fields"]), ("M", parsed["methods"])):
285
+ member_rows.extend((class_id, kind, strings.id(name), strings.id(descriptor), access,
286
+ " ".join(annotations) or None, 1 if deprecated else 0)
287
+ for name, descriptor, access, annotations, deprecated in members)
288
+ strings.flush()
289
+ db.executemany("INSERT INTO classes VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", class_rows)
290
+ db.executemany("INSERT INTO interfaces VALUES (?, ?)", interface_rows)
291
+ db.executemany("INSERT INTO members VALUES (?, ?, ?, ?, ?, ?, ?)", member_rows)
292
+ db.executemany("INSERT INTO refs VALUES (?, ?, ?, ?, ?)", ref_rows)
293
+ db.executemany("INSERT INTO errors VALUES (?, ?, ?)", [(jar_id, entry, message) for entry, message in errors])
294
+ return "changed" if existing else "new"
295
+
296
+
297
+ def summarize(db: sqlite3.Connection) -> dict:
298
+ layers = {}
299
+ for layer, jars, classes, errors, unreadable in db.execute(
300
+ "SELECT layer, COUNT(*), SUM(class_count), SUM(error_count), SUM(status='unreadable') "
301
+ "FROM jars GROUP BY layer ORDER BY layer"):
302
+ layers[layer] = {"jars": jars, "classes": classes or 0, "classErrors": errors or 0, "unreadableJars": unreadable or 0}
303
+ duplicates = {"fqcns": 0, "conflicting": 0, "byLayers": {}, "conflictingExamples": []}
304
+ for name, copies, hashes, layer_set in db.execute(
305
+ "SELECT s.value, COUNT(*), COUNT(DISTINCT c.sha256), GROUP_CONCAT(DISTINCT j.layer) "
306
+ "FROM classes c JOIN jars j ON j.id = c.jar_id JOIN strings s ON s.id = c.name_id "
307
+ "GROUP BY c.name_id HAVING COUNT(*) > 1"):
308
+ duplicates["fqcns"] += 1
309
+ key = "+".join(sorted(layer_set.split(",")))
310
+ bucket = duplicates["byLayers"].setdefault(key, {"fqcns": 0, "conflicting": 0})
311
+ bucket["fqcns"] += 1
312
+ if hashes > 1:
313
+ duplicates["conflicting"] += 1
314
+ bucket["conflicting"] += 1
315
+ if len(duplicates["conflictingExamples"]) < 20:
316
+ duplicates["conflictingExamples"].append({"class": name, "copies": copies, "distinctHashes": hashes,
317
+ "layers": sorted(layer_set.split(","))})
318
+ refs = {kind: count for kind, count in db.execute("SELECT kind, COUNT(*) FROM refs GROUP BY kind")}
319
+ return {"schemaVersion": SCHEMA_VERSION, "layers": layers, "duplicateClasses": duplicates, "storedRefs": refs}
320
+
321
+
322
+ def build(lib_root: str | None, layer_args: list[str], workspace: str, refs_layers: list[str],
323
+ workers: int | None) -> dict:
324
+ started = time.monotonic()
325
+ layers = resolve_layers(lib_root, layer_args)
326
+ unknown = sorted(set(refs_layers) - set(layers) - {"all"})
327
+ if unknown:
328
+ raise CorpusIndexError("CORPUS_REFS_LAYER_UNKNOWN", f"--refs-layer names no indexed layer: {', '.join(unknown)}")
329
+ with_refs = set(layers) if "all" in refs_layers else set(refs_layers)
330
+ target = _workspace(workspace)
331
+ db_path = target / DB_NAME
332
+ db, fresh = _connect(db_path)
333
+ try:
334
+ scanned: dict[str, tuple[str, os.stat_result]] = {}
335
+ for layer, directory in layers.items():
336
+ for jar in sorted(directory.glob("*.jar")):
337
+ scanned[str(jar.resolve())] = (layer, jar.stat())
338
+ existing = {path: {"id": key, "size": size, "mtime_ns": mtime, "sha256": sha, "refs": refs, "layer": layer}
339
+ for key, path, size, mtime, sha, refs, layer in
340
+ db.execute("SELECT id, path, size, mtime_ns, sha256, refs_indexed, layer FROM jars")}
341
+ counts = {"new": 0, "changed": 0, "unchanged": 0, "removed": 0}
342
+ for path in sorted(set(existing) - set(scanned)):
343
+ _delete_jar_rows(db, existing[path]["id"])
344
+ db.execute("DELETE FROM jars WHERE id=?", (existing[path]["id"],))
345
+ counts["removed"] += 1
346
+ tasks = []
347
+ for path, (layer, stat) in scanned.items():
348
+ known = existing.get(path)
349
+ needs_refs = layer in with_refs
350
+ reusable = known and known["layer"] == layer and bool(known["refs"]) == needs_refs
351
+ if reusable and known["size"] == stat.st_size and known["mtime_ns"] == stat.st_mtime_ns:
352
+ counts["unchanged"] += 1
353
+ continue
354
+ tasks.append((path, known["sha256"] if reusable else None, needs_refs))
355
+ strings = _Strings(db)
356
+ next_class = [db.execute("SELECT COALESCE(MAX(id), 0) + 1 FROM classes").fetchone()[0]]
357
+ pool = ProcessPoolExecutor(max_workers=workers or min(8, os.cpu_count() or 1)) if workers != 0 else None
358
+ try:
359
+ results = pool.map(parse_jar, tasks, chunksize=2) if pool else map(parse_jar, tasks)
360
+ for done, result in enumerate(results, start=1):
361
+ layer, stat = scanned[result["path"]]
362
+ outcome = _store_result(db, strings, next_class, layer, stat, existing.get(result["path"]), result)
363
+ counts[outcome] += 1
364
+ if done % 200 == 0:
365
+ db.commit()
366
+ finally:
367
+ if pool:
368
+ pool.shutdown()
369
+ if fresh:
370
+ db.executescript(_INDEXES)
371
+ meta = {"libRoot": str(Path(lib_root).expanduser().resolve()) if lib_root else None,
372
+ "layers": json.dumps({name: str(path) for name, path in layers.items()}),
373
+ "refsLayers": json.dumps(sorted(with_refs)), "updatedAt": time.strftime("%Y-%m-%dT%H:%M:%S%z")}
374
+ db.executemany("INSERT OR REPLACE INTO meta(key, value) VALUES (?, ?)", list(meta.items()))
375
+ db.commit()
376
+ summary = {"status": True, "database": str(db_path), "jars": counts, "refsLayers": sorted(with_refs),
377
+ **summarize(db), "elapsedSec": round(time.monotonic() - started, 1)}
378
+ finally:
379
+ db.close()
380
+ (target / SUMMARY_NAME).write_text(json.dumps(summary, ensure_ascii=False, indent=2), encoding="utf-8")
381
+ return summary
382
+
383
+
384
+ def main(argv: list[str] | None = None) -> int:
385
+ parser = argparse.ArgumentParser(description=__doc__.split("\n\n")[0])
386
+ parser.add_argument("--lib-root", help="Library root whose JAR subdirectories become layers")
387
+ parser.add_argument("--layer", action="append", default=[], help="Extra or overriding layer as NAME=DIRECTORY")
388
+ parser.add_argument("--workspace", required=True, help="Corpus workspace outside the repository")
389
+ parser.add_argument("--refs-layer", action="append", help="Layer whose references are stored; 'all' for every layer")
390
+ parser.add_argument("--workers", type=int, help="Parser processes (default: min(8, CPU count); 0 parses in-process)")
391
+ args = parser.parse_args(argv)
392
+ try:
393
+ summary = build(args.lib_root, args.layer, args.workspace, args.refs_layer or list(DEFAULT_REFS_LAYERS),
394
+ args.workers)
395
+ except CorpusIndexError as exc:
396
+ print(json.dumps({"status": False, "errorCode": exc.code, "message": str(exc)}, ensure_ascii=False))
397
+ return 2
398
+ print(json.dumps(summary, ensure_ascii=False, indent=2))
399
+ return 0
400
+
401
+
402
+ if __name__ == "__main__":
403
+ sys.exit(main())
@@ -0,0 +1,281 @@
1
+ """Read the structure of a JVM class file for the plugin corpus index.
2
+
3
+ The corpus holds roughly 670k classes, so running javap per class is far too slow; this module
4
+ reads the class file format directly with no JDK. It keeps what the index needs: the class's
5
+ names and hierarchy, declared members, annotation types, and the members, classes and
6
+ dynamic class names it refers to.
7
+
8
+ Method bodies are not decoded. A member reference is read from the constant pool, so it means
9
+ "this class names that member somewhere" (a call, a field access or a method handle) rather
10
+ than a count of call sites. The owner of a reference is the static receiver type the compiler
11
+ wrote at the use site; resolving it to the declaring class needs the whole hierarchy and is
12
+ the index consumer's job.
13
+
14
+ Names are returned in binary form with dots: ``kd.bos.algo.DataSet$Listener``.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import re
20
+ import struct
21
+
22
+ _U2 = struct.Struct(">H")
23
+ _U2U2 = struct.Struct(">HH")
24
+ _HEAD = struct.Struct(">HHH")
25
+ _MEMBER = struct.Struct(">HHHH")
26
+ _ATTRIBUTE = struct.Struct(">HI")
27
+
28
+ _ANNOTATION_ATTRIBUTES = ("RuntimeVisibleAnnotations", "RuntimeInvisibleAnnotations")
29
+ _REF_KINDS = {9: "F", 10: "M", 11: "I"}
30
+ # A string constant that names a platform or business class, e.g. for reflection or a plugin
31
+ # registered by class name. Package-only strings and ordinary field keys do not match.
32
+ _DYNAMIC_CLASS_RE = re.compile(r"^kd(?:\.[a-z_][\w]*)+\.[A-Z][\w$]*$")
33
+ _PRIMITIVES = {"B": "byte", "C": "char", "D": "double", "F": "float", "I": "int", "J": "long",
34
+ "S": "short", "Z": "boolean", "V": "void"}
35
+
36
+ ACC_INTERFACE = 0x0200
37
+ ACC_ABSTRACT = 0x0400
38
+ ACC_ANNOTATION = 0x2000
39
+
40
+
41
+ class ClassFormatError(ValueError):
42
+ """The bytes are not a class file this reader can parse."""
43
+
44
+
45
+ def _utf8(raw: bytes) -> str:
46
+ try:
47
+ return raw.decode("utf-8")
48
+ except UnicodeDecodeError:
49
+ # Modified UTF-8 writes NUL as C0 80 and supplementary characters as surrogate pairs.
50
+ try:
51
+ return raw.replace(b"\xc0\x80", b"\x00").decode("utf-8", "surrogatepass")
52
+ except UnicodeDecodeError:
53
+ return raw.decode("utf-8", "replace")
54
+
55
+
56
+ def _skip_element_value(data: bytes, pos: int) -> int:
57
+ tag = data[pos]
58
+ pos += 1
59
+ if tag in b"BCDFIJSZsc":
60
+ return pos + 2
61
+ if tag == 0x65: # 'e': enum type and constant name
62
+ return pos + 4
63
+ if tag == 0x40: # '@': nested annotation
64
+ return _skip_annotation_body(data, pos + 2)
65
+ if tag == 0x5B: # '[': array of element values
66
+ count = _U2.unpack_from(data, pos)[0]
67
+ pos += 2
68
+ for _ in range(count):
69
+ pos = _skip_element_value(data, pos)
70
+ return pos
71
+ raise ClassFormatError(f"unknown annotation element tag {tag}")
72
+
73
+
74
+ def _skip_annotation_body(data: bytes, pos: int) -> int:
75
+ """Skip the element pairs of an annotation whose type index was already read."""
76
+ pairs = _U2.unpack_from(data, pos)[0]
77
+ pos += 2
78
+ for _ in range(pairs):
79
+ pos = _skip_element_value(data, pos + 2)
80
+ return pos
81
+
82
+
83
+ def _annotation_types(data: bytes, pos: int, values: list) -> list[str]:
84
+ count = _U2.unpack_from(data, pos)[0]
85
+ pos += 2
86
+ names = []
87
+ for _ in range(count):
88
+ descriptor = values[_U2.unpack_from(data, pos)[0]]
89
+ if isinstance(descriptor, str) and descriptor.startswith("L") and descriptor.endswith(";"):
90
+ names.append(descriptor[1:-1].replace("/", "."))
91
+ pos = _skip_annotation_body(data, pos + 2)
92
+ return names
93
+
94
+
95
+ def descriptor_types(descriptor: str) -> tuple[list[str], str]:
96
+ """Split a method or field descriptor into parameter types and a return type.
97
+
98
+ Types use Java notation with dots and keep ``$`` for nested classes:
99
+ ``(Ljava/lang/String;[I)V`` -> (["java.lang.String", "int[]"], "void"). A field descriptor
100
+ has no parameters and its type is returned as the "return" type.
101
+ """
102
+ def read(pos: int) -> tuple[str, int]:
103
+ dims = 0
104
+ while descriptor[pos] == "[":
105
+ dims += 1
106
+ pos += 1
107
+ code = descriptor[pos]
108
+ if code == "L":
109
+ end = descriptor.index(";", pos)
110
+ name = descriptor[pos + 1:end].replace("/", ".")
111
+ pos = end + 1
112
+ elif code in _PRIMITIVES:
113
+ name = _PRIMITIVES[code]
114
+ pos += 1
115
+ else:
116
+ raise ClassFormatError(f"bad descriptor {descriptor!r}")
117
+ return name + "[]" * dims, pos
118
+
119
+ try:
120
+ if not descriptor.startswith("("):
121
+ return [], read(0)[0]
122
+ params = []
123
+ pos = 1
124
+ while descriptor[pos] != ")":
125
+ name, pos = read(pos)
126
+ params.append(name)
127
+ return params, read(pos + 1)[0]
128
+ except (IndexError, ValueError) as exc:
129
+ raise ClassFormatError(f"bad descriptor {descriptor!r}") from exc
130
+
131
+
132
+ def simple_type(java_type: str) -> str:
133
+ """``kd.bos.algo.DataSet$Listener[]`` -> ``Listener[]``, as API pages write parameter types."""
134
+ array = ""
135
+ while java_type.endswith("[]"):
136
+ array += "[]"
137
+ java_type = java_type[:-2]
138
+ return java_type.rsplit(".", 1)[-1].rsplit("$", 1)[-1] + array
139
+
140
+
141
+ def parse_class(data: bytes) -> dict:
142
+ """Parse one class file. Raises ClassFormatError for anything it cannot read."""
143
+ if len(data) < 10 or data[:4] != b"\xca\xfe\xba\xbe":
144
+ raise ClassFormatError("missing class file magic")
145
+ try:
146
+ return _parse(data)
147
+ except (struct.error, IndexError, KeyError, TypeError, AttributeError) as exc:
148
+ raise ClassFormatError(f"truncated or inconsistent class file: {exc}") from exc
149
+
150
+
151
+ def _parse(data: bytes) -> dict:
152
+ major = _U2.unpack_from(data, 6)[0]
153
+ count = _U2.unpack_from(data, 8)[0]
154
+ tags = [0] * count
155
+ values: list = [None] * count
156
+ pos = 10
157
+ index = 1
158
+ while index < count:
159
+ tag = data[pos]
160
+ if tag == 1:
161
+ length = _U2.unpack_from(data, pos + 1)[0]
162
+ values[index] = _utf8(data[pos + 3:pos + 3 + length])
163
+ pos += 3 + length
164
+ elif tag in (7, 8, 16, 19, 20):
165
+ values[index] = _U2.unpack_from(data, pos + 1)[0]
166
+ pos += 3
167
+ elif tag in (9, 10, 11, 12, 17, 18):
168
+ values[index] = _U2U2.unpack_from(data, pos + 1)
169
+ pos += 5
170
+ elif tag in (3, 4):
171
+ pos += 5
172
+ elif tag in (5, 6):
173
+ # Long and double constants take two pool slots.
174
+ tags[index] = tag
175
+ index += 2
176
+ pos += 9
177
+ continue
178
+ elif tag == 15:
179
+ values[index] = (data[pos + 1], _U2.unpack_from(data, pos + 2)[0])
180
+ pos += 4
181
+ else:
182
+ raise ClassFormatError(f"unknown constant pool tag {tag}")
183
+ tags[index] = tag
184
+ index += 1
185
+
186
+ def class_name(class_index: int) -> str:
187
+ return values[values[class_index]].replace("/", ".")
188
+
189
+ access, this_index, super_index = _HEAD.unpack_from(data, pos)
190
+ pos += 6
191
+ name = class_name(this_index)
192
+ super_name = class_name(super_index) if super_index else None
193
+ interface_count = _U2.unpack_from(data, pos)[0]
194
+ pos += 2
195
+ interfaces = [class_name(_U2.unpack_from(data, pos + 2 * i)[0]) for i in range(interface_count)]
196
+ pos += 2 * interface_count
197
+
198
+ def read_members(pos: int) -> tuple[list[tuple], int]:
199
+ member_count = _U2.unpack_from(data, pos)[0]
200
+ pos += 2
201
+ members = []
202
+ for _ in range(member_count):
203
+ member_access, name_index, descriptor_index, attribute_count = _MEMBER.unpack_from(data, pos)
204
+ pos += 8
205
+ annotations: list[str] = []
206
+ deprecated = False
207
+ for _ in range(attribute_count):
208
+ attribute_index, length = _ATTRIBUTE.unpack_from(data, pos)
209
+ pos += 6
210
+ attribute = values[attribute_index]
211
+ if attribute in _ANNOTATION_ATTRIBUTES:
212
+ annotations.extend(_annotation_types(data, pos, values))
213
+ elif attribute == "Deprecated":
214
+ deprecated = True
215
+ pos += length
216
+ members.append((values[name_index], values[descriptor_index], member_access,
217
+ tuple(annotations), deprecated or "java.lang.Deprecated" in annotations))
218
+ return members, pos
219
+
220
+ fields, pos = read_members(pos)
221
+ methods, pos = read_members(pos)
222
+
223
+ annotations: list[str] = []
224
+ deprecated = False
225
+ source_file = None
226
+ attribute_count = _U2.unpack_from(data, pos)[0]
227
+ pos += 2
228
+ for _ in range(attribute_count):
229
+ attribute_index, length = _ATTRIBUTE.unpack_from(data, pos)
230
+ pos += 6
231
+ attribute = values[attribute_index]
232
+ if attribute in _ANNOTATION_ATTRIBUTES:
233
+ annotations.extend(_annotation_types(data, pos, values))
234
+ elif attribute == "Deprecated":
235
+ deprecated = True
236
+ elif attribute == "SourceFile":
237
+ source_file = values[_U2.unpack_from(data, pos)[0]]
238
+ pos += length
239
+
240
+ member_refs = set()
241
+ class_refs = set()
242
+ dynamic_classes = set()
243
+ for index in range(1, count):
244
+ tag = tags[index]
245
+ if tag in _REF_KINDS:
246
+ class_index, nat_index = values[index]
247
+ owner = values[values[class_index]]
248
+ if owner.startswith("["):
249
+ continue # clone() and friends on an array type
250
+ name_index, descriptor_index = values[nat_index]
251
+ member_refs.add((_REF_KINDS[tag], owner.replace("/", "."), values[name_index], values[descriptor_index]))
252
+ elif tag == 7:
253
+ referenced = values[values[index]]
254
+ if referenced.startswith("["):
255
+ element = referenced.lstrip("[")
256
+ if not (element.startswith("L") and element.endswith(";")):
257
+ continue
258
+ referenced = element[1:-1]
259
+ referenced = referenced.replace("/", ".")
260
+ if referenced != name:
261
+ class_refs.add(referenced)
262
+ elif tag == 8:
263
+ text = values[values[index]]
264
+ if _DYNAMIC_CLASS_RE.match(text):
265
+ dynamic_classes.add(text)
266
+
267
+ return {
268
+ "name": name,
269
+ "major": major,
270
+ "access": access,
271
+ "super": super_name,
272
+ "interfaces": interfaces,
273
+ "annotations": annotations,
274
+ "deprecated": deprecated or "java.lang.Deprecated" in annotations,
275
+ "sourceFile": source_file,
276
+ "fields": fields,
277
+ "methods": methods,
278
+ "memberRefs": sorted(member_refs),
279
+ "classRefs": sorted(class_refs),
280
+ "dynamicClassNames": sorted(dynamic_classes),
281
+ }