@shundoo-ai/dsh-cosmic 1.0.21 → 1.0.22

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 (218) hide show
  1. package/package.json +1 -1
  2. package/skills/cosmic-build/LOCAL-PATCHES.md +43 -14
  3. package/skills/cosmic-build/SKILL.md +24 -9
  4. package/skills/cosmic-build/examples/java-rules/CosmicReportAggregation.java +26 -17
  5. package/skills/cosmic-build/examples/java-rules/CosmicReportFilters.java +96 -2
  6. package/skills/cosmic-build/knowledge/cosmic/arch/security/security_standard.md +4 -4
  7. package/skills/cosmic-build/knowledge/cosmic/platform/allimport.md +2 -1
  8. package/skills/cosmic-build/knowledge/cosmic/platform/core/dynamic-object-collection.md +6 -4
  9. package/skills/cosmic-build/knowledge/cosmic/platform/core/model.md +9 -9
  10. package/skills/cosmic-build/knowledge/cosmic/platform/core/view.md +5 -5
  11. package/skills/cosmic-build/knowledge/cosmic/platform/form-plugin.md +8 -4
  12. package/skills/cosmic-build/knowledge/cosmic/platform/list-plugin.md +5 -3
  13. package/skills/cosmic-build/knowledge/cosmic/platform/print-plugin-guide.md +1 -0
  14. package/skills/cosmic-build/knowledge/cosmic/platform/report-plugin.md +269 -57
  15. package/skills/cosmic-build/knowledge/cosmic/platform/reportlistdata-plugin.md +130 -23
  16. package/skills/cosmic-build/knowledge/cosmic/platform/reporttreedata-plugin.md +48 -28
  17. package/skills/cosmic-build/knowledge/cosmic/platform/scenes/_index.md +12 -3
  18. package/skills/cosmic-build/knowledge/cosmic/platform/scenes/common/attachment-name-process.md +17 -13
  19. package/skills/cosmic-build/knowledge/cosmic/platform/scenes/common/dynamic-update-field.md +4 -4
  20. package/skills/cosmic-build/knowledge/cosmic/platform/scenes/control/filter-grid/filter-grid-set-value.md +1 -1
  21. package/skills/cosmic-build/knowledge/cosmic/platform/scenes/control/list/filter-basedatafield.md +2 -3
  22. package/skills/cosmic-build/knowledge/cosmic/platform/scenes/control/list/filter-container-init.md +0 -1
  23. package/skills/cosmic-build/knowledge/cosmic/platform/scenes/control/list/filter-popup.md +0 -1
  24. package/skills/cosmic-build/knowledge/cosmic/platform/scenes/control/list/filter-value-parse.md +5 -6
  25. package/skills/cosmic-build/knowledge/cosmic/platform/scenes/control/list/list-show-form.md +1 -1
  26. package/skills/cosmic-build/knowledge/cosmic/platform/scenes/control/list/query-field-dynamic.md +0 -7
  27. package/skills/cosmic-build/knowledge/cosmic/platform/scenes/data-access/business-data/delete-service.md +24 -29
  28. package/skills/cosmic-build/knowledge/cosmic/platform/scenes/data-access/ksql/db-batch.md +4 -0
  29. package/skills/cosmic-build/knowledge/cosmic/platform/scenes/data-access/ksql/db-direct-access.md +4 -0
  30. package/skills/cosmic-build/knowledge/cosmic/platform/scenes/data-access/qfilter/entity-object-access.md +8 -3
  31. package/skills/cosmic-build/knowledge/cosmic/platform/scenes/data-access/qfilter/qfilter-compose.md +6 -5
  32. package/skills/cosmic-build/knowledge/cosmic/platform/scenes/event/after-bind-data-set-visible.md +5 -1
  33. package/skills/cosmic-build/knowledge/cosmic/platform/scenes/event/click-return-data.md +2 -2
  34. package/skills/cosmic-build/knowledge/cosmic/platform/scenes/event/closed-call-back-set-value.md +2 -2
  35. package/skills/cosmic-build/knowledge/cosmic/platform/scenes/event/register-listener.md +1 -1
  36. package/skills/cosmic-build/knowledge/cosmic/platform/scenes/event/set-filter-tab-org.md +3 -0
  37. package/skills/cosmic-build/knowledge/cosmic/platform/scenes/infra/dispatch-service/microservice-dev.md +18 -11
  38. package/skills/cosmic-build/knowledge/cosmic/platform/scenes/infra/dist-lock/dist-lock-basic.md +3 -6
  39. package/skills/cosmic-build/knowledge/cosmic/platform/scenes/infra/mq/mq-subscribe.md +5 -1
  40. package/skills/cosmic-build/knowledge/cosmic/platform/scenes/infra/thread-pool/async-operation.md +10 -6
  41. package/skills/cosmic-build/knowledge/cosmic/platform/scenes/infra/thread-pool/big-task-async.md +4 -2
  42. package/skills/cosmic-build/knowledge/cosmic/platform/scenes/infra/thread-pool/schedule-task.md +9 -7
  43. package/skills/cosmic-build/knowledge/cosmic/platform/scenes/operation/operation-interactive.md +33 -34
  44. package/skills/cosmic-build/knowledge/cosmic/platform/scenes/operation/operation-plugin-events.md +30 -22
  45. package/skills/cosmic-build/knowledge/cosmic/platform/scenes/report/report-async-query.md +133 -0
  46. package/skills/cosmic-build/knowledge/cosmic/platform/scenes/report/report-batch-query.md +26 -22
  47. package/skills/cosmic-build/knowledge/cosmic/platform/scenes/report/report-chart.md +112 -0
  48. package/skills/cosmic-build/knowledge/cosmic/platform/scenes/report/report-dataset-assembly.md +200 -0
  49. package/skills/cosmic-build/knowledge/cosmic/platform/scenes/report/report-head-filter.md +177 -0
  50. package/skills/cosmic-build/knowledge/cosmic/platform/scenes/report/report-hyperlink-drill.md +216 -0
  51. package/skills/cosmic-build/knowledge/cosmic/platform/scenes/report/report-list-data-query.md +12 -12
  52. package/skills/cosmic-build/knowledge/cosmic/platform/scenes/report/report-merge-column.md +44 -6
  53. package/skills/cosmic-build/knowledge/cosmic/platform/scenes/report/report-print-data.md +163 -0
  54. package/skills/cosmic-build/knowledge/cosmic/platform/scenes/report/report-row-data-toolbar.md +188 -0
  55. package/skills/cosmic-build/knowledge/cosmic/platform/scenes/report/report-subtotal-total.md +139 -0
  56. package/skills/cosmic-build/knowledge/cosmic/platform/scenes/report/report-tree-data-query.md +108 -12
  57. package/skills/cosmic-build/knowledge/cosmic/platform/scenes/report/report-tree-list.md +181 -0
  58. package/skills/cosmic-build/knowledge/cosmic/platform/scenes/system-service/report-plugin-interface.md +3 -0
  59. package/skills/cosmic-build/knowledge/cosmic/platform/scenes/tree/tree-basedata-cascade-confirm.md +14 -11
  60. package/skills/cosmic-build/knowledge/cosmic/platform/scenes/tree/tree-list-before-item-validate.md +1 -1
  61. package/skills/cosmic-build/knowledge/cosmic/platform/scenes/tree/tree-list-build-filter.md +1 -1
  62. package/skills/cosmic-build/knowledge/cosmic/platform/scenes/tree/tree-list-multi-level-filter.md +11 -11
  63. package/skills/cosmic-build/knowledge/cosmic/platform/scenes/tree/tree-list-refresh-node.md +22 -15
  64. package/skills/cosmic-build/knowledge/cosmic/platform/sdk/sdk-algo.md +2 -2
  65. package/skills/cosmic-build/knowledge/cosmic/platform/sdk/sdk-entity-model.md +3 -4
  66. package/skills/cosmic-build/knowledge/cosmic/platform/sdk/sdk-exception.md +9 -5
  67. package/skills/cosmic-build/knowledge/cosmic/platform/sdk/sdk-orm-access.md +4 -0
  68. package/skills/cosmic-build/knowledge/cosmic/platform/sdk/sdk-request-context.md +7 -3
  69. package/skills/cosmic-build/knowledge/cosmic/platform/standards.md +28 -3
  70. package/skills/cosmic-build/references/compile-guide.md +36 -1
  71. package/skills/cosmic-build/references/dcs-code-rules.md +180 -0
  72. package/skills/cosmic-build/references/delivery.md +16 -2
  73. package/skills/cosmic-build/references/execution-contract.md +15 -3
  74. package/skills/cosmic-build/references/java-plugin-delivery.md +21 -11
  75. package/skills/cosmic-build/references/plugin-handoff-input.md +82 -24
  76. package/skills/cosmic-build/references/plugin-runtime-routing.md +23 -6
  77. package/skills/cosmic-build/references/report-filter-contract.md +53 -15
  78. package/skills/cosmic-build/references/sdk-inspection.md +4 -1
  79. package/skills/cosmic-build/references/type-registry.json +26 -17
  80. package/skills/cosmic-build/schemas/data-model-authoring.schema.json +1 -1
  81. package/skills/cosmic-build/scripts/_http_client.py +3 -0
  82. package/skills/cosmic-build/scripts/compile_plugin_source.py +251 -42
  83. package/skills/cosmic-build/scripts/configure_list.py +112 -0
  84. package/skills/cosmic-build/scripts/dcs-rule-catalog.json +799 -0
  85. package/skills/cosmic-build/scripts/dcs_code_gate.py +1976 -0
  86. package/skills/cosmic-build/scripts/execution_binding.py +25 -0
  87. package/skills/cosmic-build/scripts/platform_api_contract.py +352 -7
  88. package/skills/cosmic-build/scripts/plugin_source_gate.py +9 -1
  89. package/skills/cosmic-build/scripts/plugin_target_sdk.py +12 -8
  90. package/skills/cosmic-build/scripts/prepare_plugin_handoff.py +488 -88
  91. package/skills/cosmic-build/scripts/query_plugin_handoff.py +207 -20
  92. package/skills/cosmic-build/scripts/register_plugins.py +27 -10
  93. package/skills/cosmic-build/scripts/render_output_cards.py +57 -20
  94. package/skills/cosmic-build/scripts/resolve_output_path.py +60 -26
  95. package/skills/cosmic-build/scripts/run_java_tests.py +162 -0
  96. package/skills/cosmic-build/scripts/sync_generated_locale_assets.py +28 -0
  97. package/skills/cosmic-build/scripts/test/fixtures/ReportSdkCheck.java +1 -22
  98. package/skills/cosmic-build/scripts/test/fixtures/list_column_property_declarations.json +887 -0
  99. package/skills/cosmic-build/scripts/test/fixtures/operation-entry-field-prepare.java +78 -0
  100. package/skills/cosmic-build/scripts/test/test_add_field_required_contract.py +12 -0
  101. package/skills/cosmic-build/scripts/test/test_batch5_drift_governance.py +23 -2
  102. package/skills/cosmic-build/scripts/test/test_compile_plugin_source.py +287 -4
  103. package/skills/cosmic-build/scripts/test/test_configure_list_delivery.py +132 -0
  104. package/skills/cosmic-build/scripts/test/test_cosmic_sdk_examples.py +58 -1
  105. package/skills/cosmic-build/scripts/test/test_data_model_contract.py +19 -0
  106. package/skills/cosmic-build/scripts/test/test_dcs_code_gate.py +901 -0
  107. package/skills/cosmic-build/scripts/test/test_delivery_stage_boundary.py +1 -1
  108. package/skills/cosmic-build/scripts/test/test_direct_cli_architecture.py +2 -0
  109. package/skills/cosmic-build/scripts/test/test_http_client.py +6 -0
  110. package/skills/cosmic-build/scripts/test/test_java_toolchain_feedback.py +206 -0
  111. package/skills/cosmic-build/scripts/test/test_kd_api.py +21 -0
  112. package/skills/cosmic-build/scripts/test/test_list_configuration_closure.py +184 -0
  113. package/skills/cosmic-build/scripts/test/test_mcp_cli.py +139 -2
  114. package/skills/cosmic-build/scripts/test/test_menu_creation_closure.py +16 -0
  115. package/skills/cosmic-build/scripts/test/test_modeling_input_recovery.py +55 -1
  116. package/skills/cosmic-build/scripts/test/test_open_world_cli_catalog.py +1 -1
  117. package/skills/cosmic-build/scripts/test/test_operation_field_preparation.py +84 -0
  118. package/skills/cosmic-build/scripts/test/test_platform_api_contract.py +322 -1
  119. package/skills/cosmic-build/scripts/test/test_plugin_handoff.py +951 -4
  120. package/skills/cosmic-build/scripts/test/test_plugin_source_gate.py +30 -2
  121. package/skills/cosmic-build/scripts/test/test_plugin_target_sdk.py +21 -0
  122. package/skills/cosmic-build/scripts/test/test_property_reroute.py +165 -0
  123. package/skills/cosmic-build/scripts/test/test_property_schema_first.py +35 -0
  124. package/skills/cosmic-build/scripts/test/test_query_plugin_handoff.py +221 -0
  125. package/skills/cosmic-build/scripts/test/test_register_plugins.py +76 -4
  126. package/skills/cosmic-build/scripts/test/test_render_output_cards.py +75 -3
  127. package/skills/cosmic-build/scripts/test/test_report_business_contract.py +27 -5
  128. package/skills/cosmic-build/scripts/test/test_report_doc_contract_sync.py +103 -11
  129. package/skills/cosmic-build/scripts/test/test_report_plugin_source_gate.py +46 -16
  130. package/skills/cosmic-build/scripts/test/test_report_properties.py +74 -15
  131. package/skills/cosmic-build/scripts/test/test_report_query_data_source.py +3 -1
  132. package/skills/cosmic-build/scripts/test/test_report_sdk_optimization.py +1 -2
  133. package/skills/cosmic-build/scripts/test/test_test_environment.py +76 -1
  134. package/skills/cosmic-build/scripts/test_environment.py +67 -1
  135. package/skills/cosmic-build/skills/cosmic-coding-impl/SKILL.md +52 -10
  136. package/skills/cosmic-build/skills/cosmic-meta-api/apis/ai_meta.md +16 -3
  137. package/skills/cosmic-build/skills/cosmic-meta-api/contracts/cli/report-write-contracts.json +59 -26
  138. package/skills/cosmic-build/skills/cosmic-meta-api/contracts/cli/rule-domain.json +1 -1
  139. package/skills/cosmic-build/skills/cosmic-meta-api/contracts/schema/report-write.schema.json +75 -18
  140. package/skills/cosmic-build/skills/cosmic-meta-api/modules/pdm/scripts/pdm_auto.py +39 -0
  141. package/skills/cosmic-build/skills/cosmic-meta-api/modules/pdm/scripts/pdm_client.py +97 -10
  142. package/skills/cosmic-build/skills/cosmic-meta-api/modules/verify/SKILL.md +10 -12
  143. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/_core/cmd_apps.py +3 -1
  144. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/_core/cmd_formactions.py +38 -29
  145. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/_core/cmd_listui.py +400 -233
  146. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/_core/cmd_metaops.py +87 -45
  147. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/_core/cmd_properties.py +42 -15
  148. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/_core/cmd_rules.py +135 -5
  149. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/_core/config.py +1 -1
  150. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/_core/execution_errors.py +1 -1
  151. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/_core/formatters.py +17 -11
  152. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/_core/property_reroute.py +15 -5
  153. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/_core/report_ops.py +1429 -238
  154. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/_core/validators.py +56 -28
  155. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/cosmic_api.py +380 -134
  156. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/field_decorator.py +1 -1
  157. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/meta_export.py +12 -11
  158. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/meta_verifier.py +42 -0
  159. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_add_field_placement_closure.py +8 -8
  160. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_build_configuration_readback.py +94 -0
  161. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_build_receipt_resume.py +47 -1
  162. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_metadata_unchanged_fail_closed.py +87 -0
  163. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_report_dym_evidence.py +23 -23
  164. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_report_entity_integrity.py +279 -4
  165. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_report_field_guard.py +1 -1
  166. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_report_plugin_contract.py +509 -32
  167. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_report_readback_derivation.py +178 -1
  168. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_report_recreate_plan.py +125 -10
  169. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_report_serve_contract.py +208 -0
  170. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_report_typed_filters.py +259 -50
  171. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_rule_inventory_perf.py +29 -0
  172. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_rule_write_closure.py +20 -0
  173. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_run_batch_post_build_recovery.py +18 -0
  174. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_run_batch_verification_evidence.py +35 -0
  175. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_run_identity_guard.py +25 -0
  176. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_time_range_rule.py +98 -0
  177. package/skills/cosmic-build/skills/cosmic-meta-knowledge/domain-model/report-model.md +20 -9
  178. package/skills/cosmic-build/skills/cosmic-meta-knowledge/generation/report-modeling-guide.md +55 -25
  179. package/skills/cosmic-build/skills/cosmic-meta-knowledge/property-guides/report-column-properties.md +15 -39
  180. package/skills/cosmic-build/skills/cosmic-meta-modeling/SKILL.md +19 -17
  181. package/skills/cosmic-build/skills/cosmic-meta-modeling/modules/report/SKILL.md +82 -38
  182. package/skills/cosmic-build/skills/cosmic-meta-modeling/modules/report/examples/golden-example-report-batch.md +64 -19
  183. package/skills/cosmic-build/skills/cosmic-meta-modeling/modules/report/examples/golden-example-report-plugin.md +32 -20
  184. package/skills/cosmic-build/skills/cosmic-meta-modeling/modules/report/examples/golden-example-report-tree.md +78 -31
  185. package/skills/cosmic-build/skills/cosmic-meta-modeling/modules/report/report-artifact-schema.json +2 -1
  186. package/skills/cosmic-build/skills/cosmic-meta-modeling/references/authoring-companion-fields.md +3 -0
  187. package/skills/cosmic-build/skills/cosmic-meta-modeling/references/authoring-list-configuration.md +2 -0
  188. package/skills/cosmic-build/skills/cosmic-meta-modeling/scripts/data_model_contract.py +3 -1
  189. package/skills/cosmic-build/skills/cosmic-meta-modeling/scripts/data_model_emit.py +27 -4
  190. package/skills/cosmic-build/skills/cosmic-meta-modeling/scripts/report_validator.py +99 -44
  191. package/skills/cosmic-build/skills/cosmic-meta-modeling/scripts/test_report_examples.py +31 -4
  192. package/skills/cosmic-build/skills/cosmic-meta-modeling/scripts/test_report_validator.py +129 -29
  193. package/skills/cosmic-build/skills/cosmic-meta-modifier/SKILL.md +6 -2
  194. package/skills/cosmic-build/skills/cosmic-meta-modifier/modules/operation/validation.md +2 -0
  195. package/skills/cosmic-build/skills/cosmic-meta-modifier/modules/property/SKILL.md +1 -1
  196. package/skills/cosmic-build/skills/cosmic-meta-modifier/modules/report-op/SKILL.md +104 -25
  197. package/skills/cosmic-build/skills/cosmic-meta-modifier/modules/rule/SKILL.md +9 -0
  198. package/skills/cosmic-build/skills/cosmic-meta-modifier/modules/verify/usage-guide.md +2 -2
  199. package/skills/cosmic-build/skills/cosmic-meta-modifier/references/list-filter-modification.md +39 -12
  200. package/skills/cosmic-build/skills/mcp-server/SKILL.md +39 -24
  201. package/skills/cosmic-build/skills/mcp-server/scripts/mcp_cli.py +20 -6
  202. package/skills/cosmic-dev-best-practices/SKILL.md +1 -1
  203. package/skills/cosmic-dev-best-practices/references/README.md +2 -2
  204. package/skills/cosmic-dev-best-practices/references/basedata-dev-convention.md +2 -2
  205. package/skills/cosmic-dev-best-practices/references/bill-dev-convention.md +4 -4
  206. package/skills/cosmic-dev-best-practices/references/design-document-convention.md +2 -2
  207. package/skills/cosmic-dev-best-practices/references/design-examples/basedata-design-example.md +4 -4
  208. package/skills/cosmic-dev-best-practices/references/design-examples/bill-design-example.md +13 -13
  209. package/skills/cosmic-dev-best-practices/references/elements/CustomerField.md +1 -1
  210. package/skills/cosmic-dev-best-practices/references/elements/README.md +1 -1
  211. package/skills/cosmic-dev-best-practices/references/elements/SupplierField.md +1 -1
  212. package/skills/cosmic-dev-best-practices/references/elements/common-field-property-conventions.md +109 -13
  213. package/skills/cosmic-dev-best-practices/references/layout-control-convention.md +1 -1
  214. package/skills/erp-analysis/references/delivery-contract.md +2 -1
  215. package/skills/erp-analysis/templates/AGENTS.template.md +3 -1
  216. package/skills/cosmic-build/assets/platform-report-filters/TypedReportFilterFields.java +0 -169
  217. package/skills/cosmic-build/assets/platform-report-filters/platform.patch +0 -98
  218. package/skills/cosmic-build/scripts/apply_report_filter_extension.py +0 -39
