@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
@@ -30,9 +30,10 @@ OWNERSHIP_ALIASES = {"patch-existing": "modify"}
30
30
 
31
31
 
32
32
  class HandoffError(ValueError):
33
- def __init__(self, message: str, error_code: str = "PLUGIN_HANDOFF_UNRESOLVED") -> None:
33
+ def __init__(self, message: str, error_code: str = "PLUGIN_HANDOFF_UNRESOLVED", *, details: dict | None = None) -> None:
34
34
  super().__init__(message)
35
35
  self.error_code = error_code
36
+ self.details = details or {}
36
37
 
37
38
 
38
39
  def _read_json(path: str | Path) -> Any:
@@ -185,7 +186,8 @@ def _report_column_fields(artifact: dict) -> list[dict]:
185
186
  return result
186
187
 
187
188
 
188
- def _compile_report_contract(artifact: dict, form_number: str) -> dict | None:
189
+ def _compile_report_contract(artifact: dict, form_number: str,
190
+ filter_editors: dict | None = None) -> dict | None:
189
191
  """Compile the report plugin contract straight from the artifact.
190
192
 
191
193
  The report pipeline already writes this file, but a handoff must never depend on it: a
@@ -199,11 +201,33 @@ def _compile_report_contract(artifact: dict, form_number: str) -> dict | None:
199
201
  from _core.report_ops import compile_report_plugin_contract
200
202
  except ImportError:
201
203
  return None
202
- return compile_report_plugin_contract(artifact, form_number)
204
+ return compile_report_plugin_contract(artifact, form_number, filter_editors=filter_editors)
205
+
206
+
207
+ def _run_filter_editors(run_result: Any, form_number: str) -> dict | None:
208
+ """The filter editor outcome the report run recorded for this form, when it recorded one.
209
+
210
+ It says which editor each typed filter actually rendered — a platform without the typed
211
+ report filter patch renders the legacy control — so the plugin is told the value shape it
212
+ will really receive. The artifact alone can only say what was requested.
213
+ """
214
+ candidates = []
215
+ if isinstance(run_result, dict):
216
+ candidates.append(run_result.get("reportFilterEditors"))
217
+ for item in run_result.get("results") or []:
218
+ if isinstance(item, dict) and isinstance(item.get("result"), dict):
219
+ candidates.append(item["result"].get("reportFilterEditors"))
220
+ for editors in candidates:
221
+ if not isinstance(editors, dict):
222
+ continue
223
+ if str(editors.get("formNumber") or "") in ("", str(form_number or "")):
224
+ return editors
225
+ return None
203
226
 
204
227
 
205
228
  def _report_plugin_contract(
206
229
  report_artifacts: list[tuple[str, dict]], form_number: str,
230
+ filter_editors: dict | None = None,
207
231
  ) -> dict | None:
208
232
  """The requirements this report's plugin must implement, or None when it has none.
209
233
 
