@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
@@ -15,6 +15,7 @@ from pathlib import Path
15
15
  from typing import Any
16
16
 
17
17
  from appconfig_load import inspect_config
18
+ from dcs_code_gate import dcs_rule_issues, dcs_rule_warnings
18
19
  from java_source import commentless, duplicate_methods, method_bodies, method_signatures, stripped
19
20
  from plugin_source_gate import write_state
20
21
  from platform_api_contract import (
@@ -22,8 +23,13 @@ from platform_api_contract import (
22
23
  filter_operator_issues,
23
24
  forbidden_member_issues,
24
25
  override_signature_issues,
26
+ operation_call_findings,
27
+ report_batch_issues,
28
+ report_listener_issues,
29
+ report_source_cast_warnings,
25
30
  return_type_issues,
26
31
  scene_misuse_issues,
32
+ sdk_absent_name_warnings,
27
33
  unverified_import_issues,
28
34
  unverified_member_issues,
29
35
  )
@@ -83,6 +89,76 @@ def time_fields_from_handoff(handoff: dict) -> set[str]:
83
89
  return {key for key, kinds in types.items() if kinds and kinds <= {"TimeProp", "TimeField"}}
84
90
 
85
91
 
92
+ def operation_field_issues(source_text: str, context: dict | None) -> list[dict]:
93
+ """Catch the proven direct-row/explicit-field-list case, not arbitrary Java data flow."""
94
+ if not context or not isinstance(context.get("formGraph"), dict):
95
+ return []
96
+ code = stripped(source_text)
97
+ source = commentless(source_text)
98
+ if not re.search(r"\bextends\s+(?:kd\.bos\.entity\.plugin\.)?AbstractOperationServicePlugIn\b", code):
99
+ return []
100
+ constants = {}
101
+ for match in re.finditer(r'\b(?:static\s+)?final\s+String\s+(\w+)\s*=\s*"([^"\\]*)"\s*;', source):
102
+ if code[match.start():match.start() + 5].strip():
103
+ constants.setdefault(match[1], set()).add(match[2])
104
+
105
+ def value(token):
106
+ token = token.strip()
107
+ if re.fullmatch(r'"[^"\\]*"', token):
108
+ return token[1:-1]
109
+ values = constants.get(token, set())
110
+ return next(iter(values)) if len(values) == 1 else None
111
+
112
+ prepare = [body for body in method_bodies(source_text) if body["name"] == "onPreparePropertys"]
113
+ if len(prepare) != 1 or re.search(r"\bpreparePropertys\s*\(", code):
114
+ return []
115
+ body = source[prepare[0]["start"]:prepare[0]["end"]]
116
+ masked = code[prepare[0]["start"]:prepare[0]["end"]]
117
+ # Helpers, loops and conditional adds need real data-flow analysis. Do not turn an
118
+ # unproven omission into a delivery block; explicit straight-line declarations suffice here.
119
+ if re.search(r"\b(if|for|while|switch|return|try)\b|\baddAll\s*\(", masked):
120
+ return []
121
+ lists = re.findall(r"\b(\w+)\s*=\s*\w+\.getFieldKeys\s*\(\s*\)", masked)
122
+ allowed = {"getFieldKeys", "onPreparePropertys", "add"}
123
+ if not lists or any(name not in allowed for name in re.findall(r"\b(\w+)\s*\(", masked)):
124
+ return []
125
+ declared = set()
126
+ for name in lists:
127
+ for match in re.finditer(r"\b" + re.escape(name) + r"\.add\s*\(([^()]*)\)", body):
128
+ key = value(match[1])
129
+ if key is None:
130
+ return []
131
+ declared.add(key)
132
+ entities = context["formGraph"].get("entities", [])
133
+ entries = {entity.get("collectionProperty"): entity for entity in entities
134
+ if entity.get("role") in ("entries", "subentries") and entity.get("collectionProperty")}
135
+ loaded = set(re.findall(r"\bExtendedDataEntity\s+(\w+)\s*:\s*(?:this\.)?getDataEntities\s*\(\s*\)", code))
136
+ headers = {match[1] for match in re.finditer(r"\bDynamicObject\s+(\w+)\s*=\s*(\w+)\.getDataEntity\s*\(\s*\)", code)
137
+ if match[2] in loaded and len(re.findall(r"\bDynamicObject\s+" + re.escape(match[1]) + r"\b", code)) == 1}
138
+ issues = []
139
+ for collection in re.finditer(r"\bDynamicObjectCollection\s+(\w+)\s*=\s*(\w+)\.getDynamicObjectCollection\s*\(([^()]*)\)", source):
140
+ if not code[collection.start():collection.start() + 23].strip():
141
+ continue
142
+ entity = entries.get(value(collection[3]))
143
+ if collection[2] not in headers or not entity or len(re.findall(r"\bDynamicObjectCollection\s+" + re.escape(collection[1]) + r"\b", code)) != 1:
144
+ continue
145
+ fields = {field["fieldKey"] for field in entity.get("fields", []) if isinstance(field.get("fieldKey"), str)}
146
+ rows = re.findall(r"\bDynamicObject\s+(\w+)\s*(?:=\s*" + re.escape(collection[1]) + r"\.get\s*\(|:\s*" + re.escape(collection[1]) + r"\b)", code)
147
+ for row in rows:
148
+ if len(re.findall(r"\bDynamicObject\s+" + re.escape(row) + r"\b", code)) != 1:
149
+ continue
150
+ for read in re.finditer(r"\b" + re.escape(row) + r"\.get(?:Int|Long|String|BigDecimal|Boolean|Date|DynamicObject)?\s*\(([^()]*)\)", source):
151
+ if not code[read.start():read.start() + len(row)].strip():
152
+ continue
153
+ key = value(read[1])
154
+ path = ".".join([*(entity.get("collectionPath") or [entity["collectionProperty"]]), key or ""])
155
+ if key not in fields or key in declared or path in declared:
156
+ continue
157
+ issues.append({"line": source.count("\n", 0, read.start()) + 1,
158
+ "fieldKey": key, "collectionProperty": entity["collectionProperty"],
159
+ "message": f"Entry field {key} is read from {entity['collectionProperty']} but absent from the explicit onPreparePropertys field list. Add this field key before list/batch operation loading; declaring only the collection does not load its fields."})
160
+ return issues
161
+
86
162
  def operation_dispatch_warnings(source_text: str, operation_keys: list[str]) -> list[dict]:
87
163
  code = stripped(source_text)
88
164
  source = commentless(source_text)
@@ -102,12 +178,91 @@ def operation_dispatch_warnings(source_text: str, operation_keys: list[str]) ->
102
178
  return issues
103
179
 
104
180
 
181
+ _HEAD_FILTER_ON_RE = re.compile(r"\.\s*setFilter\s*\(\s*true\s*\)")
182
+ _HEAD_FILTER_READ_RE = re.compile(r"\b(?:getTableHeadFilterItems|getHeadFilters)\s*\(")
183
+ _REPORT_DATA_TYPES = ("reportlist", "report-plugin")
184
+
185
+
186
+ def _enables_head_filter(source_text: str) -> bool:
187
+ code = stripped(source_text)
188
+ return any(body["name"] == "setSortAndFilter"
189
+ and _HEAD_FILTER_ON_RE.search(code[body["start"]:body["end"]])
190
+ for body in method_bodies(source_text))
191
+
192
+
193
+ def _archived_partners(app_root: Path, form_number: str, types: tuple[str, ...],
194
+ source_path: Path) -> list[tuple[str, str]]:
195
+ """(className, source) of each archived plugin of `types` on the same report form."""
196
+ archive_path = app_root.resolve() / "archive.json"
197
+ try:
198
+ entries = json.loads(archive_path.read_text(encoding="utf-8-sig")).get("javas") or []
199
+ except (OSError, ValueError, AttributeError):
200
+ return []
201
+ partners = []
202
+ for entry in entries:
203
+ if not isinstance(entry, dict) or entry.get("type") not in types:
204
+ continue
205
+ if str(entry.get("formNumber") or "").strip() != form_number.strip():
206
+ continue
207
+ path = Path(str(entry.get("filePath") or ""))
208
+ if not path.is_file() or path.resolve() == source_path.resolve():
209
+ continue
210
+ partners.append((str(entry.get("className") or path.stem),
211
+ path.read_text(encoding="utf-8-sig", errors="replace")))
212
+ return partners
213
+
214
+
215
+ def report_head_filter_warnings(source_text: str, plugin_type: str, form_number: str,
216
+ app_root: Path, source_path: Path) -> list[dict]:
217
+ """Header filtering switched on for a report whose data plugin never applies it.
218
+
219
+ The two halves sit in two plugins of one report: setFilter(true) in the reportform plugin's
220
+ setSortAndFilter only shows the funnel, and the rows change once the data plugin's query()
221
+ adds the header conditions. Each side is checked against the other's archived source, so
222
+ whichever of the two is compiled second reports the gap; a side not archived yet is not judged.
223
+ """
224
+ if plugin_type == "reportform" and _enables_head_filter(source_text):
225
+ partners = _archived_partners(app_root, form_number, _REPORT_DATA_TYPES, source_path)
226
+ if not partners or any(_HEAD_FILTER_READ_RE.search(stripped(text)) for _, text in partners):
227
+ return []
228
+ switched_on, data_plugins = "this plugin", [name for name, _ in partners]
229
+ elif plugin_type in _REPORT_DATA_TYPES and not _HEAD_FILTER_READ_RE.search(stripped(source_text)):
230
+ enabling = [name for name, text in _archived_partners(
231
+ app_root, form_number, ("reportform",), source_path) if _enables_head_filter(text)]
232
+ if not enabling:
233
+ return []
234
+ switched_on, data_plugins = ", ".join(enabling), ["this plugin"]
235
+ else:
236
+ return []
237
+ return [{
238
+ "severity": "warning",
239
+ "errorCode": "PLUGIN_REPORT_HEAD_FILTER_UNAPPLIED",
240
+ "formNumber": form_number,
241
+ "message": (
242
+ f"{switched_on} switches header filtering on with setFilter(true) in setSortAndFilter, "
243
+ f"but {', '.join(data_plugins)} never reads getTableHeadFilterItems() or "
244
+ "getHeadFilters(): the funnel shows and the rows do not change. Add the header "
245
+ "conditions in query(), as scenes/report/report-head-filter.md does"
246
+ ),
247
+ }]
248
+
249
+
105
250
  def compile_handoff(filename: str) -> dict:
251
+ from query_plugin_handoff import QueryError, validate_input_files
106
252
  prepared = json.loads(Path(filename).read_text(encoding="utf-8-sig"))
253
+ try:
254
+ validate_input_files(prepared)
255
+ except QueryError as exc:
256
+ raise CompileError(str(exc), "PLUGIN_HANDOFF_STALE") from exc
107
257
  context = prepared.get("handoff") if prepared.get("status") is True else None
108
258
  if not isinstance(context, dict) or not all(context.get(key) for key in
109
- ("projectRoot", "appRoot", "sourcePath", "className", "type", "formNumber", "description")):
259
+ ("projectRoot", "appRoot", "sourcePath", "className", "type", "description")):
110
260
  raise CompileError("handoff must contain successfully prepared coding context", "PLUGIN_SOURCE_INVALID")