@@ -12,7 +12,8 @@ from unittest import mock
12
12
  import pytest
13
13
 
14
14
 
15
- SCRIPT = Path(__file__).resolve().parents[1] / "render_output_cards.py"
15
+ ROOT = Path(__file__).resolve().parents[2]
16
+ SCRIPT = (ROOT / ".locales" / "zh" if os.environ.get("APP_BUILD_CARD_TEST_LOCALE") == "zh" else ROOT) / "scripts" / "render_output_cards.py"
16
17
  SPEC = importlib.util.spec_from_file_location("render_output_cards", SCRIPT)
17
18
  assert SPEC and SPEC.loader
18
19
  CARDS = importlib.util.module_from_spec(SPEC)
@@ -116,7 +117,7 @@ def test_cli_does_not_request_output_check_when_rendering_fails(capsys, tmp_path
116
117
 
117
118
  @pytest.mark.parametrize("locale", ["", ".locales/zh"])
118
119
  def test_loaded_skill_exposes_output_check_through_real_host_router(locale):
119
- root = SCRIPT.parents[1]
120
+ root = ROOT
120
121
  program = """
121
122
  const { skillRelevantToolNames } = await import(process.argv[1] + "/src/tool/router.ts");
122
123
  const output = await Bun.file(process.argv[2]).text();
@@ -518,8 +519,68 @@ def test_card_carries_list_preview_url_and_degrades_silently(tmp_path: Path) ->
518
519
  assert "listUrl" not in handoff["forms"][0]
519
520
 
520
521
 
522
+ @pytest.mark.parametrize('failure,status,reuse', [
523
+ (502, 'archived', True), (503, 'archived', True), (504, 'archived', True),
524
+ (403, 'unavailable', False), (400, 'unavailable', False),
525
+ ])
526
+ def test_preview_fallback_is_scoped_and_has_explicit_evidence(tmp_path, failure, status, reuse):
527
+ from urllib.error import HTTPError
528
+ previous = {'title': 'Target', 'url': 'https://example.test/ierp/?formId=target',
529
+ 'metadata': 'https://example.test/ierp/meta?formNumber=target'}
530
+ archive = tmp_path / 'archive.json'
531
+ archive.write_text(json.dumps({'formNumbers': [{'number': 'target', 'name': 'Target'}],
532
+ 'artifacts': [previous], 'previewEvidence': {'other': {'status': 'fresh'}}}))
533
+ with mock.patch('kd_env_loader.load_effective_skill_config', return_value={'server': {'baseUrl': 'https://example.test/ierp'}}), \
534
+ mock.patch.object(CARDS, 'get_preview_url', side_effect=HTTPError('https://example.test', failure, 'failure', {}, None)) as preview:
535
+ blocks = CARDS.render(str(tmp_path), target_form_number='target')
536
+ card = json.loads(blocks[0].splitlines()[1])
537
+ assert card['url'] == (previous['url'] if reuse else '')
538
+ preview.assert_called_once()
539
+ saved = json.loads(archive.read_text())
540
+ assert saved['previewEvidence']['target']['status'] == status
541
+ assert saved['previewEvidence']['target']['runtimeVerified'] is False
542
+ assert saved['previewEvidence']['target']['listStatus'] == 'not-run'
543
+ assert saved['previewEvidence']['other']['status'] == 'fresh'
544
+ assert 'status' not in card and 'runtimeVerified' not in card
545
+
546
+
547
+ @pytest.mark.parametrize('url', ['https://other.test/ierp/?formId=target',
548
+ 'https://example.test/other/?formId=target', 'https://example.test/ierp-other/?formId=target',
549
+ 'javascript:alert(1)', 'https://user:secret@example.test/ierp/?formId=target'])
550
+ def test_archived_preview_never_crosses_environment_or_accepts_credentials(url):
551
+ assert CARDS._archived_preview_urls([{'url': url, 'metadata': 'https://example.test/meta?formNumber=target'}],
552
+ 'https://example.test/ierp') == {}
553
+
554
+
555
+ def test_business_preview_error_does_not_restore_old_link(tmp_path):
556
+ archive = tmp_path / 'archive.json'
557
+ archive.write_text(json.dumps({'formNumbers': [{'number': 'target', 'name': 'Target'}],
558
+ 'previewEvidence': [], 'artifacts': [{'url': 'https://example.test/?formId=target',
559
+ 'metadata': 'https://example.test/meta?formNumber=target'}]}))
560
+ with mock.patch('kd_env_loader.load_effective_skill_config', return_value={'server': {'baseUrl': 'https://example.test'}}), \
561
+ mock.patch.object(CARDS, 'get_preview_url', side_effect=CARDS.PreviewUrlError('PREVIEW_URL_LOOKUP_FAILED: denied')):
562
+ blocks = CARDS.render(str(tmp_path), target_form_number='target')
563
+ assert json.loads(blocks[0].splitlines()[1])['url'] == ''
564
+ assert json.loads(archive.read_text())['previewEvidence']['target']['status'] == 'unavailable'
565
+
566
+
567
+ def test_cli_publishes_preview_evidence_without_leaking_previous_call(capsys, tmp_path):
568
+ block = ':::render:kdform\n{"title":"Target","url":""}\n:::'
569
+ def render(*args, **kwargs):
570
+ CARDS._PREVIEW_EVIDENCE = {'target': {'status': 'archived', 'runtimeVerified': False}}
571
+ return [block]
572
+ with mock.patch.object(sys, 'argv', [str(SCRIPT), '--app-root', str(tmp_path)]), \
573
+ mock.patch.object(CARDS, 'render', side_effect=render), mock.patch.object(CARDS, '_DYM_BACKGROUND_NOTICE', ''):
574
+ assert CARDS.main() == 0
575
+ assert 'PREVIEW_EVIDENCE=' in capsys.readouterr().out
576
+ with mock.patch.object(sys, 'argv', [str(SCRIPT), '--app-root', str(tmp_path)]), \
577
+ mock.patch.object(CARDS, 'render', return_value=[block]), mock.patch.object(CARDS, '_DYM_BACKGROUND_NOTICE', ''):
578
+ assert CARDS.main() == 0
579
+ assert 'PREVIEW_EVIDENCE=' not in capsys.readouterr().out
580
+
581
+
521
582
  def test_localized_card_cli_keeps_target_filter_contract() -> None:
522
- localized = SCRIPT.parents[1] / ".locales" / "zh" / "scripts" / SCRIPT.name
583
+ localized = ROOT / ".locales" / "zh" / "scripts" / SCRIPT.name
523
584
  result = subprocess.run(
524
585
  [sys.executable, str(localized), "--help"],
525
586
  check=True,
@@ -530,3 +591,14 @@ def test_localized_card_cli_keeps_target_filter_contract() -> None:
530
591
  assert "--form-number" in result.stdout
531
592
  assert "--form-name" in result.stdout
532
593
  assert "repeat for" in result.stdout and "multiple forms" in result.stdout
594
+
595
+
596
+ def test_localized_renderer_runs_regressions_in_its_own_process():
597
+ """中文运行树在独立进程中跑同一组回归,避免复用 canonical 的模块缓存。"""
598
+ if os.environ.get("APP_BUILD_CARD_TEST_LOCALE") == "zh":
599
+ pytest.skip("已在中文独立进程中")
600
+ result = subprocess.run([sys.executable, "-m", "pytest", str(Path(__file__).resolve()),
601
+ "-q", "-k", "not test_localized_renderer_runs_regressions_in_its_own_process"],
602
+ cwd=ROOT, env={**os.environ, "APP_BUILD_CARD_TEST_LOCALE": "zh"},
603
+ capture_output=True, text=True, encoding="utf-8", timeout=60)
604
+ assert result.returncode == 0, result.stdout + result.stderr
@@ -99,6 +99,18 @@ class ReportBusinessContractTest(unittest.TestCase):
99
99
  cosmic_api._execute_report_pipeline({}, "token", artifact, "APP-ID", "UNIT-ID")
100
100
  create_page.assert_not_called()
101
101
 
102
+ def test_default_sort_preflight_fails_before_create_page(self):
103
+ for plugins, sort_key, error in (([], "a", "REPORT_DEFAULT_SORT_PLUGIN_REQUIRED"),
104
+ ([{"className": "kd.test.P", "enabled": False}], "a", "REPORT_DEFAULT_SORT_PLUGIN_REQUIRED"),
105
+ ([{"className": "kd.test.P"}], "missing", "sort columns not found")):
106
+ artifact = self._dym_artifact()
107
+ artifact["reportPlugins"] = plugins
108
+ artifact["reportSorts"][0]["fieldKey"] = sort_key
109
+ with self.subTest(plugins=plugins, key=sort_key), mock.patch.object(cosmic_api, "do_create_page") as create:
110
+ with self.assertRaisesRegex(ValueError, error):
111
+ cosmic_api._execute_report_pipeline({}, "token", artifact, "APP", "UNIT")
112
+ create.assert_not_called()
113
+
102
114
  def test_data_source_write_surface_is_gone(self) -> None:
103
115
  for name in ("normalize_data_source_input", "add_data_source"):
104
116
  self.assertFalse(hasattr(report_ops, name), name)
@@ -251,23 +263,32 @@ class ReportBusinessContractTest(unittest.TestCase):
251
263
  "reportSorts": [{"fieldKey": "a", "direction": "desc"}],
252
264
  "reportPlugins": [{"className": "kd.uhyl.report.P", "enabled": True}],
253
265
  }),
266
+ tempfile.TemporaryDirectory() as root,
254
267
  ):
255
268
  result = cosmic_api._execute_report_pipeline(
256
- {}, "token", self._dym_artifact(), "APP-ID", "UNIT-ID", app_root="/tmp/app")
269
+ {}, "token", self._dym_artifact(), "APP-ID", "UNIT-ID", app_root=root)
270
+ persisted = json.loads(Path(result["reportPluginContract"]["path"]).read_text(encoding="utf-8"))
271
+ self.assertEqual(persisted["defaultSorts"], result["reportDefaultSorts"]["requirements"])
272
+ self.assertFalse(persisted["defaultSortPolicy"]["runtimeVerified"])
257
273
 
258
274
  # The invariant is the order, not the exact call list: the export is synced after every
259
275
  # write, so it cannot be older than what it is used to prove.
260
276
  sync_at = calls.index("syncDym")
261
- for write in ("columns", "sorts", "plugin"):
277
+ self.assertNotIn("sorts", calls)
278
+ for write in ("columns", "plugin"):
262
279
  self.assertLess(calls.index(write), sync_at, f"{write} ran after the sync")
263
280
  self.assertEqual("syncDym", calls[-1])
264
- # Both sections the layout cannot carry are now proved, so verification is real.
281
+ # 只证明元数据列和插件;默认排序要求被保留,但运行时仍未验证。
265
282
  self.assertEqual("formLayout+dym", result["readBackSource"])
266
283
  self.assertTrue(result["verification"]["verified"])
267
284
  self.assertEqual(
268
- {"reportColumns", "reportSorts", "reportPlugins"},
285
+ {"reportColumns", "reportPlugins"},
269
286
  {check["section"] for check in result["verification"]["checks"]},
270
287
  )
288
+ self.assertEqual(result["reportDefaultSorts"]["requirements"], [
289
+ {"fieldKey": "a", "dateIndex": "a", "direction": "desc"}])
290
+ self.assertFalse(result["reportDefaultSorts"]["runtimeVerified"])
291
+ self.assertFalse(result["reportDefaultSorts"]["metadataWritten"])
271
292
 
272
293
  def test_pipeline_survives_a_failed_design_state_sync(self) -> None:
273
294
  """Design-state evidence is additive; losing it must not fail a successful write."""
@@ -282,9 +303,10 @@ class ReportBusinessContractTest(unittest.TestCase):
282
303
  return_value=self._derived_schema()),
283
304
  mock.patch.object(cosmic_api, "_export_report_dym_evidence",
284
305
  side_effect=RuntimeError("sync root not configured")),
306
+ tempfile.TemporaryDirectory() as root,
285
307
  ):
286
308
  result = cosmic_api._execute_report_pipeline(
287
- {}, "token", self._dym_artifact(), "APP-ID", "UNIT-ID", app_root="/tmp/app")
309
+ {}, "token", self._dym_artifact(), "APP-ID", "UNIT-ID", app_root=root)
288
310
 
289
311
  self.assertTrue(result["ok"])
290
312
  self.assertEqual("formLayout", result["readBackSource"])
@@ -10,8 +10,13 @@ Every failure this file guards against has already happened at least once:
10
10
  contract rejects;
11
11
  - guidance to call `setFieldType(ReportColumn.TYPE_AMOUNT)`, a constant name that appears
12
12
  nowhere in the knowledge base;
13
- - a documented `id` / `pid` DataSet-column convention for tree reports, which no API page
14
- supports.
13
+ - an `id` / `pid` DataSet-column convention for tree reports, where the standard product's tree
14
+ reports carry `rowid` / `pid` / `isgroupnode`;
15
+ - a left-tree example registering its tree plugin as a second `reportPlugins` entry, which is
16
+ the report list's data-fetch slot, on a template that has no left tree;
17
+ - a left-tree example created from `bos_grouprpttpl` as if it were a left-tree template. It is
18
+ the grouped report, and no public template presets a left tree: one exists only after a user
19
+ adds the report tree control, so a creation Artifact cannot declare `reportProperties.tree`.
15
20
 
16
21
  Prose is not tested here. What is tested is every place where documentation restates a value
17
22
  the implementation owns, because that is where the two silently drift apart.
@@ -25,6 +30,8 @@ import unittest
25
30
  from pathlib import Path
26
31
 
27
32
  APP_BUILD_ROOT = Path(__file__).resolve().parents[2]
33
+ if APP_BUILD_ROOT.name == "zh" and APP_BUILD_ROOT.parent.name == ".locales":
34
+ APP_BUILD_ROOT = APP_BUILD_ROOT.parent.parent
28
35
  ZH_ROOT = APP_BUILD_ROOT / ".locales" / "zh"
29
36
 
30
37
  VALIDATOR = APP_BUILD_ROOT / "skills/cosmic-meta-modeling/scripts/report_validator.py"
@@ -66,8 +73,8 @@ EMITTED_RULE = re.compile(r'rule="(R-[a-z-]+)"')
66
73
  # and in the sentence-boundary pattern below are matcher input, not prose.
67
74
  _DENIAL_MARKERS = (
68
75
  "no longer exists", "no documented", "not invent", "do not", "does not", "never",
69
- "removed", "rejected", "refuse", "cannot",
70
- "已不存在", "不存在", "没有", "不要", "禁止", "不再", "无法", "拒绝",
76
+ "removed", "rejected", "refuse", "cannot", "blocked",
77
+ "已不存在", "不存在", "没有", "不要", "禁止", "不再", "无法", "拒绝", "阻断",
71
78
  )
72
79
 
73
80
 
@@ -171,12 +178,53 @@ class CommandTableTest(unittest.TestCase):
171
178
  table = [line for line in _read(path).splitlines() if line.startswith("| ")]
172
179
  referenced = {
173
180
  name for line in table
174
- for name in re.findall(r"`(set[A-Za-z]+|register[A-Za-z]+|plan[A-Za-z]+|deleteForm|run)`", line)
181
+ for name in re.findall(r"`((?:set|add|remove|register|plan)[A-Za-z]+|deleteForm|run)`", line)
175
182
  }
176
183
  with self.subTest(doc=path.name):
177
184
  self.assertTrue(referenced, f"{path}: no command found in the table")
178
185
  unknown = sorted(referenced - published - routed)
179
186
  self.assertEqual(unknown, [], f"{path} advertises unpublished commands")
187
+ self.assertIn("addReportColumns", referenced)
188
+ self.assertIn("removeReportColumn", referenced)
189
+
190
+ def test_unsafe_sort_writer_is_only_documented_as_blocked(self):
191
+ self.assertEqual(self.contracts["commands"]["setReportSorts"]["availability"], "blocked")
192
+ self.assertEqual(self.contracts["designStateEvidence"]["provenSections"], ["reportPlugins"])
193
+ checked = 0
194
+ for path in _report_doc_paths():
195
+ text = _read(path)
196
+ for match in re.finditer(r"`setReportSorts`", text):
197
+ checked += 1
198
+ with self.subTest(doc=str(path)):
199
+ self.assertTrue(_denied_near(text, match.start(), match.end()),
200
+ f"{path} advertises the unsafe sort writer")
201
+ self.assertGreater(checked, 0)
202
+ for relative in ("skills/cosmic-meta-modeling/modules/report/SKILL.md",
203
+ "skills/cosmic-meta-modifier/modules/report-op/SKILL.md",
204
+ "knowledge/cosmic/platform/reportlistdata-plugin.md"):
205
+ for root in (APP_BUILD_ROOT, ZH_ROOT):
206
+ with self.subTest(tree=root.name, doc=relative):
207
+ text = _read(root / relative)
208
+ self.assertIn("defaultSorts", text)
209
+ self.assertIn("defaultSortPolicy", text)
210
+
211
+ def test_column_removal_has_one_strict_payload_and_release_boundary(self):
212
+ removal = json.loads(_read(WRITE_SCHEMA))["$defs"]["columnRemoval"]
213
+ self.assertEqual(removal["required"], ["fieldKey"])
214
+ self.assertFalse(removal["additionalProperties"])
215
+ self.assertEqual(set(removal["properties"]), {"fieldKey"})
216
+ contract = self.contracts["commands"]["removeReportColumn"]
217
+ self.assertTrue(contract["payloadSchemaRef"].endswith("#/$defs/columnRemoval"))
218
+ self.assertIn("ReportColumns.remove", contract["writeSemantics"])
219
+ self.assertIn("REPORT_COLUMN_DELETE_API_UNAVAILABLE", contract["compatibility"])
220
+ self.assertIn("removeReportColumn", self.contracts["columnChangeRoute"]["remove"])
221
+ for root in (APP_BUILD_ROOT, ZH_ROOT):
222
+ for relative in ("skills/cosmic-meta-modifier/modules/report-op/SKILL.md",
223
+ "skills/cosmic-meta-modeling/modules/report/SKILL.md"):
224
+ with self.subTest(tree=root.name, doc=relative):
225
+ text = _read(root / relative)
226
+ self.assertIn("removeReportColumn", text)
227
+ self.assertIn("REPORT_COLUMN_DELETE_API_UNAVAILABLE", text)
180
228
 
181
229
  def test_a_removed_command_is_only_ever_mentioned_to_warn_against_it(self):
182
230
  removed = set(self.contracts.get("removedCommands") or {})
@@ -216,6 +264,7 @@ class CommandTableTest(unittest.TestCase):
216
264
  """
217
265
  message = _block(_read(REPORT_OPS), "_REPORT_COLUMNS_ALREADY_SET = (")
218
266
  self.assertIn("addReportColumns", message)
267
+ self.assertIn("removeReportColumn", message)
219
268
  self.assertIn("planReportRecreate", message)
220
269
  self.assertIn("designer", message)
221
270
  self.assertTrue(_denied_near(message, message.index("designer"),
@@ -336,7 +385,12 @@ class PresentationContractTest(unittest.TestCase):
336
385
 
337
386
 
338
387
  class TreeSemanticsTest(unittest.TestCase):
339
- """Two features are called tree, and neither has an id/pid DataSet convention."""
388
+ """Two features are called tree, and each has its own plugin shape and slot.
389
+
390
+ Collapsible rows are one row set whose DataSet carries rowid / pid / isgroupnode, as the
391
+ standard product's tree reports build it; an id / pid convention is only ever named to warn
392
+ against it. Left-tree navigation is a second plugin on the left tree's own slot.
393
+ """
340
394
 
341
395
  def test_no_document_claims_an_id_pid_dataset_convention(self):
342
396
  pattern = re.compile(r"`id`\s*(?:\+|/|and|与|和)\s*`pid`")
@@ -348,11 +402,49 @@ class TreeSemanticsTest(unittest.TestCase):
348
402
  with self.subTest(doc=path.name):
349
403
  self.assertTrue(
350
404
  _denied_near(text, match.start(), match.end()),
351
- f"{path} states an id/pid DataSet convention without denying it; no "
352
- "API page supports one",
405
+ f"{path} states an id/pid DataSet convention without denying it; "
406
+ "collapsible tree rows carry rowid / pid / isgroupnode",
353
407
  )
354
408
  self.assertGreater(checked, 0, "the id/pid scan matched nothing; the pattern is stale")
355
409
 
410
+ def test_collapsible_rows_name_the_row_columns_in_both_trees(self):
411
+ for relative in ("references/plugin-runtime-routing.md",
412
+ "skills/cosmic-meta-knowledge/generation/report-modeling-guide.md",
413
+ "knowledge/cosmic/platform/reportlistdata-plugin.md"):
414
+ for root in (APP_BUILD_ROOT, ZH_ROOT):
415
+ text = _read(root / relative)
416
+ for column in ("`rowid`", "`pid`", "`isgroupnode`"):
417
+ with self.subTest(doc=relative, tree=root.name, column=column):
418
+ self.assertIn(column, text)
419
+
420
+ def test_the_left_tree_example_registers_the_tree_plugin_on_the_tree(self):
421
+ """reportPlugins is the report list's data-fetch slot, and no template has a left tree."""
422
+ relative = "skills/cosmic-meta-modeling/modules/report/examples/golden-example-report-tree.md"
423
+ for root in (APP_BUILD_ROOT, ZH_ROOT):
424
+ text = _read(root / relative)
425
+ artifacts = [json.loads(block) for block in re.findall(
426
+ r"```json\s*\n(.*?)\n```", text, re.DOTALL)]
427
+ artifact = next(item for item in artifacts if item.get("reportColumns"))
428
+ with self.subTest(tree=root.name):
429
+ self.assertEqual(artifact["templateNumber"], "bos_rpttpl")
430
+ self.assertEqual(len(artifact["reportPlugins"]), 1)
431
+ self.assertNotIn("tree", artifact.get("reportProperties") or {})
432
+ # The tree plugin is written after the user adds the report tree control.
433
+ self.assertIn("--target tree", text)
434
+ self.assertIn('"ReportPlugin": "kd.kdtest.report.CostAnalysisTreePlugin"', text)
435
+
436
+ def test_no_document_calls_the_grouped_template_a_left_tree(self):
437
+ """bos_grouprpttpl is the grouped report (group panel + group list), not a left tree."""
438
+ pattern = re.compile(r"left-tree template|左树模板")
439
+ for path in _report_doc_paths():
440
+ text = _read(path)
441
+ for match in pattern.finditer(text):
442
+ with self.subTest(doc=path.name):
443
+ self.assertTrue(
444
+ _denied_near(text, match.start(), match.end()),
445
+ f"{path} names a left-tree template; none exists",
446
+ )
447
+
356
448
  def test_routing_uses_registered_report_types(self):
357
449
  routing = _read(APP_BUILD_ROOT / "references/plugin-runtime-routing.md")
358
450
  types = {entry["type"] for entry in json.loads(
@@ -432,7 +524,8 @@ class ReadBackTierTest(unittest.TestCase):
432
524
 
433
525
  def test_contract_documents_every_tier_the_implementation_can_return(self):
434
526
  documented = set(json.loads(_read(WRITE_CONTRACTS))["readBackSource"])
435
- self.assertEqual(documented, {"canonical", "formLayout", "formLayout+dym", "unavailable"})
527
+ self.assertEqual(documented, {"canonical", "formLayout", "formLayout+dym",
528
+ "reportFilterProjection", "unavailable"})
436
529
  source = _read(REPORT_OPS)
437
530
  for tier in documented:
438
531
  with self.subTest(tier=tier):
@@ -454,8 +547,7 @@ class PipelineStepCountTest(unittest.TestCase):
454
547
  steps = set(re.findall(r"Step (\d)/(\d): ", cli))
455
548
  self.assertTrue(steps)
456
549
  totals = {total for _index, total in steps}
457
- # 7 = the six create-time writes plus the report property section, which is applied
458
- # last so it settles ShowFilterFields after the filter write.
550
+ # 七步包含默认排序契约保留;属性仍最后结算过滤写入后的 ShowFilterFields。
459
551
  self.assertEqual(totals, {"7"}, "the report pipeline no longer has exactly 7 steps")
460
552
  skill = _read(APP_BUILD_ROOT / "skills/cosmic-meta-modeling/modules/report/SKILL.md")
461
553
  ordered = re.search(r"`run` executes the fixed platform dependency order internally:(.+?)\n\n",
@@ -35,6 +35,7 @@ import render_output_cards as cards
35
35
 
36
36
  REPORT = "kdtest_ljbb"
37
37
  FQCN = "kd.test.guaner.report.LjbbReportPlugin"
38
+ TREE_FQCN = "kd.test.guaner.report.LjbbTreePlugin"
38
39
 
39
40
 
40
41
  def _write(path: Path, payload) -> None:
@@ -46,7 +47,7 @@ class _Root:
46
47
  """An appRoot carrying the defect's exact shape: report archived, plugin registered."""
47
48
 
48
49
  def __init__(self, stack, *, javas=None, contract=True, plugin_classes=(FQCN,),
49
- source_on_disk=False):
50
+ source_on_disk=False, plugin_types=None):
50
51
  self.path = Path(stack.enter_context(TemporaryDirectory()))
51
52
  self.archive = {"formNumbers": [{"name": "lj report", "number": REPORT}]}
52
53
  if javas is not None:
@@ -59,18 +60,18 @@ class _Root:
59
60
  self.source = str(source)
60
61
  _write(self.path / "archive.json", self.archive)
61
62
  if contract:
62
- _write(
63
- self.path / "_runtime" / f"{REPORT}.report-plugin-contract.json",
64
- {
65
- "schemaVersion": "app-build-report-plugin-contract/v1",
66
- "formNumber": REPORT,
67
- "pluginClasses": list(plugin_classes),
68
- "dataSetColumns": ["billno", "createtime", "creator_phone"],
69
- "dataSetColumnTypes": [{"dateIndex": "billno", "javaType": "String"}],
70
- "columnPresentation": [],
71
- "filterEnforcement": [],
72
- },
73
- )
63
+ payload = {
64
+ "schemaVersion": "app-build-report-plugin-contract/v1",
65
+ "formNumber": REPORT,
66
+ "pluginClasses": list(plugin_classes),
67
+ "dataSetColumns": ["billno", "createtime", "creator_phone"],
68
+ "dataSetColumnTypes": [{"dateIndex": "billno", "javaType": "String"}],
69
+ "columnPresentation": [],
70
+ "filterEnforcement": [],
71
+ }
72
+ if plugin_types is not None:
73
+ payload["pluginTypes"] = plugin_types
74
+ _write(self.path / "_runtime" / f"{REPORT}.report-plugin-contract.json", payload)
74
75
 
75
76
  def undelivered(self):
76
77
  return cards.undelivered_report_plugins(str(self.path), [REPORT], self.archive)
@@ -130,6 +131,22 @@ class GateTest(unittest.TestCase):
130
131
  self.assertEqual(
131
132
  cards.undelivered_report_plugins(str(root.path), ["kdtest_other"], root.archive), [])
132
133
 
134
+ def test_each_missing_class_names_the_type_its_slot_runs(self):
135
+ """A left-tree report registers two classes on two slots with two base classes.
136
+
137
+ The refusal used to send every missing class to type reportlist, which for the tree class
138
+ is the wrong base class and the wrong query signature.
139
+ """
140
+ missing = _Root(self.stack, plugin_classes=(FQCN, TREE_FQCN), plugin_types={
141
+ FQCN: "reportlist", TREE_FQCN: "reporttreelist"}).undelivered()
142
+ self.assertEqual({item["className"]: item["type"] for item in missing},
143
+ {FQCN: "reportlist", TREE_FQCN: "reporttreelist"})
144
+
145
+ def test_a_contract_without_plugin_types_means_data_fetch_plugins(self):
146
+ """Contracts written before pluginTypes existed registered reportPlugins entries only."""
147
+ missing = _Root(self.stack).undelivered()
148
+ self.assertEqual(missing[0]["type"], "reportlist")
149
+
133
150
 
134
151
  class RenderRefusesTest(unittest.TestCase):
135
152
  """The gate has to stop the cards, not just be available to callers."""
@@ -148,6 +165,17 @@ class RenderRefusesTest(unittest.TestCase):
148
165
  self.assertIn("java-plugin-delivery", message)
149
166
  self.assertNotIn(":::render:", message)
150
167
 
168
+ def test_the_refusal_names_the_delivery_type_of_each_class(self):
169
+ root = _Root(self.stack, plugin_classes=(FQCN, TREE_FQCN), plugin_types={
170
+ FQCN: "reportlist", TREE_FQCN: "reporttreelist"})
171
+ with self.assertRaises(cards.ReportPluginSourceMissing) as caught:
172
+ cards.render(str(root.path))
173
+ lines = str(caught.exception).splitlines()
174
+ tree_line = next(line for line in lines if TREE_FQCN in line)
175
+ self.assertIn("as type reporttreelist", tree_line)
176
+ list_line = next(line for line in lines if FQCN in line and TREE_FQCN not in line)
177
+ self.assertIn("as type reportlist", list_line)
178
+
151
179
  def test_refusal_happens_before_the_archive_is_rewritten(self):
152
180
  """Failing after writing artifacts back would record a delivery that did not happen."""
153
181
  root = _Root(self.stack)
@@ -196,13 +224,15 @@ class MirrorParityTest(unittest.TestCase):
196
224
  return ast.dump(tree)
197
225
 
198
226
  def test_the_gate_is_identical_in_both_trees(self):
199
- main = SCRIPTS / "render_output_cards.py"
200
- mirror = SCRIPTS.parent / ".locales/zh/scripts/render_output_cards.py"
227
+ app_root = SCRIPTS.parents[2] if SCRIPTS.parent.parent.name == ".locales" else SCRIPTS.parent
228
+ main = app_root / "scripts/render_output_cards.py"
229
+ mirror = app_root / ".locales/zh/scripts/render_output_cards.py"
201
230
  self.assertTrue(mirror.is_file(), f"{mirror} is missing")
202
231
  self.assertEqual(self._skeleton(main), self._skeleton(mirror))
203
232
 
204
233
  def test_the_mirror_exposes_the_same_gate_api(self):
205
- source = (SCRIPTS.parent / ".locales/zh/scripts/render_output_cards.py").read_text(
234
+ app_root = SCRIPTS.parents[2] if SCRIPTS.parent.parent.name == ".locales" else SCRIPTS.parent
235
+ source = (app_root / ".locales/zh/scripts/render_output_cards.py").read_text(
206
236
  encoding="utf-8")
207
237
  for name in ("ReportPluginSourceMissing", "undelivered_report_plugins",
208
238
  "_refuse_undelivered", "REPORT_PLUGIN_SOURCE_MISSING"):
@@ -13,8 +13,8 @@ guard below exists because the platform would not report the mistake:
13
13
  persist, so "this source cannot see it" must never be reported as a failed write;
14
14
  - the design-state export omits any value equal to the platform default, so an absent node
15
15
  proves the default rather than a lost write;
16
- - ShowFilterFields is written by setReportFilters too, as a comma-joined string the runtime
17
- cannot parse, so the repair must not read as a no-op just because the key list is unchanged;
16
+ - 旧部署的 setReportFilters 曾写入运行时无法解析的逗号字符串,当前附件已使用 JSON;
17
+ 修复历史 ShowFilterFields 时,不能因 key 列表相同就把格式迁移判为 no-op;
18
18
  - ReportPluginExt is a full replacement whose accessor carries a doubled r, so a merge composed
19
19
  without reading rreportPluginExt would silently drop every plugin already registered.
20
20
  """
@@ -69,7 +69,6 @@ class TargetResolutionTest(unittest.TestCase):
69
69
  "filterPanel": ("reportfilterap", "ReportFilterAp", "ReportFilterPanel"),
70
70
  "moreFilterPanel": ("reportmorefilterpanelap", "ReportMoreFilterPanelAp",
71
71
  "ReportMoreFilterPanel"),
72
- "tree": ("reporttreeap", "ReportTreeAp", "ReportTree"),
73
72
  }
74
73
  for name, (element, host, complex_type) in expected.items():
75
74
  spec = _target(name)
@@ -81,6 +80,17 @@ class TargetResolutionTest(unittest.TestCase):
81
80
  def test_the_form_target_writes_the_root_control_whose_key_is_the_form_number(self) -> None:
82
81
  self.assertEqual(_target("form")["elementKey"], FORM)
83
82
 
83
+ def test_the_tree_target_has_no_fixed_key_because_a_user_names_the_control(self) -> None:
84
+ spec = _target("tree")
85
+ self.assertIsNone(spec["elementKey"])
86
+ self.assertEqual(spec["hostType"], "ReportTreeAp")
87
+ self.assertEqual(spec["complexType"], "ReportTree")
88
+ self.assertEqual(_target("tree", "my_tree", "ReportTreeAp")["elementKey"], "my_tree")
89
+
90
+ def test_a_tree_element_of_another_type_is_refused(self) -> None:
91
+ with self.assertRaisesRegex(ValueError, "writes a ReportTreeAp"):
92
+ _target("tree", "reportlistap", "ReportListAp")
93
+
84
94
  def test_a_column_target_without_an_element_is_refused(self) -> None:
85
95
  with self.assertRaisesRegex(ValueError, "needs --element"):
86
96
  _target("column")
@@ -164,7 +174,7 @@ class AllowListTest(unittest.TestCase):
164
174
  "ReportPluginExt": "registerReportPluginExt",
165
175
  "ReportQueryPlugin": "registerReportPlugin",
166
176
  "ReportColumns": "addReportColumns",
167
- "InitField": "setReportSorts",
177
+ "InitField": "default column visibility, not sorting",
168
178
  }
169
179
  for key, owner in cases.items():
170
180
  with self.assertRaisesRegex(ValueError, owner):
@@ -174,7 +184,7 @@ class AllowListTest(unittest.TestCase):
174
184
  payload = _normalize("filterPanel", {"ShowFilterFields": ["flt_a", "flt_b"]})
175
185
  self.assertEqual(json.loads(payload["ShowFilterFields"]), ["flt_a", "flt_b"])
176
186
 
177
- def test_show_filter_fields_refuses_the_comma_string_the_platform_writes(self) -> None:
187
+ def test_show_filter_fields_refuses_the_legacy_comma_string_as_write_input(self) -> None:
178
188
  with self.assertRaisesRegex(ValueError, "must be a non-empty array"):
179
189
  _normalize("filterPanel", {"ShowFilterFields": "flt_a,flt_b"})
180
190
 
@@ -192,7 +202,7 @@ class ValueComparisonTest(unittest.TestCase):
192
202
  self.assertFalse(report_ops._values_match(report_ops._KIND_BOOL, True, "true"))
193
203
 
194
204
  def test_both_persisted_forms_of_show_filter_fields_read_as_the_same_key_list(self) -> None:
195
- """setReportFilters writes the comma form; this command writes the JSON form."""
205
+ """读回兼容旧逗号值和 JSON;兼容读取不代表当前 writer 仍写逗号。"""
196
206
  self.assertTrue(report_ops._values_match(
197
207
  report_ops._KIND_IDENTIFIER_LIST, '["a","b"]', "a,b"))
198
208
 
@@ -233,7 +243,13 @@ class ReadBackKeyTest(unittest.TestCase):
233
243
  self.assertNotIn("Merge", observed)
234
244
 
235
245
 
236
- def _write_backend(before, after, *, expect_complex_type=None, recorder=None):
246
+ _CONTROLS = [
247
+ {"key": "total_amount", "type": "EntryFieldAp", "category": "control"},
248
+ {"key": "amt_group", "type": "EntryFieldGroupAp", "category": "control"},
249
+ ]
250
+
251
+
252
+ def _write_backend(before, after, *, expect_complex_type=None, recorder=None, controls=()):
237
253
  """A backend that answers the pre-read, the write and the post-read in that order."""
238
254
  state = {"written": False}
239
255
 
@@ -251,10 +267,7 @@ def _write_backend(before, after, *, expect_complex_type=None, recorder=None):
251
267
  state["written"] = True
252
268
  return {"status": True, "errorCode": "0"}
253
269
  if endpoint == "listElements":
254
- return {"status": True, "errorCode": "0", "data": [
255
- {"key": "total_amount", "type": "EntryFieldAp", "category": "control"},
256
- {"key": "amt_group", "type": "EntryFieldGroupAp", "category": "control"},
257
- ]}
270
+ return {"status": True, "errorCode": "0", "data": _CONTROLS + list(controls)}
258
271
  raise AssertionError(f"unexpected endpoint {endpoint}")
259
272
 
260
273
  return call
@@ -299,17 +312,63 @@ class WriteVerdictTest(unittest.TestCase):
299
312
  with self.assertRaisesRegex(RuntimeError, "REPORT_PROPERTY_MISMATCH"):
300
313
  report_ops.set_report_properties({}, "t", FORM, "list", {"ShowSeq": False})
301
314
 
302
- def test_a_host_resolving_none_of_the_keys_is_refused_before_the_write(self) -> None:
303
- """A report with no left tree would otherwise report a successful no-op."""
315
+ def test_a_report_without_a_tree_control_is_refused_before_the_write(self) -> None:
316
+ """No public template presets one; a write would otherwise report a successful no-op."""
304
317
  recorder = []
305
318
  call = _write_backend({}, {}, recorder=recorder)
306
319
  with _platform(call):
307
- with self.assertRaisesRegex(ValueError, "REPORT_PROPERTY_ELEMENT_MISMATCH"):
320
+ with self.assertRaisesRegex(ValueError,
321
+ "REPORT_PROPERTY_ELEMENT_MISMATCH: .* no ReportTreeAp"):
322
+ report_ops.set_report_properties(
323
+ {}, "t", FORM, "tree", {"LeafNodeQuery": False})
324
+ self.assertEqual([endpoint for endpoint, _ in recorder
325
+ if endpoint in ("getProperties", "setComplexProperty")], [])
326
+
327
+ def test_a_host_resolving_none_of_the_keys_is_refused_before_the_write(self) -> None:
328
+ recorder = []
329
+ call = _write_backend({}, {}, recorder=recorder,
330
+ controls=[{"key": "my_tree", "type": "ReportTreeAp"}])
331
+ with _platform(call):
332
+ with self.assertRaisesRegex(ValueError, "REPORT_PROPERTY_ELEMENT_MISMATCH: 'my_tree'"):
308
333
  report_ops.set_report_properties(
309
334
  {}, "t", FORM, "tree", {"LeafNodeQuery": False})
310
335
  self.assertEqual([endpoint for endpoint, _ in recorder if endpoint == "setComplexProperty"],
311
336
  [])
312
337
 
338
+ def test_a_tree_is_found_by_its_type_whatever_key_the_user_gave_it(self) -> None:
339
+ recorder = []
340
+ call = _write_backend({"LeafNodeQuery": True}, {"LeafNodeQuery": False},
341
+ expect_complex_type="ReportTree", recorder=recorder,
342
+ controls=[{"key": "my_tree", "type": "ReportTreeAp"}])
343
+ with _platform(call):
344
+ result = report_ops.set_report_properties(
345
+ {}, "t", FORM, "tree", {"LeafNodeQuery": False})
346
+ self.assertTrue(result["verified"])
347
+ self.assertEqual((result["elementKey"], result["hostType"]), ("my_tree", "ReportTreeAp"))
348
+ self.assertEqual({p["elementKey"] for e, p in recorder if e == "getProperties"},
349
+ {"my_tree"})
350
+ write = next(p for e, p in recorder if e == "setComplexProperty")
351
+ self.assertEqual(write["elementKey"], "my_tree")
352
+
353
+ def test_several_tree_controls_need_element_to_name_one(self) -> None:
354
+ trees = [{"key": "tree_a", "type": "ReportTreeAp"},
355
+ {"key": "tree_b", "type": "ReportTreeAp"}]
356
+ call = _write_backend({"LeafNodeQuery": True}, {"LeafNodeQuery": False}, controls=trees)
357
+ with _platform(call):
358
+ with self.assertRaisesRegex(ValueError, "several ReportTreeAp controls"):
359
+ report_ops.set_report_properties(
360
+ {}, "t", FORM, "tree", {"LeafNodeQuery": False})
361
+ result = report_ops.set_report_properties(
362
+ {}, "t", FORM, "tree", {"LeafNodeQuery": False}, element_key="tree_b")
363
+ self.assertEqual(result["elementKey"], "tree_b")
364
+
365
+ def test_a_tree_element_naming_another_control_is_refused(self) -> None:
366
+ call = _write_backend({}, {}, controls=[{"key": "my_tree", "type": "ReportTreeAp"}])
367
+ with _platform(call):
368
+ with self.assertRaisesRegex(ValueError, "is not a ReportTreeAp"):
369
+ report_ops.set_report_properties(
370
+ {}, "t", FORM, "tree", {"LeafNodeQuery": False}, element_key="total_amount")
371
+
313
372
  def test_a_write_of_only_unobservable_keys_is_not_reported_as_verified(self) -> None:
314
373
  """An empty read-back is never a passing one."""
315
374
  call = _write_backend({}, {})
@@ -333,7 +392,7 @@ class WriteVerdictTest(unittest.TestCase):
333
392
  write = next(p for e, p in recorder if e == "setComplexProperty")
334
393
  self.assertEqual(write["elementKey"], "total_amount")
335
394
 
336
- def test_only_a_column_write_carries_an_element_key(self) -> None:
395
+ def test_a_fixed_host_write_carries_no_element_key(self) -> None:
337
396
  recorder = []
338
397
  call = _write_backend({"ShowSeq": True}, {"ShowSeq": False}, recorder=recorder)
339
398
  with _platform(call):