@@ -214,7 +238,7 @@ def _report_plugin_contract(
214
238
  was handed a contract missing whatever that generation could not yet express.
215
239
  """
216
240
  for artifact_path, artifact in report_artifacts:
217
- contract = _compile_report_contract(artifact, form_number)
241
+ contract = _compile_report_contract(artifact, form_number, filter_editors)
218
242
  if contract:
219
243
  contract["source"] = f"compiled from {Path(artifact_path).name}"
220
244
  return contract
@@ -222,17 +246,41 @@ def _report_plugin_contract(
222
246
 
223
247
 
224
248
  def _registered_plugin_classes(artifact: dict) -> list[str]:
225
- """Plugin FQCNs the artifact already registered on the platform."""
226
- classes = []
249
+ """Plugin FQCNs the artifact already registered on the platform.
250
+
251
+ Both report plugin slots count: reportPlugins (the report list's data-fetch plugin) and
252
+ reportProperties.tree.ReportPlugin (the left tree's own data plugin). Reading only the first
253
+ made a left-tree report's tree class unpreparable: the list plugin was registered, so a
254
+ className naming the tree class failed with PLUGIN_CLASS_NOT_REGISTERED.
255
+ """
256
+ return [slot["className"] for slot in _registered_plugin_slots(artifact)]
257
+
258
+
259
+ def _registered_plugin_slots(artifact: dict) -> list[dict]:
260
+ """Each registered plugin class with the plugin type its slot runs.
261
+
262
+ The type is what keeps one slot's classes from deciding another type's handoff. A report's
263
+ own reportform plugin sits in neither slot, so matching it against the slot classes either
264
+ refused its className with PLUGIN_CLASS_NOT_REGISTERED or, with no className given, handed
265
+ the coding step the data plugin's class and package.
266
+ """
267
+ slots: list[dict] = []
268
+ seen: set[str] = set()
227
269
  for plugin in artifact.get("reportPlugins") or []:
228
270
  if not isinstance(plugin, dict):
229
271
  continue
230
272
  if str(plugin.get("type") or "Java").strip().lower() != "java":
231
273
  continue
232
274
  class_name = str(plugin.get("className") or "").strip()
233
- if class_name and class_name not in classes:
234
- classes.append(class_name)
235
- return classes
275
+ if class_name and class_name not in seen:
276
+ seen.add(class_name)
277
+ slots.append({"className": class_name, "type": "reportlist"})
278
+ properties = artifact.get("reportProperties")
279
+ tree = properties.get("tree") if isinstance(properties, dict) else None
280
+ tree_plugin = str(tree.get("ReportPlugin") or "").strip() if isinstance(tree, dict) else ""
281
+ if tree_plugin and tree_plugin not in seen:
282
+ slots.append({"className": tree_plugin, "type": "reporttreelist"})
283
+ return slots
236
284
 
237
285
 
238
286
  def _catalog(
@@ -276,8 +324,8 @@ def _catalog(
276
324
  "artifact": str(Path(artifact_path).resolve()),
277
325
  }
278
326
  forms.append(form_row)
279
- for class_name in _registered_plugin_classes(artifact):
280
- registered_plugins.append({**form_row, "className": class_name})
327
+ for slot in _registered_plugin_slots(artifact):
328
+ registered_plugins.append({**form_row, **slot})
281
329
  template_fields = {
282
330
  str(item.get("entityKey") or "").strip(): item.get("fields") or []
283
331
  for item in artifact.get("templateFields") or []
@@ -460,7 +508,7 @@ def _field_java_type(field_type: str) -> str | None:
460
508
  if field_type.startswith("Mul"):
461
509
  return None
462
510
  # Match the exact time-only types: DateTimeField also contains "TimeField" as a suffix.
463
- if field_type in ("TimeProp", "TimeField"):
511
+ if field_type in ("TimeProp", "TimeField", "IntegerProp", "IntegerField"):
464
512
  return "Integer"
465
513
  for marker, java_type in _FIELD_JAVA_TYPES:
466
514
  if marker.lower() in field_type.lower():
@@ -503,9 +551,13 @@ def _field_contract(row: dict) -> dict | None:
503
551
  java_type = _field_java_type(field_type)
504
552
  if java_type:
505
553
  contract["javaType"] = java_type
554
+ if field_type in ("IntegerProp", "IntegerField"):
555
+ contract["valueAccessor"] = f'getInt("{field_key}")'
556
+ contract["note"] = "getInt returns a primitive; inspect the raw value first when empty differs from zero."
506
557
  if field_type in ("TimeProp", "TimeField"):
507
558
  contract["valueAccessor"] = f'getInt("{field_key}")'
508
559
  contract["timeValueUnit"] = "seconds-after-midnight"
560
+ contract["note"] = 'Target 8.0 time-only runtime: a negative raw value (observed empty sentinel -1) is empty/invalid; null is also empty. Zero is valid midnight. Inspect the raw value before getInt coercion, normalize empty values, then apply business-hour or ordering rules.'
509
561
  reference = (bool(row.get("refEntity") or row.get("refIdProperty"))
510
562
  if field_type.endswith("Prop") else _is_reference_field(field_type))
511
563
  if reference and not field_type.startswith("Mul"):
@@ -594,6 +646,11 @@ def _external_scope(catalog: list[dict], selector: str) -> list[dict]:
594
646
  ]
595
647
 
596
648
 
649
+ def _external_contract_scope(catalog: list[dict], entry: dict) -> list[dict]:
650
+ return [row for row in _external_scope(catalog, str(entry.get("entity") or ""))
651
+ if not entry.get("formNumber") or row.get("formNumber") == entry["formNumber"]]
652
+
653
+
597
654
  def _external_field(scope: list[dict], selector: Any) -> tuple[dict, str, dict | None]:
598
655
  """Resolve one exact field key/name, optionally scoped to one nested entity."""
599
656
  if isinstance(selector, dict):
@@ -640,7 +697,7 @@ def _external_field(scope: list[dict], selector: Any) -> tuple[dict, str, dict |
640
697
  )
641
698
 
642
699
 
643
- def _verify_external_contracts(contracts: list, catalog: list[dict]) -> list:
700
+ def _verify_external_contract_entries(contracts: list, catalog: list[dict]) -> list:
644
701
  """Prove every declared external entity and field, or fail closed.
645
702
 
646
703
  Returns the same list with each verified entry enriched by ``verifiedFields``, so the
@@ -648,7 +705,7 @@ def _verify_external_contracts(contracts: list, catalog: list[dict]) -> list:
648
705
  """
649
706
  missing = list(dict.fromkeys(str(entry["entity"]).strip() for entry in contracts
650
707
  if isinstance(entry, dict) and entry.get("entity")
651
- and not _external_scope(catalog, str(entry["entity"]))))
708
+ and not _external_contract_scope(catalog, entry)))
652
709
  if missing:
653
710
  raise HandoffError(
654
711
  "external contract entities/forms are not verified: " + ", ".join(missing)
@@ -664,9 +721,18 @@ def _verify_external_contracts(contracts: list, catalog: list[dict]) -> list:
664
721
  if not entity_selector:
665
722
  verified.append(entry)
666
723
  continue
667
- scope = _external_scope(catalog, entity_selector)
724
+ scope = _external_contract_scope(catalog, entry)
668
725
  selectors = _contract_field_selectors(entry)
669
- resolved = [_external_field(scope, selector) for selector in selectors]
726
+ resolved, errors = [], []
727
+ for selector in selectors:
728
+ try:
729
+ resolved.append(_external_field(scope, selector))
730
+ except HandoffError as exc:
731
+ errors.append((selector, str(exc)))
732
+ if errors:
733
+ failed = json.dumps([selector for selector, _message in errors], ensure_ascii=False)
734
+ raise HandoffError(f"unverified field selectors ({len(errors)}): {failed}; {errors[0][1]}",
735
+ "PLUGIN_EXTERNAL_CONTRACT_UNVERIFIED")
670
736
  entities = {row["entityKey"] for row, _key, _field in resolved}
671
737
  if not resolved and len(scope) != 1:
672
738
  raise HandoffError(
@@ -691,6 +757,24 @@ def _verify_external_contracts(contracts: list, catalog: list[dict]) -> list:
691
757
  return verified
692
758
 
693
759
 
760
+ def _verify_external_contracts(contracts: list, catalog: list[dict]) -> list:
761
+ verified, errors = [], []
762
+ for entry in contracts:
763
+ try:
764
+ verified.extend(_verify_external_contract_entries([entry], catalog))
765
+ except HandoffError as exc:
766
+ selector = str(entry.get("entity") or "") if isinstance(entry, dict) else str(entry)
767
+ errors.append((selector, exc))
768
+ if errors:
769
+ details = [f"{selector}: {str(error)[:700]}"
770
+ + (" [diagnostic text omitted]" if len(str(error)) > 700 else "")
771
+ for selector, error in errors[:8]]
772
+ remaining = f"; {len(errors) - 8} additional failed contracts omitted" if len(errors) > 8 else ""
773
+ raise HandoffError(f"{len(errors)} external contract(s) failed verification: " + "; ".join(details) + remaining,
774
+ errors[0][1].error_code)
775
+ return verified
776
+
777
+
694
778
  def _verified_field(field_key: str, field: dict | None, entity: dict | None = None) -> dict:
695
779
  """One field of an external contract, as the read-back proved it."""
696
780
  if field is None:
@@ -717,6 +801,7 @@ def _verified_field(field_key: str, field: dict | None, entity: dict | None = No
717
801
  if field_type in ("TimeProp", "TimeField"):
718
802
  result["valueAccessor"] = f'getInt("{field_key}")'
719
803
  result["timeValueUnit"] = "seconds-after-midnight"
804
+ result["note"] = 'Target 8.0 time-only runtime: a negative raw value (observed empty sentinel -1) is empty/invalid; null is also empty. Zero is valid midnight. Inspect the raw value before getInt coercion, normalize empty values, then apply business-hour or ordering rules.'
720
805
  return result
721
806
 
722
807
 
@@ -750,6 +835,24 @@ def _verified_operation_keys(value: Any) -> set[str]:
750
835
  return found
751
836
 
752
837
 
838
+ def _validate_operation_result(value: Any, form_number: str, app_root: str, keys: list[str]) -> set[str]:
839
+ if value is None:
840
+ raise HandoffError("type=operation requires a listOperations read-back via --operations-file")
841
+ verified = _verified_operation_keys(value)
842
+ for data in (value, value.get("data") if isinstance(value, dict) else None):
843
+ if not isinstance(data, dict):
844
+ continue
845
+ if data.get("formNumber") and data["formNumber"] != form_number:
846
+ raise HandoffError("listOperations result belongs to another form")
847
+ if data.get("appRoot") and Path(data["appRoot"]).resolve() != Path(app_root).resolve():
848
+ raise HandoffError("listOperations result belongs to another application")
849
+ missing = [key for key in keys if key not in verified]
850
+ if missing:
851
+ raise HandoffError("operationKeys are absent from listOperations read-back: " + ", ".join(missing)
852
+ + "; available operationKeys: " + ", ".join(sorted(verified)))
853
+ return verified
854
+
855
+
753
856
  def operation_contracts(value: Any, keys: list[str]) -> list[dict]:
754
857
  """Preserve actual operation parameters instead of guessing business state transitions."""
755
858
  if isinstance(value, list):
@@ -774,21 +877,54 @@ def read_platform(app_root: str, command: str, selector: str, flag: str = "--for
774
877
  try:
775
878
  completed = subprocess.run([sys.executable, str(COSMIC_API), "--output", str(destination), command,
776
879
  "--app-root", str(Path(app_root).resolve()), flag, selector],
777
- capture_output=True, text=True, encoding="utf-8", errors="replace", timeout=60)
880
+ capture_output=True, text=True, encoding="utf-8", errors="replace", timeout=240)
778
881
  except (OSError, subprocess.TimeoutExpired) as exc:
779
882
  raise HandoffError(f"{command} could not complete for {selector}", "PLUGIN_EVIDENCE_READ_FAILED") from exc
780
883
  if completed.returncode or not destination.is_file():
781
- raise HandoffError(f"{command} failed for {selector}; no fresh evidence saved",
884
+ # 只保留结构化诊断,避免把子进程中的认证配置或完整元数据复制进错误串。
885
+ detail = {}
886
+ response = None
887
+ try:
888
+ response = json.loads(completed.stdout)
889
+ if isinstance(response, dict):
890
+ detail = {key: response[key] for key in ("errorCode", "phase", "retryable") if key in response}
891
+ except (ValueError, TypeError):
892
+ pass
893
+ response = _read_json(destination) if destination.is_file() else response
894
+ _raise_missing_entity(command, selector, response)
895
+ raise HandoffError(f"{command} failed for {selector}; no fresh evidence saved. "
896
+ f"Child exit={completed.returncode}, diagnostics={json.dumps(detail)}. "
897
+ "For unchanged external contracts, pass their exact successful getFormSchema files "
898
+ "with --external-schema; never replace a failed current read with an implicit stale cache.",
782
899
  "PLUGIN_EVIDENCE_READ_FAILED")
783
900
  value = _read_json(destination)
784
901
  if isinstance(value, dict) and (value.get("status") is False
785
902
  or value.get("errorCode") not in (None, "", 0, "0")
786
903
  or isinstance(value.get("data"), dict) and value["data"].get("success") is False):
904
+ _raise_missing_entity(command, selector, value)
787
905
  raise HandoffError(f"{command} failed for {selector}: {value.get('message') or value.get('errorCode')}",
788
906
  "PLUGIN_EVIDENCE_READ_FAILED")
907
+ if command == "getEntityType" and isinstance(value, dict):
908
+ value = {**value, "_pluginEvidence": {"formNumber": selector, "path": str(destination),
909
+ "sha256": hashlib.sha256(destination.read_bytes()).hexdigest()}}
789
910
  return value
790
911
 
791
912
 
913
+ def _raise_missing_entity(command: str, selector: str, response: Any) -> None:
914
+ """Only a structured entity-metadata miss permits the related-form schema fallback."""
915
+ if command != "getEntityType" or not isinstance(response, dict):
916
+ return
917
+ layers = [response, response.get("data")]
918
+ for layer in layers:
919
+ if not isinstance(layer, dict):
920
+ continue
921
+ message = str(layer.get("message") or "").strip()
922
+ if (re.fullmatch(r"实体.+?元数据不存在[。.!!]?", message)
923
+ or layer.get("errorCode") in {"bos.entityMetaDataNotExist", "ENTITY_METADATA_NOT_FOUND", "METADATA_NOT_FOUND"}):
924
+ raise HandoffError(f"getEntityType metadata does not exist for {selector}",
925
+ "PLUGIN_ENTITY_METADATA_NOT_FOUND")
926
+
927
+
792
928
  def prepare_inputs(*, app_root: str, intent: dict, plugin_type: str, artifact_paths: list[str],
793
929
  run_result: Any, operation_result: Any, external_schemas: list[Any]) -> tuple[Any, list[Any]]:
794
930
  """Collect only missing read-only platform evidence for this verified target."""
@@ -814,7 +950,7 @@ def prepare_inputs(*, app_root: str, intent: dict, plugin_type: str, artifact_pa
814
950
  missing = [entry for name in ("externalContracts", "externalApis")
815
951
  for entry in _contract_list(specialized.get(name), name)
816
952
  if isinstance(entry, dict) and entry.get("entity")
817
- and not _external_scope(catalog, str(entry["entity"]))]
953
+ and not _external_contract_scope(catalog, entry)]
818
954
  selectors = list(dict.fromkeys(str(entry.get("formNumber") or entry["entity"]) for entry in missing))
819
955
  invalid = [selector for selector in selectors if not re.fullmatch(r"[A-Za-z0-9_]+", selector)]
820
956
  if invalid:
@@ -894,8 +1030,9 @@ def runtime_catalog(snapshots: list[Any]) -> tuple[list[dict], list[dict], list[
894
1030
  return forms, entities, fields, []
895
1031
 
896
1032
 
897
- def prepare_runtime_inputs(app_root: str, form_number: str, intent: dict, plugin_type: str) -> tuple[list[Any], Any]:
898
- """Read current main/explicitly referenced forms once, independently of modeling artifacts."""
1033
+ def prepare_runtime_inputs(app_root: str, form_number: str, intent: dict, plugin_type: str,
1034
+ *, operation_result: Any = None, external_schemas: list[Any] | None = None) -> tuple[list[Any], Any]:
1035
+ """Read the main graph once; reuse explicit evidence and parallelize independent reads."""
899
1036
  _string_list(intent.get("coreLogic"), "coreLogic", required=True)
900
1037
  keys = _string_list(intent.get("operationKeys"), "operationKeys")
901
1038
  if plugin_type == "operation" and not keys:
@@ -907,36 +1044,60 @@ def prepare_runtime_inputs(app_root: str, form_number: str, intent: dict, plugin
907
1044
  raise HandoffError("declare specialized.unresolvedDependencies before platform reads")
908
1045
  if specialized["unresolvedDependencies"]:
909
1046
  raise HandoffError("resolve declared dependencies before platform reads", "PLUGIN_EXTERNAL_CONTRACT_UNRESOLVED")
910
- main = read_platform(app_root, "getEntityType", form_number, "--entityId")
1047
+ schemas = external_schemas if external_schemas is not None else []
1048
+ external_catalog = _external_entity_catalog(schemas)
1049
+ from concurrent.futures import ThreadPoolExecutor
1050
+ with ThreadPoolExecutor(max_workers=2) as pool:
1051
+ main_read = pool.submit(read_platform, app_root, "getEntityType", form_number, "--entityId")
1052
+ operation_read = (pool.submit(read_platform, app_root, "listOperations", form_number)
1053
+ if plugin_type == "operation" and operation_result is None else None)
1054
+ main = main_read.result()
1055
+ if operation_read is not None:
1056
+ operation_result = operation_read.result()
911
1057
  forms, entities, fields, _ = runtime_catalog([main])
912
1058
  if forms[0]["formNumber"] != form_number:
913
1059
  raise HandoffError("runtime metadata belongs to another form", "PLUGIN_CONTEXT_INCOMPLETE")
914
- archive = Path(app_root) / "archive.json"
915
- identity = _read_json(archive) if archive.is_file() else {}
916
- if forms[0].get("appCode") and identity.get("appCode") and forms[0]["appCode"] != identity["appCode"]:
917
- raise HandoffError("runtime main form belongs to another application", "PLUGIN_CONTEXT_INCOMPLETE")
1060
+ if plugin_type == "operation":
1061
+ _validate_operation_result(operation_result, form_number, app_root, keys)
918
1062
  references = {field["refEntity"] for field in fields if field.get("refEntity")}
919
1063
  requests = []
920
- catalog = _artifact_external_catalog(entities, fields)
1064
+ catalog = _artifact_external_catalog(entities, fields) + external_catalog
921
1065
  for name in ("externalContracts", "externalApis"):
922
1066
  for entry in _contract_list(specialized.get(name), name):
923
- if not isinstance(entry, dict) or not entry.get("entity") or _external_scope(catalog, str(entry["entity"])):
1067
+ if not isinstance(entry, dict) or not entry.get("entity") or _external_contract_scope(catalog, entry):
924
1068
  continue
925
1069
  selector = str(entry.get("formNumber") or entry["entity"])
926
1070
  if not re.fullmatch(r"[A-Za-z0-9_]+", selector):
927
- # Resolve a unique declared reference's display name from its native metadata.
928
- # Fetching declared reference targets is bounded to this form, never a global form tour.
1071
+ # Only the main form's declared references may resolve an exact display name.
929
1072
  requests += sorted(references)
930
1073
  else:
931
1074
  requests.append(selector)
932
- from concurrent.futures import ThreadPoolExecutor
1075
+
1076
+ def related_read(selector: str) -> tuple[Any, Any]:
1077
+ try:
1078
+ native = read_platform(app_root, "getEntityType", selector, "--entityId")
1079
+ except HandoffError as exc:
1080
+ if exc.error_code != "PLUGIN_ENTITY_METADATA_NOT_FOUND":
1081
+ raise
1082
+ schema = read_platform(app_root, "getFormSchema", selector)
1083
+ related_catalog = _external_entity_catalog([schema])
1084
+ if not related_catalog or any(row["formNumber"] != selector for row in related_catalog):
1085
+ raise HandoffError("related schema belongs to another form", "PLUGIN_EXTERNAL_CONTRACT_UNVERIFIED")
1086
+ return None, schema
1087
+ related_forms, _, _, _ = runtime_catalog([native])
1088
+ if related_forms[0]["formNumber"] != selector:
1089
+ raise HandoffError("related runtime metadata belongs to another form", "PLUGIN_CONTEXT_INCOMPLETE")
1090
+ return native, None
1091
+
933
1092
  with ThreadPoolExecutor(max_workers=4) as pool:
934
- related = list(pool.map(lambda selector: read_platform(app_root, "getEntityType", selector, "--entityId"),
935
- dict.fromkeys(requests)))
936
- snapshots = [main] + related
937
- runtime_catalog(snapshots) # Fail before coding when any requested graph is incomplete.
938
- operations = read_platform(app_root, "listOperations", form_number) if plugin_type == "operation" else None
939
- return snapshots, operations
1093
+ related = list(pool.map(related_read, dict.fromkeys(requests)))
1094
+ snapshots = [main] + [native for native, _schema in related if native is not None]
1095
+ schemas.extend(schema for _native, schema in related if schema is not None)
1096
+ _, related_entities, related_fields, _ = runtime_catalog(snapshots)
1097
+ catalog = _artifact_external_catalog(related_entities, related_fields) + _external_entity_catalog(schemas)
1098
+ for name in ("externalContracts", "externalApis"):
1099
+ _verify_external_contracts(_contract_list(specialized.get(name), name), catalog)
1100
+ return snapshots, operation_result
940
1101
 
941
1102
 
942
1103
  def _bind_unique_mentions(
@@ -986,20 +1147,106 @@ def _conventional_class_name(form_number: str, class_suffix: str | None) -> str
986
1147
  return "".join(part[:1].upper() + part[1:] for part in parts) + class_suffix
987
1148
 
988
1149
 
1150
+ def read_ui_context(app_root: str, form_number: str, plugin_type: str, *, include_list: bool = False) -> dict:
1151
+ """Read and validate the selected page without requiring a runtime entity."""
1152
+ import xml.etree.ElementTree as ET
1153
+ response = read_platform(app_root, "getFormMetadata", form_number, "--formId")
1154
+ data = response.get("data") if isinstance(response, dict) else None
1155
+ xml = data if isinstance(data, str) else (data.get("xml") or data.get("metadata") or data.get("formMetadata")) if isinstance(data, dict) else None
1156
+ if not isinstance(xml, str) or not xml.strip():
1157
+ raise HandoffError("page metadata did not contain XML", "PLUGIN_CONTEXT_INCOMPLETE")
1158
+ try:
1159
+ root = ET.fromstring(xml)
1160
+ except ET.ParseError as exc:
1161
+ raise HandoffError("page metadata XML is invalid", "PLUGIN_CONTEXT_INCOMPLETE") from exc
1162
+ from run import _xml_child_text, _xml_localname, parse_form_metadata_xml_document
1163
+ if _xml_localname(root.tag) != "FormMetadata":
1164
+ root = next((item for item in root.iter() if _xml_localname(item.tag) == "FormMetadata"), None)
1165
+ items = next((item for item in root if _xml_localname(item.tag) == "Items"), None) if root is not None else None
1166
+ if root is None or _xml_child_text(root, "Key") != form_number or items is None:
1167
+ raise HandoffError("page metadata does not prove the selected form identity and layout", "PLUGIN_CONTEXT_INCOMPLETE")
1168
+ form_config, control_tree, list_config, list_meta = parse_form_metadata_xml_document(xml)
1169
+ if form_config is None and control_tree is None:
1170
+ raise HandoffError("page layout is absent", "PLUGIN_CONTEXT_INCOMPLETE")
1171
+ if include_list and (not isinstance(list_config, dict) or not isinstance(list_meta, dict)):
1172
+ raise HandoffError("list metadata is absent", "PLUGIN_CONTEXT_INCOMPLETE")
1173
+ controls = []
1174
+ def visit(node):
1175
+ if isinstance(node, dict):
1176
+ if node.get("Key") and node.get("_Type_"):
1177
+ controls.append({"key": node["Key"], "type": node["_Type_"]})
1178
+ for child in node.values():
1179
+ if isinstance(child, (dict, list)):
1180
+ visit(child)
1181
+ elif isinstance(node, list):
1182
+ for child in node:
1183
+ visit(child)
1184
+ visit(control_tree)
1185
+ # A control key is not necessarily a model field. Only expose an explicit
1186
+ # page-field binding from the returned XML; never derive a field from the key.
1187
+ bindings = {}
1188
+ for item in root.iter():
1189
+ key = _xml_child_text(item, "Key")
1190
+ field = _xml_child_text(item, "FieldKey") or _xml_child_text(item, "FieldName")
1191
+ if key and field:
1192
+ bindings.setdefault(key, set()).add(field)
1193
+ for control in controls:
1194
+ matches = bindings.get(control["key"], set())
1195
+ if len(matches) == 1:
1196
+ control["fieldKey"] = next(iter(matches))
1197
+ if include_list:
1198
+ tree = list_meta.get("BillList")
1199
+ visit(json.loads(tree) if isinstance(tree, str) else tree)
1200
+ list_plugin = plugin_type in {"listform", "treelist", "moblist"}
1201
+ plugin_rows = ((list_config or {}).get("plugins", []) if list_plugin else (form_config or {}).get("Plugins", []))
1202
+ if not list_plugin and not plugin_rows:
1203
+ from run import _kingdee_plugins_from_basedata
1204
+ for host in items:
1205
+ if _xml_localname(host.tag) in {"FormAp", "BillFormAp", "BasedataFormAp"}:
1206
+ plugin_rows.extend(_kingdee_plugins_from_basedata(host)[0])
1207
+ plugins = [{"className": item.get("ClassName") or item.get("className"), "type": "form" if plugin_type == "operation" else plugin_type}
1208
+ for item in plugin_rows if isinstance(item, dict)
1209
+ and (item.get("ClassName") or item.get("className")) and item.get("Enabled", item.get("enabled", True))]
1210
+ return {"formNumber": form_number, "formName": _xml_child_text(root, "Name") or form_number,
1211
+ "modelType": _xml_child_text(root, "ModelType") or None, "controls": controls, "plugins": plugins,
1212
+ "source": "getFormMetadata"}
1213
+
1214
+
989
1215
  def prepare_handoff(
990
1216
  *, app_root: str, project_root: str | None, plugin_type: str,
991
1217
  intent: dict, artifact_paths: list[str], run_result: Any = None,
992
1218
  operation_result: Any = None, external_schemas: list[Any] | None = None,
993
- runtime_metadata: list[Any] | None = None,
1219
+ runtime_metadata: list[Any] | None = None, ui_context: dict | None = None, operation_context: dict | None = None, source_only: bool = False, prepare_source: bool = False, refresh_report_state: bool = False,
994
1220
  ) -> dict:
995
1221
  if not isinstance(intent, dict):
996
1222
  raise HandoffError("intent must be a JSON object")
1223
+ if refresh_report_state and (not artifact_paths or runtime_metadata is not None):
1224
+ raise HandoffError("--refresh-report-state requires a complete report artifact, not runtime metadata", "PLUGIN_REPORT_STATE_INPUT_INVALID")
997
1225
  if runtime_metadata is not None:
998
1226
  if not runtime_metadata:
999
1227
  raise HandoffError("runtime metadata is empty", "PLUGIN_CONTEXT_INCOMPLETE")
1000
1228
  artifact_paths = []
1001
1229
  forms, entities, fields, registered_plugins = (runtime_catalog(runtime_metadata) if runtime_metadata
1002
1230
  else _catalog(artifact_paths, run_result))
1231
+ if operation_context:
1232
+ if not forms:
1233
+ forms = [{"formNumber": operation_context["formNumber"], "formName": operation_context["formNumber"]}]
1234
+ elif not any(row["formNumber"] == operation_context["formNumber"] for row in forms):
1235
+ raise HandoffError("operation and entity metadata identify different forms", "PLUGIN_CONTEXT_INCOMPLETE")
1236
+ if source_only:
1237
+ if plugin_type == "operation" or intent.get("ownershipMode") != "modify" or not intent.get("sourcePath"):
1238
+ raise HandoffError("source-only preparation requires an existing non-operation class", "PLUGIN_CONTEXT_INCOMPLETE")
1239
+ forms = [{"formNumber": "", "formName": ""}]
1240
+ if ui_context:
1241
+ if forms and not any(row["formNumber"] == ui_context["formNumber"] for row in forms):
1242
+ raise HandoffError("page and entity metadata identify different forms", "PLUGIN_CONTEXT_INCOMPLETE")
1243
+ if not forms:
1244
+ forms = [{"formNumber": ui_context["formNumber"], "formName": ui_context["formName"]}]
1245
+ registered_plugins += [{"formNumber": ui_context["formNumber"], **item} for item in ui_context["plugins"]]
1246
+ if not runtime_metadata:
1247
+ fields += [{"formNumber": ui_context["formNumber"], "formName": ui_context["formName"],
1248
+ "fieldKey": control["fieldKey"], "fieldName": control["fieldKey"], "role": "header"}
1249
+ for control in ui_context["controls"] if control.get("fieldKey")]
1003
1250
  main_form = _exact_one(forms, intent.get("form"), ("formName", "formNumber"), "form")
1004
1251
  related = None
1005
1252
  if intent.get("relatedForm"):
@@ -1009,24 +1256,8 @@ def prepare_handoff(
1009
1256
  operation_keys = _string_list(intent.get("operationKeys"), "operationKeys")
1010
1257
  if plugin_type == "operation" and not operation_keys:
1011
1258
  raise HandoffError("type=operation requires operationKeys")
1012
- if plugin_type == "operation":
1013
- if operation_result is None:
1014
- raise HandoffError("type=operation requires a listOperations read-back via --operations-file")
1015
- verified_operations = _verified_operation_keys(operation_result)
1016
- operation_data = operation_result.get("data") if isinstance(operation_result, dict) else None
1017
- if isinstance(operation_data, dict):
1018
- if operation_data.get("formNumber") and operation_data["formNumber"] != main_form["formNumber"]:
1019
- raise HandoffError("listOperations result belongs to another form")
1020
- if operation_data.get("appRoot") and Path(operation_data["appRoot"]).resolve() != Path(app_root).resolve():
1021
- raise HandoffError("listOperations result belongs to another application")
1022
- missing_operations = [key for key in operation_keys if key not in verified_operations]
1023
- if missing_operations:
1024
- raise HandoffError(
1025
- "operationKeys are absent from listOperations read-back: "
1026
- + ", ".join(missing_operations)
1027
- )
1028
- else:
1029
- verified_operations = set()
1259
+ verified_operations = (_validate_operation_result(operation_result, main_form["formNumber"], app_root, operation_keys)
1260
+ if plugin_type == "operation" else set())
1030
1261
  if plugin_type != "operation" and operation_keys:
1031
1262
  raise HandoffError("operationKeys are only valid for type=operation")
1032
1263
  trigger = str(intent.get("trigger") or "").strip()
@@ -1039,7 +1270,24 @@ def prepare_handoff(
1039
1270
  "ownershipMode must be create-new or modify (patch-existing is accepted as modify)"
1040
1271
  )
1041
1272
 
1042
- path_result = resolve_output_path(app_root, plugin_type, project_root)
1273
+ source_directory = intent.get("sourceDirectory")
1274
+ if source_directory is not None and (not isinstance(source_directory, str) or not Path(source_directory).is_absolute()):
1275
+ raise HandoffError("sourceDirectory must be an absolute Java package directory", "PLUGIN_WORKSPACE_UNRESOLVED")
1276
+ explicit_source = intent.get("sourcePath") if ownership_mode == "modify" else None
1277
+ if explicit_source and not Path(explicit_source).expanduser().is_absolute():
1278
+ raise HandoffError("sourcePath must be absolute", "PLUGIN_WORKSPACE_UNRESOLVED")
1279
+ source_resolution = source_directory
1280
+ if (source_directory or explicit_source) and not project_root:
1281
+ directory = Path(source_directory).expanduser().resolve() if source_directory else Path(explicit_source).expanduser().resolve().parent
1282
+ source_resolution = str(directory)
1283
+ java_root = next((item for item in (directory, *directory.parents)
1284
+ if item.name == "java" and item.parent.name == "main" and item.parent.parent.name == "src"), None)
1285
+ if java_root is None:
1286
+ raise HandoffError("sourceDirectory must be inside a Java module src/main/java", "PLUGIN_WORKSPACE_UNRESOLVED")
1287
+ project_root = str(java_root.parent.parent.parent)
1288
+ path_result = resolve_output_path(app_root, plugin_type, project_root,
1289
+ prepare_source=prepare_source and ownership_mode == "create-new",
1290
+ source_directory=source_resolution)
1043
1291
  if not path_result.get("projectRoot"):
1044
1292
  raise HandoffError(
1045
1293
  path_result.get("error") or "cannot determine the Java project root",
@@ -1054,11 +1302,20 @@ def prepare_handoff(
1054
1302
  # The appCode-derived convention package would silently disagree with it and the class
1055
1303
  # would not be found at runtime, so the registered FQCN wins here.
1056
1304
  package_path = path_result.get("packagePath")
1057
- package_source = "appCode convention" if package_path else None
1305
+ package_source = ("selected source directory" if source_directory else "appCode convention") if package_path else None
1058
1306
  form_plugins = [
1059
1307
  row for row in registered_plugins
1060
1308
  if row["formNumber"] == main_form["formNumber"]
1309
+ # A registered row names the slot its class runs in; a plugin of another type, such as
1310
+ # the report form's own reportform plugin, is none of them. `report-plugin` is the
1311
+ # registry's older name for the data-fetch type and runs in the same slot.
1312
+ and row.get("type", plugin_type) == ("reportlist" if plugin_type == "report-plugin" else plugin_type)
1061
1313
  ]
1314
+ # A page can have several independent plugins. Creating a new class must not
1315
+ # reuse an unrelated registered class, while an existing identity stays protected.
1316
+ page_plugins = form_plugins if ui_context and ownership_mode == "create-new" else []
1317
+ if page_plugins:
1318
+ form_plugins = []
1062
1319
  if form_plugins:
1063
1320
  if class_name:
1064
1321
  matching = [row for row in form_plugins if row["className"].rsplit(".", 1)[-1] == class_name]
@@ -1090,11 +1347,13 @@ def prepare_handoff(
1090
1347
  f"registered plugin class {registered_class!r} must match [A-Za-z][A-Za-z0-9]*",
1091
1348
  "PLUGIN_CLASS_NOT_QUALIFIED",
1092
1349
  )
1350
+ if source_directory and registered_package.replace(".", "/") != package_path:
1351
+ raise HandoffError("registered class package differs from the selected source directory", "PLUGIN_CLASS_NOT_REGISTERED")
1093
1352
  package_path = registered_package.replace(".", "/")
1094
1353
  package_source = "registered plugin className"
1095
1354
  class_name = registered_class
1096
1355
 
1097
- if not package_path:
1356
+ if not package_path and ownership_mode != "modify":
1098
1357
  raise HandoffError(
1099
1358
  path_result.get("error") or "cannot determine packagePath",
1100
1359
  str(path_result.get("errorCode") or "PLUGIN_PACKAGE_UNRESOLVED"),
@@ -1131,6 +1390,9 @@ def prepare_handoff(
1131
1390
  raise HandoffError("existing source package conflicts with the registered plugin class", "PLUGIN_CLASS_NOT_REGISTERED")
1132
1391
  package_path = source_package
1133
1392
  package_source = "existing source declaration"
1393
+ elif not package_path:
1394
+ package_path = ""
1395
+ package_source = "existing source default package"
1134
1396
  else:
1135
1397
  # An existing file at the create-new target means the class is already in this project,
1136
1398
  # so generating it again would either duplicate its methods or overwrite its logic. The
@@ -1138,6 +1400,8 @@ def prepare_handoff(
1138
1400
  # the case where the caller never checked whether the class was already there.
1139
1401
  candidate = class_name or _conventional_class_name(
1140
1402
  main_form["formNumber"], path_result.get("classSuffix"))
1403
+ if candidate and any(row["className"].rsplit(".", 1)[-1] == candidate for row in page_plugins):
1404
+ raise HandoffError(f"class {candidate} is already registered; use ownershipMode=modify", "PLUGIN_OWNERSHIP_CONFLICT")
1141
1405
  target = (
1142
1406
  Path(path_result["projectRoot"]) / "src" / "main" / "java" / package_path / f"{candidate}.java"
1143
1407
  if candidate else None
@@ -1234,11 +1498,27 @@ def prepare_handoff(
1234
1498
  "fields": field_map,
1235
1499
  "coreLogic": core_logic,
1236
1500
  "specialized": specialized,
1501
+ "sdkUsage": intent.get("sdkUsage", []),
1502
+ **({"metadataNeeds": intent["metadataNeeds"]} if "metadataNeeds" in intent else {}),
1237
1503
  "description": (str(intent.get("description") or "").strip() or core_logic[0])[:20],
1238
1504
  }
1505
+ if ui_context:
1506
+ handoff["uiContext"] = ui_context
1507
+ evidence_files = [snapshot["_pluginEvidence"] for snapshot in runtime_metadata or []
1508
+ if isinstance(snapshot, dict) and isinstance(snapshot.get("_pluginEvidence"), dict)]
1509
+ if evidence_files:
1510
+ handoff["entityEvidence"] = evidence_files
1511
+ binding = Path(app_root).resolve() / "_runtime" / "context.json"
1512
+ if (ui_context or evidence_files) and binding.is_file():
1513
+ import hashlib
1514
+ handoff["reuseBinding"] = {"path": str(binding), "sha256": hashlib.sha256(binding.read_bytes()).hexdigest()}
1515
+ if operation_context:
1516
+ handoff["operationContext"] = operation_context
1517
+ if source_only:
1518
+ handoff["sourceContext"] = {"scope": "source-only", "sourcePath": str(Path(source_path).expanduser().resolve())}
1239
1519
  if runtime_metadata:
1240
1520
  def graph(form: dict) -> dict:
1241
- return {key: form[key] for key in ("formNumber", "formName", "metadataSource", "metadataVersion")} | {
1521
+ return {key: form[key] for key in ("formNumber", "formName", "metadataSource", "metadataVersion", "appCode")} | {
1242
1522
  "entities": [{key: entity[key] for key in
1243
1523
  ("entityKey", "entityName", "entityType", "role", "parentEntityKey", "collectionProperty", "collectionPath")}
1244
1524
  | {"fields": [_field_contract(row) for row in fields
@@ -1248,6 +1528,9 @@ def prepare_handoff(
1248
1528
  handoff["relatedForms"] = [graph(form) for form in forms if form["formNumber"] != main_form["formNumber"]]
1249
1529
  handoff["entities"].update({row["entityName"]: row["entityKey"] for row in entities
1250
1530
  if row["formNumber"] == main_form["formNumber"]})
1531
+ if operation_result is not None and plugin_type != "operation":
1532
+ verified = _validate_operation_result(operation_result, main_form["formNumber"], app_root, [])
1533
+ handoff["availableOperations"] = operation_contracts(operation_result, sorted(verified))
1251
1534
  if operation_keys:
1252
1535
  handoff["operationContracts"] = operation_contracts(operation_result, operation_keys)
1253
1536
  if runtime_metadata:
@@ -1290,10 +1573,38 @@ def prepare_handoff(
1290
1573
  report_artifacts = [
1291
1574
  (path, artifact)
1292
1575
  for path, artifact in ((path, _read_json(path)) for path in artifact_paths)
1293
- if isinstance(artifact, dict)
1576
+ if isinstance(artifact, dict) and str(Path(path).resolve()) == main_form.get("artifact")
1294
1577
  and _is_report_entity((artifact.get("entities") or [None])[0])
1295
1578
  ]
1296
- report_contract = _report_plugin_contract(report_artifacts, main_form["formNumber"])
1579
+ report_state_path = Path(app_root).resolve() / "_runtime" / f"{main_form['formNumber']}.report-authoring.json"
1580
+ filter_editors = _run_filter_editors(run_result, main_form["formNumber"])
1581
+ if refresh_report_state and not report_artifacts:
1582
+ raise HandoffError("--refresh-report-state requires the selected form's complete report artifact", "PLUGIN_REPORT_STATE_INPUT_INVALID")
1583
+ state_inputs = []
1584
+ if report_artifacts and (report_state_path.is_file() or refresh_report_state):
1585
+ _compile_report_contract(report_artifacts[0][1], main_form["formNumber"])
1586
+ from _core.report_ops import report_readback_verification, write_report_authoring_state
1587
+ if refresh_report_state:
1588
+ schema = read_platform(app_root, "getReportSchema", main_form["formNumber"])
1589
+ artifact = report_artifacts[0][1]
1590
+ checks = [report_readback_verification(section, artifact.get(key) or [], schema)
1591
+ for section, key in (("columns", "reportColumns"), ("filters", "reportFilters"))]
1592
+ if (schema.get("formNumber") != main_form["formNumber"] or not schema.get("formId")
1593
+ or (main_form.get("formId") and main_form["formId"] != schema["formId"])
1594
+ or any(not item.get("verified") for item in checks)):
1595
+ raise HandoffError("Full report artifact must match current observable metadata before local reconciliation", "PLUGIN_REPORT_STATE_MISMATCH")
1596
+ # 旧运行回执的 editorStatus 不属于本次独立读取,不复制为当前证据。
1597
+ write_report_authoring_state(app_root, main_form["formNumber"], schema["formId"], artifact, schema)
1598
+ import hashlib
1599
+ state_inputs = [{"path": str(report_state_path), "sha256": hashlib.sha256(report_state_path.read_bytes()).hexdigest()}]
1600
+ state = _read_json(report_state_path)
1601
+ if (not isinstance(state, dict) or state.get("status") != "current" or state.get("formNumber") != main_form["formNumber"]
1602
+ or not state.get("formId") or (main_form.get("formId") and state["formId"] != main_form["formId"])):
1603
+ raise HandoffError("Report authoring state is incomplete; reconcile the full current artifact with --refresh-report-state (read-only platform lookup)", "PLUGIN_REPORT_STATE_STALE")
1604
+ if _compile_report_contract(state.get("artifact"), main_form["formNumber"]) != _compile_report_contract(report_artifacts[0][1], main_form["formNumber"]):
1605
+ raise HandoffError(f"Supplied report artifact is stale; use current artifactPath {state.get('artifactPath')} or reconcile a complete artifact with --refresh-report-state", "PLUGIN_REPORT_STATE_STALE")
1606
+ filter_editors = state.get("filterEditors")
1607
+ report_contract = _report_plugin_contract(report_artifacts, main_form["formNumber"], filter_editors)
1297
1608
  if report_contract:
1298
1609
  # Column types, presentation binding and non-persisted filter requirements are part of
1299
1610
  # the plugin's job, not optional polish, so they travel inside the handoff itself.
@@ -1305,9 +1616,9 @@ def prepare_handoff(
1305
1616
  if source_path:
1306
1617
  handoff["sourcePath"] = str(Path(source_path).expanduser().resolve())
1307
1618
  if class_name:
1308
- handoff["fqcn"] = package_path.replace("/", ".") + "." + class_name
1619
+ handoff["fqcn"] = ".".join(part for part in (package_path.replace("/", "."), class_name) if part)
1309
1620
  try:
1310
- handoff["codingEvidence"] = plugin_coding_evidence(plugin_type, _string_list(intent.get("scenes"), "scenes"))
1621
+ handoff["codingEvidence"] = plugin_coding_evidence(plugin_type, _string_list(intent.get("scenes"), "scenes"), core_logic=core_logic, hit_modules=specialized["hitModules"])
1311
1622
  except ValueError as exc:
1312
1623
  raise HandoffError(str(exc), "PLUGIN_CODING_EVIDENCE_INVALID") from exc
1313
1624
  if ownership_mode == "modify":
@@ -1315,8 +1626,11 @@ def prepare_handoff(
1315
1626
  # list means an empty class body rather than an unread file. New logic belongs inside
1316
1627
  # these; declaring one of them again is rejected by the compile boundary.
1317
1628
  handoff["existingMethods"] = existing_methods
1629
+ state_inputs.extend({"path": row["path"], "sha256": row["sha256"]} for row in evidence_files)
1318
1630
  return {
1319
1631
  "status": True,
1632
+ "inputFiles": state_inputs,
1633
+ **({"reportStatePath": str(report_state_path)} if report_artifacts and report_state_path.is_file() else {}),
1320
1634
  "handoff": handoff,
1321
1635
  "evidence": {
1322
1636
  "forms": [{"formName": row["formName"], "formNumber": row["formNumber"]} for row in forms],
@@ -1337,15 +1651,20 @@ def prepare_handoff(
1337
1651
 
1338
1652
  def handoff_response(result: dict, *, full_output: bool = False) -> dict:
1339
1653
  """Keep the CLI below the host output limit; the saved context remains complete."""
1654
+ coding_start = [sys.executable, str(Path(__file__).with_name("query_plugin_handoff.py")), "coding-start", "--handoff", result["handoffPath"]]
1340
1655
  if full_output:
1341
- return {key: result[key] for key in ("status", "handoffPath", "handoff")}
1656
+ return {**{key: result[key] for key in ("status", "handoffPath", "handoff", "sourcePreparation") if key in result},
1657
+ "codingStartCommand": coding_start}
1342
1658
  handoff = result["handoff"]
1343
1659
  coding = handoff.get("codingEvidence", {})
1344
1660
  graphs = [handoff["formGraph"], *handoff.get("relatedForms", [])] if handoff.get("formGraph") else []
1661
+ page = handoff.get("uiContext") if not graphs else None
1345
1662
  return {
1346
1663
  "status": result["status"],
1347
1664
  "handoffPath": result["handoffPath"],
1665
+ "codingStartCommand": coding_start,
1348
1666
  "contextMode": "reference-only",
1667
+ **({"sourcePreparation": result["sourcePreparation"]} if "sourcePreparation" in result else {}),
1349
1668
  "handoff": {
1350
1669
  **{key: handoff[key] for key in (
1351
1670
  "appRoot", "projectRoot", "packagePath", "type", "ownershipMode", "formNumber",
@@ -1361,21 +1680,22 @@ def handoff_response(result: dict, *, full_output: bool = False) -> dict:
1361
1680
  },
1362
1681
  },
1363
1682
  "metadata": {
1364
- "formCount": len(graphs),
1683
+ "formCount": len(graphs) + (1 if page else 0),
1365
1684
  "forms": [{
1366
1685
  **{key: graph[key] for key in ("formNumber", "metadataSource", "metadataVersion") if key in graph},
1367
1686
  "entityCount": len(graph.get("entities", [])),
1368
1687
  "fieldCount": sum(len(entity.get("fields", [])) for entity in graph.get("entities", [])),
1369
- } for graph in graphs[:8]],
1688
+ } for graph in graphs[:8]] + ([{"formNumber": page["formNumber"], "metadataSource": page["source"],
1689
+ "controlCount": len(page.get("controls", []))}] if page else []),
1370
1690
  },
1371
- "contextSections": ["handoff.formGraph", "handoff.relatedForms", "handoff.specialized",
1372
- "handoff.operationContracts", "handoff.stateFields", "handoff.stateWarnings",
1373
- "handoff.codingEvidence", "handoff.reportContract", "handoff.targetSdk"],
1691
+ "contextSections": ["handoff." + key for key in ("formGraph", "relatedForms", "uiContext", "sourceContext",
1692
+ "operationContext", "specialized", "operationContracts", "stateFields", "stateWarnings",
1693
+ "codingEvidence", "reportContract", "targetSdk") if key in handoff],
1374
1694
  "instruction": (
1375
1695
  "This response is a summary, not the coding context. Keep the complete saved handoffPath "
1376
- "for native compilation; begin with query_plugin_handoff.py coding-start, then query only missing field paths, collection hierarchy, external contracts, "
1696
+ "for native compilation; execute codingStartCommand directly, then query only missing field paths, collection hierarchy, external contracts, "
1377
1697
  "operation values, warnings and SDK signatures needed for this class with query_plugin_handoff.py. "
1378
- "Index or find exact paths before get; do not read the whole JSON into the model. Use its exact codingEntry "
1698
+ "Use known JSON pointers directly with get; use index or find only for unknown paths; do not read the whole JSON into the model. Use its exact codingEntry "
1379
1699
  "and verified signatureTypes imports; missing or ambiguous SDK types require the supplied API lookup. "
1380
1700
  "Pass handoffPath directly to compile_plugin_source.py --handoff; do not search for another file."
1381
1701
  ),
@@ -1388,12 +1708,13 @@ def assert_handoff_replace(destination: Path, result: dict, replace: bool = Fals
1388
1708
  existing = _read_json(destination)
1389
1709
  previous = existing.get("handoff") if isinstance(existing, dict) and existing.get("status") is True else None
1390
1710
  current = result["handoff"]
1391
- fields = ("projectRoot", "appRoot", "sourcePath", "fqcn", "formNumber", "type", "coreLogic")
1711
+ fields = ("projectRoot", "appRoot", "sourcePath", "fqcn", "formNumber", "type", "coreLogic",
1712
+ "ownershipMode", "fields", "fieldContracts", "specialized", "sdkUsage", "reportContract")
1392
1713
  if not isinstance(previous, dict) or any(previous.get(key) != current.get(key) for key in fields) or sorted(
1393
1714
  previous.get("operationKeys", [])) != sorted(current.get("operationKeys", [])):
1394
1715
  raise HandoffError(
1395
1716
  f"Prepared class already has a different snapshot at {destination}. "
1396
- "Review the existing snapshot and reconcile the class identity and business scope before explicitly replacing it with --replace-handoff.",
1717
+ "For a separate plugin class, set an explicit intent.className. For the same class, reconcile its full business scope before explicitly replacing it with --replace-handoff.",
1397
1718
  "PLUGIN_HANDOFF_CONFLICT")
1398
1719
 
1399
1720
 
@@ -1407,6 +1728,7 @@ def main() -> None:
1407
1728
  artifacts.add_argument("--artifact", action="append", dest="artifact_paths")
1408
1729
  artifacts.add_argument("--emit-manifest", help="Exact manifest path returned by the emitter; includes every declared artifact")
1409
1730
  parser.add_argument("--run-result")
1731
+ parser.add_argument("--refresh-report-state", action="store_true", help="Reconcile a complete report artifact against current getReportSchema; never writes platform metadata")
1410
1732
  parser.add_argument("--form-number", help="Exact existing form number; independently reads runtime metadata")
1411
1733
  parser.add_argument("--operations-file", help="Reuse an existing listOperations result; otherwise read once automatically")
1412
1734
  parser.add_argument("--output", help="Save prepared context here; default is the app-local class handoff file")
@@ -1421,41 +1743,99 @@ def main() -> None:
1421
1743
  intent_group.add_argument("--intent-file")
1422
1744
  args = parser.parse_args()
1423
1745
  try:
1746
+ import hashlib
1747
+ from query_plugin_handoff import validate_input_files
1748
+ input_files = [{"path": str(Path(path).resolve()), "sha256": hashlib.sha256(Path(path).read_bytes()).hexdigest()}
1749
+ for path in dict.fromkeys(filter(None, [args.intent_file, args.run_result, args.operations_file,
1750
+ args.emit_manifest, *(args.external_schemas or [])]))]
1424
1751
  intent = json.loads(args.intent_json) if args.intent_json else _read_json(args.intent_file)
1425
1752
  if not isinstance(intent, dict):
1426
1753
  raise HandoffError("intent must be a JSON object")
1754
+ if not args.project_root and not intent.get("sourceDirectory") and not (intent.get("ownershipMode") == "modify" and intent.get("sourcePath")):
1755
+ raise HandoffError("Java plugin directory is not specified; ask the user for the source directory before continuing", "PLUGIN_WORKSPACE_UNRESOLVED")
1427
1756
  run_result = _read_json(args.run_result) if args.run_result else None
1428
1757
  if run_result is not None and not isinstance(run_result, dict):
1429
1758
  raise HandoffError("write result must be a JSON object")
1430
1759
  operation_result = _read_json(args.operations_file) if args.operations_file else None
1760
+ external_schemas = [_read_json(path) for path in args.external_schemas or []]
1431
1761
  artifact_paths = (manifest_artifacts(args.emit_manifest) if args.emit_manifest else args.artifact_paths
1432
1762
  or (run_result or {}).get("artifactPaths"))
1763
+ if args.plugin_type != "operation" and (not args.form_number or args.plugin_type in {"reportlist", "report-plugin", "reportform", "reporttreelist"}):
1764
+ input_files.extend({"path": str(Path(path).resolve()), "sha256": hashlib.sha256(Path(path).read_bytes()).hexdigest()}
1765
+ for path in artifact_paths or [])
1433
1766
  runtime_metadata = None
1434
- if args.form_number or not artifact_paths or args.plugin_type == "operation":
1767
+ ui_context = None
1768
+ operation_context = None
1769
+ needs = intent.get("metadataNeeds")
1770
+ if needs is not None:
1771
+ needs = _string_list(needs, "metadataNeeds")
1772
+ if set(needs) - {"form", "list", "entity", "operate"}:
1773
+ raise HandoffError("unknown metadataNeeds", "PLUGIN_CONTEXT_INCOMPLETE")
1774
+ if args.plugin_type == "operation" and "operate" not in needs:
1775
+ raise HandoffError("operation plugins require operation evidence", "PLUGIN_CONTEXT_INCOMPLETE")
1776
+ if "entity" not in needs and ((intent.get("fields") and not ({"form", "list"} & set(needs))) or intent.get("entities") or intent.get("relatedForm")
1777
+ or (intent.get("specialized") or {}).get("externalContracts") or (intent.get("specialized") or {}).get("externalApis")):
1778
+ raise HandoffError("entity selectors require entity metadata", "PLUGIN_CONTEXT_INCOMPLETE")
1779
+ source_only = needs == [] and intent.get("ownershipMode") == "modify" and bool(intent.get("sourcePath"))
1780
+ if source_only and args.plugin_type == "operation":
1781
+ raise HandoffError("operation plugins require verified operation metadata", "PLUGIN_CONTEXT_INCOMPLETE")
1782
+ if args.refresh_report_state and not artifact_paths:
1783
+ raise HandoffError("--refresh-report-state requires --artifact or --emit-manifest", "PLUGIN_REPORT_STATE_INPUT_INVALID")
1784
+ if args.form_number and artifact_paths and args.plugin_type in {"reportlist", "report-plugin", "reportform", "reporttreelist"}:
1785
+ selected = _exact_one(_catalog(artifact_paths, run_result)[0], args.form_number, ("formNumber",), "form")
1786
+ intent["form"] = selected["formNumber"]
1787
+ if (args.form_number and args.plugin_type not in {"reportlist", "report-plugin", "reportform", "reporttreelist"}) or not artifact_paths or args.plugin_type == "operation":
1435
1788
  form_number = args.form_number or str(intent.get("form") or "")
1436
1789
  if not args.form_number and artifact_paths:
1437
1790
  hints = (run_result or {}).get("forms") or _catalog(artifact_paths, run_result)[0]
1438
1791
  form_number = _exact_one(hints, intent.get("form"), ("formName", "formNumber"), "form")["formNumber"]
1439
- if not re.fullmatch(r"[A-Za-z0-9_]+", form_number):
1792
+ if not source_only and not re.fullmatch(r"[A-Za-z0-9_]+", form_number):
1440
1793
  raise HandoffError("independent preparation requires --form-number with an exact existing form code")
1441
- runtime_metadata, operation_result = prepare_runtime_inputs(args.app_root, form_number, intent, args.plugin_type)
1442
- intent["form"] = form_number
1794
+ requested_ui = set(needs or []) & {"form", "list"}
1795
+ if requested_ui and "entity" in (needs or []):
1796
+ from concurrent.futures import ThreadPoolExecutor
1797
+ with ThreadPoolExecutor(max_workers=2) as pool:
1798
+ ui_read = pool.submit(read_ui_context, args.app_root, form_number, args.plugin_type,
1799
+ include_list="list" in requested_ui)
1800
+ entity_read = pool.submit(prepare_runtime_inputs, args.app_root, form_number, intent, args.plugin_type,
1801
+ operation_result=operation_result, external_schemas=external_schemas)
1802
+ ui_context = ui_read.result()
1803
+ runtime_metadata, operation_result = entity_read.result()
1804
+ elif "list" in requested_ui:
1805
+ ui_context = read_ui_context(args.app_root, form_number, args.plugin_type, include_list=True)
1806
+ elif "form" in requested_ui:
1807
+ ui_context = read_ui_context(args.app_root, form_number, args.plugin_type)
1808
+ if needs is not None and "operate" in needs and (args.plugin_type != "operation" or "entity" not in needs):
1809
+ operation_result = operation_result or read_platform(args.app_root, "listOperations", form_number)
1810
+ _validate_operation_result(operation_result, form_number, args.app_root,
1811
+ _string_list(intent.get("operationKeys"), "operationKeys") if args.plugin_type == "operation" else [])
1812
+ if args.plugin_type == "operation" and "entity" not in needs:
1813
+ operation_context = {"formNumber": form_number, "source": "listOperations"}
1814
+ if needs is None or "entity" in needs:
1815
+ if runtime_metadata is None:
1816
+ runtime_metadata, operation_result = prepare_runtime_inputs(args.app_root, form_number, intent, args.plugin_type,
1817
+ operation_result=operation_result, external_schemas=external_schemas)
1818
+ elif not ui_context and not operation_context and not source_only:
1819
+ raise HandoffError("a verified page context is required when entity metadata is omitted", "PLUGIN_CONTEXT_INCOMPLETE")
1820
+ intent["form"] = "" if source_only else form_number
1443
1821
  artifact_paths = []
1444
1822
  else:
1445
1823
  operation_result, external_schemas = prepare_inputs(
1446
1824
  app_root=args.app_root, intent=intent, plugin_type=args.plugin_type,
1447
1825
  artifact_paths=artifact_paths, run_result=run_result, operation_result=operation_result,
1448
- external_schemas=[_read_json(path) for path in args.external_schemas or []])
1826
+ external_schemas=external_schemas)
1449
1827
  result = prepare_handoff(
1450
1828
  app_root=args.app_root, project_root=args.project_root, plugin_type=args.plugin_type,
1451
1829
  intent=intent, artifact_paths=artifact_paths, run_result=run_result,
1452
- operation_result=operation_result, runtime_metadata=runtime_metadata,
1453
- external_schemas=([_read_json(path) for path in args.external_schemas or []]
1454
- if runtime_metadata else external_schemas),
1830
+ operation_result=operation_result, runtime_metadata=runtime_metadata, ui_context=ui_context, operation_context=operation_context, source_only=source_only, prepare_source=args.prepare_source,
1831
+ refresh_report_state=args.refresh_report_state,
1832
+ external_schemas=external_schemas,
1455
1833
  )
1456
1834
  destination = (Path(args.output).resolve() if args.output else Path(args.app_root).resolve()
1457
1835
  / "_runtime" / "plugin-handoffs" / (result["handoff"]["className"] + ".json"))
1458
1836
  assert_handoff_replace(destination, result, args.replace_handoff)
1837
+ result["inputFiles"] = [*input_files, *result.get("inputFiles", [])]
1838
+ validate_input_files(result)
1459
1839
  if args.prepare_source:
1460
1840
  from plugin_target_sdk import verify_target_sdk
1461
1841
  result['handoff']['targetSdk'] = verify_target_sdk(result['handoff']['projectRoot'],
@@ -1468,17 +1848,37 @@ def main() -> None:
1468
1848
  target = Path(handoff["sourcePath"]).resolve()
1469
1849
  if not target.is_relative_to(Path(handoff["projectRoot"]).resolve() / "src" / "main" / "java"):
1470
1850
  raise HandoffError("source preparation target is outside the verified Java module", "PLUGIN_SOURCE_INVALID")
1851
+ validate_input_files(result)
1471
1852
  target.parent.mkdir(parents=True, exist_ok=True)
1472
- write_state(handoff["projectRoot"], handoff["className"], "coding",
1473
- os.environ.get("LINGEE_SESSION_ID") or os.environ.get("OPENCODE_SESSION_ID"), emit=False)
1474
- handoff["sourceGate"] = {"state": "coding", "path": gate_path(handoff["projectRoot"], handoff["className"])}
1853
+ # 保存的是门禁请求;实际写成功后才在响应中报告 coding。
1854
+ handoff["sourceGate"] = {"requestedState": "coding", "path": gate_path(handoff["projectRoot"], handoff["className"])}
1475
1855
  destination.parent.mkdir(parents=True, exist_ok=True)
1476
1856
  result["handoffPath"] = str(destination)
1477
- destination.write_text(json.dumps(result, ensure_ascii=False, indent=2), encoding="utf-8")
1857
+ validate_input_files(result)
1858
+ import os
1859
+ import uuid
1860
+ temporary = destination.with_name(destination.name + "." + uuid.uuid4().hex + ".tmp")
1861
+ try:
1862
+ temporary.write_text(json.dumps(result, ensure_ascii=False, indent=2), encoding="utf-8")
1863
+ os.replace(temporary, destination)
1864
+ finally:
1865
+ temporary.unlink(missing_ok=True)
1866
+ if args.prepare_source:
1867
+ # 快照落盘失败时不推进门禁;门禁失败也不输出准备成功。
1868
+ try:
1869
+ write_state(handoff["projectRoot"], handoff["className"], "coding",
1870
+ os.environ.get("LINGEE_SESSION_ID") or os.environ.get("OPENCODE_SESSION_ID"), emit=False)
1871
+ except OSError as exc:
1872
+ raise HandoffError("Coding context saved, but source gate unavailable; resolve directory authorization before editing or compiling",
1873
+ "PLUGIN_SOURCE_GATE_UNAVAILABLE", details={"handoffPath": str(destination),
1874
+ "sourcePath": handoff["sourcePath"], "gatePath": handoff["sourceGate"]["path"],
1875
+ "codingStartCommand": [sys.executable, str(Path(__file__).with_name("query_plugin_handoff.py")), "coding-start", "--handoff", str(destination)],
1876
+ "recovery": "The context can be read without repeating preparation. Do not probe alternative write paths, copy scripts or retry unchanged builds. Resolve the selected directory's authorization or ask for a revised delivery scope; the source gate remains unverified."}) from exc
1877
+ result["sourcePreparation"] = {"state": "coding", "verified": True, "path": handoff["sourceGate"]["path"]}
1478
1878
  print(json.dumps(handoff_response(result, full_output=args.full_output), ensure_ascii=False))
1479
- except (HandoffError, json.JSONDecodeError, ValueError) as exc:
1879
+ except (HandoffError, json.JSONDecodeError, ValueError, OSError) as exc:
1480
1880
  error_code = getattr(exc, "error_code", getattr(exc, "code", "PLUGIN_HANDOFF_UNRESOLVED"))
1481
- print(json.dumps({"status": False, "errorCode": error_code, "message": str(exc)}, ensure_ascii=False))
1881
+ print(json.dumps({"status": False, "errorCode": error_code, "message": str(exc), **getattr(exc, "details", {})}, ensure_ascii=False))
1482
1882
  raise SystemExit(2)
1483
1883
 
1484
1884