261
+ source = context.get("sourceContext")
262
+ if not context.get("formNumber") and not (isinstance(source, dict) and source.get("scope") == "source-only"
263
+ and source.get("sourcePath") == context.get("sourcePath") and context.get("ownershipMode") == "modify"
264
+ and context.get("type") != "operation"):
265
+ raise CompileError("formNumber is required for a bound plugin", "PLUGIN_SOURCE_INVALID")
111
266
  return context
112
267
  # Scanned over the whole file, comments included: these three are markers of an unresolved
113
268
  # dependency, so one sitting in a comment is exactly the case to block.
@@ -124,10 +279,11 @@ CODE_BLOCKERS = (
124
279
 
125
280
 
126
281
  class CompileError(ValueError):
127
- def __init__(self, message: str, code: str, phase: str = "preflight") -> None:
282
+ def __init__(self, message: str, code: str, phase: str = "preflight", *, details: dict | None = None) -> None:
128
283
  super().__init__(message)
129
284
  self.code = code
130
285
  self.phase = phase
286
+ self.details = details or {}
131
287
 
132
288
 
133
289
  def _settings_file(gradle_root: Path) -> Path:
@@ -160,15 +316,22 @@ def resolve_module(gradle_root: Path, project_root: Path) -> str:
160
316
  return matches[0]
161
317
 
162
318
 
163
- def inspect_compile_environment(project_root: str) -> dict:
164
- config = inspect_config(write_derived=False)
319
+ def inspect_compile_environment(project_root: str, config: dict | None = None) -> dict:
320
+ config = inspect_config(write_derived=False) if config is None else config
321
+ facts = {key: config[key] for key in ("configPath", "compile", "autoregister", "missing", "invalid") if key in config}
322
+ facts["configCommands"] = {
323
+ "status": [sys.executable, str(Path(__file__).with_name("appconfig_load.py")), "--status"],
324
+ "help": [sys.executable, str(Path(__file__).with_name("appconfig_load.py")), "--help"],
325
+ }
165
326
  if config.get("status") != "ready":
166
- return {"status": config.get("status", "unknown"), "message": config.get("message", "Compile environment unavailable")}
327
+ return {**facts, **{key: config[key] for key in ("GRADLE_ROOT", "JAVA_HOME") if key in config},
328
+ "status": config.get("status", "unknown"), "message": config.get("message", "Compile environment unavailable")}
167
329
  root = Path(str(config["GRADLE_ROOT"])).resolve()
168
330
  try:
169
331
  module = resolve_module(root, Path(project_root))
170
332
  except CompileError as error:
171
- return {"status": "unbound", "code": error.code, "message": str(error)}
333
+ return {**facts, "status": "unbound", "code": error.code, "message": str(error),
334
+ "gradleRoot": str(root), "projectRoot": str(Path(project_root).resolve())}
172
335
  sdk_paths = []
173
336
  properties = root / "gradle.properties"
174
337
  scheme = root / "config.gradle"
@@ -190,7 +353,7 @@ def inspect_compile_environment(project_root: str) -> dict:
190
353
  continue
191
354
  if directory.is_dir() and any(directory.glob("*.jar")):
192
355
  sdk_paths.append({"path": str(directory.resolve()), "source": [str(properties), str(scheme)], "property": match[1]})
193
- return {"status": "ready", "gradleRoot": str(root), "module": module,
356
+ return {**facts, "status": "ready", "gradleRoot": str(root), "module": module,
194
357
  "wrapperPath": str(root / ("gradlew.bat" if os.name == "nt" else "gradlew")),
195
358
  "javaHome": config["JAVA_HOME"], "sdkDirectories": sdk_paths,
196
359
  "sdkPathEvidence": "Matched one target Gradle property and config.gradle library expression; ambiguous or unavailable configurations remain unresolved, and Gradle was not executed"}
@@ -233,6 +396,18 @@ def validate_source(source_path: Path, project_root: Path, class_name: str) -> l
233
396
  return issues
234
397
 
235
398
 
399
+ def sample_diagnostics(errors: list[dict], limit: int) -> list[dict]:
400
+ """Show each file before repeating one file; retain the full log for overflow."""
401
+ selected = []
402
+ seen = set()
403
+ for index, error in enumerate(errors):
404
+ if error["file"] not in seen and len(selected) < limit:
405
+ selected.append(index)
406
+ seen.add(error["file"])
407
+ selected += [index for index in range(len(errors)) if index not in selected][:max(0, limit - len(selected))]
408
+ return [errors[index] for index in selected]
409
+
410
+
236
411
  def collect_compiler_errors(
237
412
  output: str, target: str | None = None,
238
413
  ) -> tuple[list[dict[str, Any]], list[dict[str, Any]], int]:
@@ -242,14 +417,12 @@ def collect_compiler_errors(
242
417
  compile produced diagnostics for a class this run never touched — and once thirty of those were
243
418
  collected, the real ones were dropped. `inTargetSource` tells the two apart.
244
419
 
245
- Only the unrelated errors are capped during collection, and scanning continues past them so a
246
- target error appearing afterwards is still found. The drop count is returned so a caller can
247
- report how many errors exist rather than how many it chose to show.
420
+ Scan the entire output before sampling other files, so later target/helper errors survive
421
+ earlier noise. The drop count reports all errors, not only the displayed sample.
248
422
  """
249
423
  lines = output.splitlines()
250
424
  mine: list[dict[str, Any]] = []
251
425
  others: list[dict[str, Any]] = []
252
- dropped = 0
253
426
  index = 0
254
427
  while index < len(lines):
255
428
  match = JAVA_ERROR_RE.match(lines[index])
@@ -260,30 +433,28 @@ def collect_compiler_errors(
260
433
  index += 1
261
434
  while index < len(lines) and not JAVA_ERROR_RE.match(lines[index]):
262
435
  stripped = lines[index].strip()
263
- if stripped.startswith(("符号:", "位置:", "symbol:", "location:")):
264
- detail.append(stripped)
265
- if stripped.startswith(("> Task", "FAILURE:", "BUILD FAILED")):
436
+ if stripped.startswith(("> Task", "FAILURE:", "BUILD FAILED", "BUILD SUCCESSFUL", "* What went wrong:", "* Try:")) or re.fullmatch(r"\d+ (?:errors?|warnings?|个错误|个警告)", stripped):
266
437
  break
438
+ if stripped:
439
+ detail.append(stripped)
267
440
  index += 1
268
441
  error = {
269
442
  "file": match.group("file"),
270
443
  "line": int(match.group("line")),
271
444
  "message": match.group("message").strip(),
272
- "detail": detail,
445
+ "detail": detail[:40],
446
+ **({"detailTruncated": True} if len(detail) > 40 else {}),
273
447
  }
274
448
  if target is None:
275
449
  others.append(error)
276
- if len(others) == DIAGNOSTIC_LIMIT:
277
- break
278
450
  continue
279
451
  error["inTargetSource"] = str(Path(error["file"]).resolve()) == target
280
452
  if error["inTargetSource"]:
281
453
  mine.append(error)
282
- elif len(others) < DIAGNOSTIC_LIMIT:
283
- others.append(error)
284
454
  else:
285
- dropped += 1
286
- return mine, others, dropped
455
+ others.append(error)
456
+ dropped = max(0, len(others) - DIAGNOSTIC_LIMIT)
457
+ return mine, sample_diagnostics(others, DIAGNOSTIC_LIMIT), dropped
287
458
 
288
459
 
289
460
  def parse_compiler_errors(output: str, target_source: Path | None = None) -> list[dict[str, Any]]:
@@ -300,7 +471,7 @@ def compile_source(
300
471
  project_root: Path, source_path: Path, class_name: str, timeout_sec: int,
301
472
  *, app_root: Path, plugin_type: str, form_number: str,
302
473
  operation_keys: list[str], description: str, module_runs: dict | None = None,
303
- time_fields: set[str] | None = None,
474
+ time_fields: set[str] | None = None, field_context: dict | None = None, preflight_only: bool = False,
304
475
  ) -> dict[str, Any]:
305
476
  # Before the source is read: this used to sit just before the Gradle call, so a description one
306
477
  # character too long was reported only after all the source gates had run.
@@ -318,13 +489,14 @@ def compile_source(
318
489
  }
319
490
  source_hash = hashlib.sha256(source_path.read_bytes()).hexdigest()
320
491
  source_text = source_path.read_text(encoding="utf-8-sig")
321
- archive_path = app_root.resolve() / "archive.json"
322
- if archive_path.is_file():
323
- previous = json.loads(archive_path.read_text(encoding="utf-8-sig"))
324
- for entry in previous.get("javas", []):
325
- if (str(entry.get("className", "")).rsplit(".", 1)[-1] == class_name
326
- and Path(str(entry.get("filePath") or "")).resolve() == source_path.resolve()):
327
- write_archive(archive_path, javas=[{**entry, "compileStatus": "pending"}])
492
+ if not preflight_only:
493
+ archive_path = app_root.resolve() / "archive.json"
494
+ if archive_path.is_file():
495
+ previous = json.loads(archive_path.read_text(encoding="utf-8-sig"))
496
+ for entry in previous.get("javas", []):
497
+ if (str(entry.get("className", "")).rsplit(".", 1)[-1] == class_name
498
+ and Path(str(entry.get("filePath") or "")).resolve() == source_path.resolve()):
499
+ write_archive(archive_path, javas=[{**entry, "compileStatus": "pending"}])
328
500
  # Independent source checks share one response so a repair can address them together.
329
501
  checks = [
330
502
  ("PLUGIN_SOURCE_DUPLICATE_METHOD", duplicate_methods),
@@ -336,12 +508,22 @@ def compile_source(
336
508
  ("PLUGIN_SOURCE_UNVERIFIED_API", unverified_import_issues),
337
509
  ("PLUGIN_SOURCE_UNVERIFIED_API", unverified_member_issues),
338
510
  ("PLUGIN_SOURCE_LIFECYCLE_MISUSE", scene_misuse_issues),
511
+ ("PLUGIN_SOURCE_LISTENER_UNREGISTERED", report_listener_issues),
512
+ ("PLUGIN_SOURCE_BATCH_NOT_NARROWED", report_batch_issues),
513
+ # The rules DCS rejects a deployment for, caught here so the package passes it first time.
514
+ ("PLUGIN_SOURCE_DCS_RULE_VIOLATION", lambda text: dcs_rule_issues(text, source_path.resolve())),
339
515
  ]
340
- findings = [{**issue, "errorCode": code} for code, check in checks for issue in check(source_text)]
341
- warnings = [{**issue, "errorCode": "PLUGIN_SOURCE_FIELD_ACCESSOR_MISMATCH"}
516
+ operation_issues, operation_warnings = operation_call_findings(source_text, (form_number,))
517
+ findings = operation_issues + [{**issue, "errorCode": code} for code, check in checks for issue in check(source_text)]
518
+ warnings = operation_warnings + [{**issue, "errorCode": "PLUGIN_SOURCE_FIELD_ACCESSOR_MISMATCH"}
342
519
  for issue in time_accessor_issues(source_text, time_fields or set())]
520
+ warnings += sdk_absent_name_warnings(source_text) + report_source_cast_warnings(source_text)
521
+ warnings += report_head_filter_warnings(source_text, plugin_type, form_number, app_root, source_path)
522
+ warnings += dcs_rule_warnings(source_text, source_path.resolve())
343
523
  if plugin_type == "operation":
344
524
  warnings += operation_dispatch_warnings(source_text, operation_keys)
525
+ findings += [{**issue, "errorCode": "PLUGIN_SOURCE_OPERATION_FIELD_NOT_PREPARED"}
526
+ for issue in operation_field_issues(source_text, field_context)]
345
527
  if findings:
346
528
  return {
347
529
  "status": False, "errorCode": findings[0]["errorCode"], "phase": "source_validation",
@@ -350,6 +532,8 @@ def compile_source(
350
532
  "issues": findings,
351
533
  "sourceWarnings": warnings,
352
534
  }
535
+ if preflight_only:
536
+ return {"status": True, "phase": "source_validation", "sourceWarnings": warnings}
353
537
  build_context = None
354
538
  def close_out(module: str, elapsed: float | None, compile_skipped: bool) -> dict[str, Any]:
355
539
  archive_path = app_root.resolve() / "archive.json"
@@ -432,12 +616,20 @@ def compile_source(
432
616
  config = inspect_config(write_derived=False)
433
617
  config_status = config.get("status")
434
618
  if config_status == "disabled":
619
+ unverified_overloads = [item for item in operation_warnings
620
+ if item["errorCode"] == "PLUGIN_SOURCE_OPERATION_OVERLOAD_UNVERIFIED"]
621
+ if unverified_overloads:
622
+ return {"status": False, "errorCode": unverified_overloads[0]["errorCode"],
623
+ "errorCodes": [unverified_overloads[0]["errorCode"]], "phase": "source_validation",
624
+ "message": "操作重载未被已收录签名覆盖,且编译已关闭;改用已验证签名或绑定目标 SDK 并启用编译后重试,不能标记源码就绪。",
625
+ "issues": unverified_overloads, "sourceWarnings": warnings}
435
626
  # appconfig says compilation is not needed here. Treating that as a hard failure
436
627
  # left the archive entry and the source_ready gate permanently unreachable, which
437
628
  # also blocked register_plugins.py — it only reads archived class entries.
438
629
  return close_out(module=":", elapsed=None, compile_skipped=True)
439
630
  if config_status != "ready":
440
- raise CompileError(str(config.get("message") or "compile config is not ready"), "PLUGIN_COMPILE_CONFIG_UNAVAILABLE")
631
+ raise CompileError(str(config.get("message") or "compile config is not ready"), "PLUGIN_COMPILE_CONFIG_UNAVAILABLE",
632
+ details={"buildEnvironment": inspect_compile_environment(str(project_root), config)})
441
633
  gradle_root = Path(str(config["GRADLE_ROOT"]))
442
634
  module = resolve_module(gradle_root, project_root)
443
635
  wrapper = gradle_root / ("gradlew.bat" if os.name == "nt" else "gradlew")
@@ -445,6 +637,7 @@ def compile_source(
445
637
  build_context = {"gradleRoot": str(gradle_root.resolve()), "javaProjectRoot": str(project_root.resolve()),
446
638
  "wrapperPath": str(wrapper.resolve()), "javaHome": str(Path(config["JAVA_HOME"]).resolve()),
447
639
  "compileTask": task, "testTaskCandidate": f"{module}:test" if module else "test",
640
+ "testCommand": [sys.executable, str(Path(__file__).with_name("run_java_tests.py")), "--project-root", str(project_root.resolve())],
448
641
  "testTaskEvidence": "Java task convention only; task availability and test results are not verified by this compile receipt"}
449
642
  env = os.environ.copy()
450
643
  env["JAVA_HOME"] = str(config["JAVA_HOME"])
@@ -491,8 +684,7 @@ def compile_source(
491
684
  log_path.write_text(completed.stdout, encoding="utf-8")
492
685
  mine, others, dropped = collect_compiler_errors(
493
686
  completed.stdout, str(source_path.resolve()))
494
- diagnostics = mine[:5]
495
- diagnostics += others[: max(0, 5 - len(diagnostics))]
687
+ diagnostics = sample_diagnostics([*mine, *others], 5)
496
688
  # Counted before truncation: reporting the shown subset understated a class with more than
497
689
  # DIAGNOSTIC_LIMIT errors, and dropped the "plus N elsewhere" clause exactly when the module
498
690
  # carried the most unrelated noise.
@@ -543,6 +735,7 @@ def main() -> int:
543
735
  args = parser.parse_args()
544
736
  try:
545
737
  time_fields = None
738
+ context = None
546
739
  if args.handoff:
547
740
  if any([args.project_root, args.app_root, args.sources_file, args.source_path, args.class_name,
548
741
  args.plugin_type, args.form_number, args.description, args.operation_key]):
@@ -570,7 +763,6 @@ def main() -> int:
570
763
  raise CompileError("Batch requires nonempty source records", "PLUGIN_BATCH_INPUT_INVALID")
571
764
  module_runs = {}
572
765
  results = []
573
- reported_logs = set()
574
766
  for item in entries:
575
767
  context = item
576
768
  try:
@@ -584,24 +776,40 @@ def main() -> int:
584
776
  app_root=Path(args.app_root), plugin_type=context["type"], form_number=context["formNumber"],
585
777
  operation_keys=context.get("operationKeys", []), description=context["description"], module_runs=module_runs,
586
778
  time_fields=time_fields_from_handoff(context) if item.get("handoffPath") else None,
779
+ field_context=context if item.get("handoffPath") else None, preflight_only=True,
587
780
  )
588
781
  except CompileError as exc:
589
- outcome = {"status": False, "errorCode": exc.code, "phase": exc.phase, "message": str(exc)}
590
- if outcome.get("logPath") in reported_logs:
591
- outcome.pop("diagnostics", None)
592
- if outcome.get("logPath"):
593
- reported_logs.add(outcome["logPath"])
782
+ outcome = {"status": False, "errorCode": exc.code, "phase": exc.phase, "message": str(exc), **exc.details}
594
783
  results.append({"className": context.get("className"), **outcome})
784
+ if all(item.get("status") is True for item in results):
785
+ results = []
786
+ for item in entries:
787
+ context = compile_handoff(item["handoffPath"]) if item.get("handoffPath") else item
788
+ try:
789
+ outcome = compile_source(
790
+ Path(args.project_root), Path(context["sourcePath"]), context["className"], max(1, args.timeout_sec),
791
+ app_root=Path(args.app_root), plugin_type=context["type"], form_number=context["formNumber"],
792
+ operation_keys=context.get("operationKeys", []), description=context["description"], module_runs=module_runs,
793
+ time_fields=time_fields_from_handoff(context) if item.get("handoffPath") else None,
794
+ field_context=context if item.get("handoffPath") else None,
795
+ )
796
+ except CompileError as exc:
797
+ outcome = {"status": False, "errorCode": exc.code, "phase": exc.phase, "message": str(exc), **exc.details}
798
+ results.append({"className": context.get("className"), **outcome})
799
+ else:
800
+ results = [{**item, "status": False, "errorCode": "PLUGIN_BATCH_PREFLIGHT_BLOCKED",
801
+ "message": "Another source has preflight errors; no source was compiled or archived"}
802
+ if item.get("status") is True else item for item in results]
595
803
  result = {"status": all(item.get("status") is True for item in results), "results": results}
596
804
  else:
597
- if not all([args.source_path, args.class_name, args.plugin_type, args.form_number, args.description]):
805
+ if not all([args.source_path, args.class_name, args.plugin_type, args.description]) or (not args.form_number and not context):
598
806
  raise CompileError("Supply single-source arguments or --sources-file", "PLUGIN_SOURCE_INVALID")
599
807
  result = compile_source(
600
808
  Path(args.project_root), Path(args.source_path), args.class_name,
601
809
  max(1, args.timeout_sec),
602
810
  app_root=Path(args.app_root), plugin_type=args.plugin_type,
603
811
  form_number=args.form_number, operation_keys=args.operation_key,
604
- description=args.description, time_fields=time_fields,
812
+ description=args.description, time_fields=time_fields, field_context=context,
605
813
  )
606
814
  print(json.dumps(result, ensure_ascii=False))
607
815
  return 0 if result.get("status") is True else 2
@@ -611,6 +819,7 @@ def main() -> int:
611
819
  "errorCode": exc.code,
612
820
  "phase": exc.phase,
613
821
  "message": str(exc),
822
+ **exc.details,
614
823
  }, ensure_ascii=False))
615
824
  return 2
616
825
  except Exception as exc:
@@ -0,0 +1,112 @@
1
+ #!/usr/bin/env python3
2
+ """Standalone list update plus native card finalization; never replays a write."""
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import json
7
+ import subprocess
8
+ import sys
9
+ import uuid
10
+ from pathlib import Path
11
+
12
+ SCRIPTS = Path(__file__).resolve().parent
13
+ API = SCRIPTS.parent / "skills" / "cosmic-meta-api" / "scripts" / "cosmic_api.py"
14
+
15
+
16
+ def execute(app_root: str | None, project_root: str | None, form_name: str, input_value: str) -> tuple[int, str]:
17
+ root = Path(app_root or project_root).resolve()
18
+ destination = root / "_runtime" / ("list-result-" + uuid.uuid4().hex + ".json")
19
+ destination.parent.mkdir(parents=True, exist_ok=True)
20
+ command = [sys.executable, str(API), "--output", str(destination), "configureList",
21
+ "--app-root" if app_root else "--project-root", str(root), "--form-name", form_name, "--input", input_value]
22
+ try:
23
+ configured = subprocess.run(command, capture_output=True, text=True, encoding="utf-8", errors="replace", timeout=240)
24
+ receipt = json.loads(destination.read_text(encoding="utf-8-sig")) if destination.is_file() else None
25
+ except (OSError, ValueError, subprocess.TimeoutExpired) as error:
26
+ return 3, json.dumps({"status": False, "errorCode": "LIST_RESULT_UNAVAILABLE", "message": str(error),
27
+ "completion": {"nextAction": "reconcile_write_state", "message": "Do not replay a possibly completed write"},
28
+ "resultPath": str(destination)}, ensure_ascii=False)
29
+ if not isinstance(receipt, dict):
30
+ return 3, json.dumps({"status": False, "errorCode": "LIST_RESULT_UNAVAILABLE", "exitCode": configured.returncode,
31
+ "completion": {"nextAction": "reconcile_write_state", "message": "No structured receipt; do not replay the command"},
32
+ "resultPath": str(destination)}, ensure_ascii=False)
33
+ result = {key: receipt[key] for key in ("status", "errorCode", "message", "verification", "mutation", "data", "writeAttempted", "metadataRetained") if key in receipt}
34
+ result["resultPath"] = str(destination)
35
+ if (receipt.get("verification") or {}).get("verified") is True:
36
+ result["verifiedPropertyCount"] = sum(len(column.get("properties") or {}) for column in receipt["verification"].get("columns", []))
37
+ data = dict(result.get("data")) if isinstance(result.get("data"), dict) else {}
38
+ result["data"] = data
39
+ mutation = receipt.get("mutation") or {}
40
+ result["writeAttempted"] = receipt.get("writeAttempted", mutation.get("writeAttempted"))
41
+ outcome = mutation.get("outcome")
42
+ if outcome in {"verified_noop", "failed_preflight"}:
43
+ result["completion"] = {"outcome": outcome, "requiresArtifactFinalization": False,
44
+ "nextAction": "respond_no_change" if outcome == "verified_noop" else (data.get("completion") or {}).get("nextAction", "respond_unmet_request"),
45
+ "message": "End from this receipt. No new or reused card, output_check or source-sync task. Prior card reminders do not apply to this result."}
46
+ if result["completion"]["nextAction"] == "repair_only_reported_input":
47
+ result["completion"]["message"] = "Zero writes. Correct only the reported input using its local recovery; no cards, output_check, source sync or global discovery."
48
+ data["completion"] = result["completion"]
49
+ return (0 if outcome == "verified_noop" else 3), json.dumps(result, ensure_ascii=False)
50
+ if receipt.get("status") is not True or outcome != "verified" or (receipt.get("verification") or {}).get("verified") is not True:
51
+ result["completion"] = {"nextAction": "reconcile_write_state", "message": "No verified completion; do not replay successful fields"}
52
+ result["configurationVerified"] = (receipt.get("verification") or {}).get("verified") is True
53
+ result["status"] = False
54
+ if result.get("errorCode") in (None, "", "0", 0):
55
+ result["errorCode"] = "LIST_DELIVERY_PENDING" if result["configurationVerified"] else "LIST_CONFIGURATION_UNVERIFIED"
56
+ data["completion"] = result["completion"]
57
+ return 3, json.dumps(result, ensure_ascii=False)
58
+ target_root = data.get("appRoot")
59
+ number = data.get("formNumber")
60
+ if not target_root or not number:
61
+ result["completion"] = {"nextAction": "resolve_delivery_target", "message": "Configuration is verified; do not replay it"}
62
+ result["configurationVerified"] = (receipt.get("verification") or {}).get("verified") is True
63
+ result["status"] = False
64
+ if result.get("errorCode") in (None, "", "0", 0):
65
+ result["errorCode"] = "LIST_DELIVERY_PENDING" if result["configurationVerified"] else "LIST_CONFIGURATION_UNVERIFIED"
66
+ data["completion"] = result["completion"]
67
+ return 3, json.dumps(result, ensure_ascii=False)
68
+ delivery = [sys.executable, str(SCRIPTS / "render_output_cards.py"), "--app-root", str(target_root), "--forms-only", "--form-number", str(number)]
69
+ try:
70
+ rendered = subprocess.run(delivery, capture_output=True, text=True, encoding="utf-8", errors="replace", timeout=120)
71
+ except (OSError, subprocess.TimeoutExpired) as error:
72
+ result["completion"] = {"nextAction": "retry_delivery_only", "command": delivery, "message": str(error)}
73
+ result["configurationVerified"] = (receipt.get("verification") or {}).get("verified") is True
74
+ result["status"] = False
75
+ if result.get("errorCode") in (None, "", "0", 0):
76
+ result["errorCode"] = "LIST_DELIVERY_PENDING" if result["configurationVerified"] else "LIST_CONFIGURATION_UNVERIFIED"
77
+ data["completion"] = result["completion"]
78
+ return 3, json.dumps(result, ensure_ascii=False)
79
+ if rendered.returncode or not rendered.stdout.lstrip().startswith(":::render:"):
80
+ result["completion"] = {"nextAction": "retry_delivery_only", "command": delivery,
81
+ "message": "Configuration is verified. Card delivery failed; do not replay the configuration.", "diagnostic": rendered.stderr[-1500:]}
82
+ result["configurationVerified"] = (receipt.get("verification") or {}).get("verified") is True
83
+ result["status"] = False
84
+ if result.get("errorCode") in (None, "", "0", 0):
85
+ result["errorCode"] = "LIST_DELIVERY_PENDING" if result["configurationVerified"] else "LIST_CONFIGURATION_UNVERIFIED"
86
+ data["completion"] = result["completion"]
87
+ return 3, json.dumps(result, ensure_ascii=False)
88
+ result["completion"] = {"outcome": "verified", "requiresArtifactFinalization": False,
89
+ "nextAction": "launch_returned_background_request_then_respond" if "DYM_BACKGROUND_REQUEST=" in rendered.stdout else "respond_with_returned_cards",
90
+ "message": "Preserve exact card blocks. Native render stdout arms the existing host output validation; do not call output_check or rerender. Launch only the returned tracked source-sync request, without waiting."}
91
+ data["completion"] = result["completion"]
92
+ return 0, rendered.stdout.rstrip() + "\nRESULT_SUMMARY=" + json.dumps(result, ensure_ascii=False)
93
+
94
+
95
+ def main() -> int:
96
+ parser = argparse.ArgumentParser(description=__doc__)
97
+ target = parser.add_mutually_exclusive_group(required=True)
98
+ target.add_argument("--app-root")
99
+ target.add_argument("--project-root")
100
+ parser.add_argument("--form-name", required=True)
101
+ parser.add_argument("--input", required=True)
102
+ args = parser.parse_args()
103
+ code, output = execute(args.app_root, args.project_root, args.form_name, args.input)
104
+ print(output, flush=True)
105
+ return code
106
+
107
+
108
+ if __name__ == "__main__":
109
+ for stream in (sys.stdout, sys.stderr):
110
+ if hasattr(stream, "reconfigure"):
111
+ stream.reconfigure(encoding="utf-8", errors="replace")
112
+ raise SystemExit(main())