@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
@@ -66,7 +66,28 @@ def describe(value: object, pointer: str) -> dict:
66
66
  return result
67
67
 
68
68
 
69
- def load_handoff(filename: str) -> tuple[Path, dict]:
69
+ def validate_input_files(document: dict) -> None:
70
+ """准备失败后,旧成功快照不能掩盖已经变化或丢失的输入。"""
71
+ if not isinstance(document, dict):
72
+ raise QueryError("HANDOFF_INPUT_INVALID", "handoff must be an object")
73
+ records = document.get("inputFiles", [])
74
+ if not isinstance(records, list):
75
+ raise QueryError("HANDOFF_INPUT_INVALID", "inputFiles must be an array")
76
+ for record in records:
77
+ if not isinstance(record, dict) or not isinstance(record.get("path"), str) or not Path(record["path"]).is_absolute():
78
+ raise QueryError("HANDOFF_INPUT_INVALID", "inputFiles must contain absolute paths and hashes")
79
+ if not isinstance(record.get("sha256"), str) or len(record["sha256"]) != 64:
80
+ raise QueryError("HANDOFF_INPUT_INVALID", "inputFiles must contain SHA256 hashes")
81
+ file = Path(record["path"])
82
+ try:
83
+ digest = hashlib.sha256(file.read_bytes()).hexdigest() if file.is_file() else None
84
+ except OSError as exc:
85
+ raise QueryError("HANDOFF_INPUT_STALE", f"Prepared input is unreadable: {file}; prepare again") from exc
86
+ if digest != record["sha256"]:
87
+ raise QueryError("HANDOFF_INPUT_STALE", f"Prepared input changed or disappeared: {file}; prepare the handoff again before coding or archiving")
88
+
89
+
90
+ def load_handoff(filename: str, *, validate_inputs: bool = True) -> tuple[Path, dict]:
70
91
  file = Path(filename)
71
92
  if not file.is_absolute() or file.suffix.lower() != ".json":
72
93
  raise QueryError("HANDOFF_PATH_INVALID", "handoff must be an absolute JSON path returned by preparation")
@@ -78,16 +99,145 @@ def load_handoff(filename: str) -> tuple[Path, dict]:
78
99
  raise QueryError("HANDOFF_INVALID", "file is not a successful plugin handoff")
79
100
  if not isinstance(document.get("handoffPath"), str) or Path(document["handoffPath"]).resolve() != file.resolve():
80
101
  raise QueryError("HANDOFF_PATH_MISMATCH", "handoffPath does not match the selected file")
102
+ if validate_inputs:
103
+ validate_input_files(document)
81
104
  return file, document
82
105
 
83
106
 
107
+ def observed_source_gate(handoff: dict) -> dict:
108
+ """Project the current local gate, never an unfulfilled preparation request."""
109
+ from plugin_source_gate import gate_path
110
+
111
+ requested = handoff.get("sourceGate")
112
+ if not isinstance(requested, dict):
113
+ return {}
114
+ if not handoff.get("projectRoot") or not handoff.get("className"):
115
+ raise QueryError("HANDOFF_SOURCE_GATE_INVALID", "source gate requires projectRoot and className")
116
+ expected = Path(gate_path(handoff["projectRoot"], handoff["className"]))
117
+ if not requested.get("path") or Path(requested["path"]).resolve() != expected.resolve():
118
+ raise QueryError("HANDOFF_SOURCE_GATE_INVALID", "source gate path does not match this class")
119
+ try:
120
+ content = expected.read_text(encoding="utf-8").strip() if expected.is_file() else "missing"
121
+ except (OSError, UnicodeError) as error:
122
+ raise QueryError("HANDOFF_SOURCE_GATE_UNAVAILABLE", "cannot read this class's source gate") from error
123
+ state, _, session = content.partition(":")
124
+ return {"sourceGate": {**requested, "state": state, "sessionId": session or None,
125
+ "verified": state in ("coding", "source_ready"),
126
+ "evidenceScope": "Local marker only; session binding still applies to edits, and this is not a compile receipt."}}
127
+
128
+
129
+ def report_coding_context(document: dict, operation: str) -> dict:
130
+ """报表契约独立于运行时表单实体图,保留真实类型证据和精确查询指针。"""
131
+ from compile_plugin_source import inspect_compile_environment
132
+
133
+ handoff = document["handoff"]
134
+ contract = handoff.get("reportContract")
135
+ evidence = handoff["codingEvidence"]
136
+ if not isinstance(contract, dict) or contract.get("formNumber") != handoff.get("formNumber"):
137
+ raise QueryError("HANDOFF_CODING_CONTEXT_INVALID", "a reportContract for this exact report is required")
138
+ sections = ("dataSetColumns", "dataSetColumnTypes", "columnPresentation", "filterEnforcement", "filterValueContracts")
139
+ if "defaultSorts" in contract:
140
+ sections = (*sections, "defaultSorts")
141
+ if "defaultSortPolicy" in contract and not isinstance(contract["defaultSortPolicy"], dict):
142
+ raise QueryError("HANDOFF_CODING_CONTEXT_INVALID", "defaultSortPolicy must be an object")
143
+ if any(not isinstance(contract.get(key), list) for key in sections):
144
+ raise QueryError("HANDOFF_CODING_CONTEXT_INVALID", "report contract sections must be arrays")
145
+ if any(not isinstance(row, dict) for key in sections[1:] for row in contract[key]) or any(not isinstance(row, str) for row in contract[sections[0]]):
146
+ raise QueryError("HANDOFF_CODING_CONTEXT_INVALID", "report columns must be strings and contract records must be objects")
147
+ if not isinstance(evidence.get("apiContracts", []), list) or any(not isinstance(row, dict) for row in evidence.get("apiContracts", [])):
148
+ raise QueryError("HANDOFF_CODING_CONTEXT_INVALID", "apiContracts must be an array of objects")
149
+ if not isinstance(handoff.get("specialized", {}), dict) or not isinstance(handoff.get("targetSdk", {}), dict):
150
+ raise QueryError("HANDOFF_CODING_CONTEXT_INVALID", "specialized and targetSdk must be objects")
151
+ if not isinstance(handoff.get("specialized", {}).get("externalContracts", []), list):
152
+ raise QueryError("HANDOFF_CODING_CONTEXT_INVALID", "externalContracts must be an array")
153
+ result = {
154
+ "status": True, "contextMode": operation, "domain": "report",
155
+ "handoffPath": document["handoffPath"],
156
+ "handoffSha256": hashlib.sha256(Path(document["handoffPath"]).read_bytes()).hexdigest(),
157
+ **{key: handoff[key] for key in ("appRoot", "projectRoot", "formNumber", "type", "className",
158
+ "sourcePath", "fqcn", "ownershipMode", "existingMethods", "coreLogic", "sourceGate") if key in handoff},
159
+ **observed_source_gate(handoff),
160
+ "reportContract": {
161
+ "path": "/handoff/reportContract",
162
+ **{key: contract[key] for key in ("schemaVersion", "formNumber", "source", "pluginTypes", "permissionFilterChannel", "defaultSortPolicy") if key in contract},
163
+ "sections": {key: {"path": f"/handoff/reportContract/{key}", "count": len(contract[key])} for key in sections},
164
+ **{key: contract[key] for key in sections},
165
+ },
166
+ "externalContracts": {"path": "/handoff/specialized/externalContracts",
167
+ "entities": [entry.get("entity") for entry in handoff.get("specialized", {}).get("externalContracts", [])
168
+ if isinstance(entry, dict)]},
169
+ "codingEvidence": {
170
+ **{key: evidence[key] for key in ("codingEntry", "signatureTypes", "apiLookup", "sdkLookup", "scenes", "standards", "specializedReferences") if key in evidence},
171
+ **({"guide": evidence["guide"]} if isinstance(evidence.get("guide"), str) and len(evidence["guide"]) <= 512 else {}),
172
+ "apiContracts": [{"class": item.get("class"), "path": f"/handoff/codingEvidence/apiContracts/{index}"}
173
+ for index, item in enumerate(evidence.get("apiContracts", []))],
174
+ },
175
+ "sdkUsage": handoff.get("sdkUsage", []),
176
+ "targetSdk": {**{key: (handoff.get("targetSdk") or {}).get(key) for key in ("status", "evidenceScope")},
177
+ "path": "/handoff/targetSdk"},
178
+ "stateWarnings": handoff.get("stateWarnings", []),
179
+ "queryRootEvidence": {"boundary": "Report columns are DataSet output, not persisted query roots; external entities require their own verified contracts."},
180
+ "evidenceScope": "Design and source facts only; compilation, deployment, filter value classes and runtime behavior are unverified.",
181
+ }
182
+ if not result["targetSdk"]["status"]:
183
+ result["targetSdk"] = {"status": "not-provided"}
184
+ if operation == "coding-start":
185
+ result["buildEnvironment"] = inspect_compile_environment(handoff["projectRoot"]) if handoff.get("projectRoot") else {"status": "not-provided"}
186
+ limit = MAX_CODING_START_BYTES if operation == "coding-start" else MAX_CODING_CONTEXT_BYTES
187
+ if len(encoded(result).encode("utf-8")) > limit:
188
+ for key in sections:
189
+ result["reportContract"].pop(key)
190
+ result["reportContract"]["detailsOmitted"] = True
191
+ for key in ("sdkUsage", "existingMethods", "coreLogic"):
192
+ if key in result:
193
+ result[key + "Path"] = f"/handoff/{key}"
194
+ result.pop(key)
195
+ result["externalContracts"] = {"path": "/handoff/specialized/externalContracts"}
196
+ result["codingEvidence"] = {"path": "/handoff/codingEvidence"}
197
+ result["reportContract"].pop("pluginTypes", None)
198
+ result["stateWarnings"] = {"path": "/handoff/stateWarnings"}
199
+ return result
200
+
201
+
84
202
  def query(document: dict, args: argparse.Namespace) -> dict:
203
+ if args.operation == "runtime-context":
204
+ from test_environment import plugin_runtime_context
205
+ handoff = document["handoff"]
206
+ if not handoff.get("appRoot") or not handoff.get("formNumber"):
207
+ raise QueryError("HANDOFF_RUNTIME_TARGET_MISSING", "runtime-context requires appRoot and formNumber")
208
+ return {"status": True, "contextMode": "runtime-context", "runtime": plugin_runtime_context(
209
+ handoff["appRoot"], handoff["formNumber"], resolve_entry=getattr(args, "resolve_entry", False))}
85
210
  if args.operation in ("coding-context", "coding-start"):
86
211
  handoff = document["handoff"]
87
212
  graph = handoff.get("formGraph")
88
213
  evidence = handoff.get("codingEvidence")
89
- if not isinstance(graph, dict) or not isinstance(evidence, dict):
90
- raise QueryError("HANDOFF_CODING_CONTEXT_INVALID", "formGraph and codingEvidence are required")
214
+ if not isinstance(evidence, dict):
215
+ raise QueryError("HANDOFF_CODING_CONTEXT_INVALID", "codingEvidence is required")
216
+ needs = handoff.get("metadataNeeds")
217
+ if needs is not None and not (handoff.get("type") in ("reportlist", "report-plugin", "reportform", "reporttreelist") and isinstance(handoff.get("reportContract"), dict)):
218
+ if not isinstance(needs, list) or any(not isinstance(item, str) or item not in {"form", "list", "entity", "operate"} for item in needs):
219
+ raise QueryError("HANDOFF_CODING_CONTEXT_INVALID", "metadataNeeds is invalid")
220
+ if "entity" in needs and not isinstance(graph, dict):
221
+ raise QueryError("HANDOFF_CODING_CONTEXT_INVALID", "selected entity metadata is missing")
222
+ if ({"form", "list"} & set(needs)) and not isinstance(handoff.get("uiContext"), dict):
223
+ raise QueryError("HANDOFF_CODING_CONTEXT_INVALID", "selected page metadata is missing")
224
+ if "operate" in needs and "operationContracts" not in handoff and "availableOperations" not in handoff:
225
+ raise QueryError("HANDOFF_CODING_CONTEXT_INVALID", "selected operation metadata is missing")
226
+ if handoff.get("type") in ("reportlist", "report-plugin", "reportform", "reporttreelist") and isinstance(handoff.get("reportContract"), dict):
227
+ return report_coding_context(document, args.operation)
228
+ ui = handoff.get("uiContext")
229
+ source = handoff.get("sourceContext")
230
+ operation_context = handoff.get("operationContext")
231
+ operation_only = (isinstance(operation_context, dict) and operation_context.get("source") == "listOperations"
232
+ and operation_context.get("formNumber") == handoff.get("formNumber")
233
+ and handoff.get("type") == "operation" and bool(handoff.get("operationKeys")))
234
+ source_only = (isinstance(source, dict) and source.get("scope") == "source-only"
235
+ and source.get("sourcePath") == handoff.get("sourcePath")
236
+ and handoff.get("ownershipMode") == "modify" and handoff.get("type") != "operation")
237
+ if not isinstance(graph, dict) and not source_only and not operation_only and not (isinstance(ui, dict) and ui.get("formNumber") == handoff.get("formNumber") and ui.get("source") == "getFormMetadata"):
238
+ raise QueryError("HANDOFF_CODING_CONTEXT_INVALID", "verified page context, source context or formGraph is required")
239
+ if not isinstance(graph, dict):
240
+ graph = {"entities": []}
91
241
  if not isinstance(handoff.get("relatedForms", []), list) or not isinstance(evidence.get("apiContracts", []), list):
92
242
  raise QueryError("HANDOFF_CODING_CONTEXT_INVALID", "relatedForms and apiContracts must be arrays")
93
243
 
@@ -110,12 +260,20 @@ def query(document: dict, args: argparse.Namespace) -> dict:
110
260
  start = {
111
261
  "status": True, "contextMode": "coding-start", "handoffPath": document["handoffPath"],
112
262
  "handoffSha256": hashlib.sha256(Path(document["handoffPath"]).read_bytes()).hexdigest(),
113
- **pick(handoff, ("appRoot", "projectRoot", "formNumber", "className", "sourcePath", "fqcn", "operationKeys", "coreLogic", "sourceGate")),
114
- "entities": [{**pick(entity, ("entityKey", "role", "collectionProperty", "collectionPath")),
263
+ "compileCommand": [sys.executable, str(Path(__file__).with_name("compile_plugin_source.py")), "--handoff", document["handoffPath"]],
264
+ **({"runtimeContextCommand": [sys.executable, str(Path(__file__).resolve()), "runtime-context", "--handoff", document["handoffPath"]]} if not source_only else {}),
265
+ **({"testCommand": [sys.executable, str(Path(__file__).with_name("run_java_tests.py")), "--project-root", handoff["projectRoot"]]} if handoff.get("projectRoot") else {}),
266
+ **pick(handoff, ("appRoot", "projectRoot", "formNumber", "className", "sourcePath", "fqcn", "operationKeys", "coreLogic", "sourceGate", "metadataNeeds")),
267
+ **observed_source_gate(handoff),
268
+ **({"uiContext": ui} if isinstance(ui, dict) else {}),
269
+ **({"sourceContext": source} if source_only else {}),
270
+ **({"operationContext": operation_context} if operation_only else {}),
271
+ **({"entities": [{**pick(entity, ("entityKey", "role", "collectionProperty", "collectionPath")),
115
272
  "fieldsPath": f"/handoff/formGraph/entities/{index}/fields",
116
- "fields": [pick(field, ("fieldKey", "fieldName", "fieldType", "javaType", "valueAccessor", "idAccessor"))
273
+ "fields": [pick(field, ("fieldKey", "fieldName", "fieldType", "javaType", "valueAccessor", "idAccessor", "timeValueUnit", "note"))
117
274
  for field in entity.get("fields", [])]}
118
- for index, entity in enumerate(graph["entities"])],
275
+ for index, entity in enumerate(graph["entities"])]}
276
+ if isinstance(handoff.get("formGraph"), dict) else {"entityEvidenceScope": "Entity fields and query roots were not requested and are unverified."}),
119
277
  "relatedForms": [{**pick(form, ("formNumber", "formName", "metadataVersion")),
120
278
  "path": f"/handoff/relatedForms/{index}"}
121
279
  for index, form in enumerate(handoff.get("relatedForms", []))],
@@ -127,24 +285,40 @@ def query(document: dict, args: argparse.Namespace) -> dict:
127
285
  for row in handoff.get("availableOperations", [])
128
286
  if isinstance(row, dict) and row.get("operationKey") in handoff.get("operationKeys", [])],
129
287
  "stateWarnings": handoff.get("stateWarnings", []),
130
- "stateFields": [pick(field, ("fieldKey", "fieldName", "entityKey", "comboOptions")) for field in handoff.get("stateFields", [])],
288
+ "stateFields": [pick(field, ("fieldKey", "fieldName", "entityKey", "comboOptions"))
289
+ for field in handoff.get("stateFields", [])
290
+ if field.get("entityKey") in {entity.get("entityKey") for entity in graph["entities"]}],
291
+ "stateFieldsScope": "Main form only; related-form values remain at /handoff/stateFields",
131
292
  "buildEnvironment": inspect_compile_environment(handoff["projectRoot"]) if handoff.get("projectRoot") else {"status": "not-provided"},
132
293
  "targetSdk": target_sdk(),
133
- "codingEvidence": {**pick(evidence, ("codingEntry", "operationDispatch", "apiLookup", "sdkLookup")),
294
+ "codingEvidence": {**pick(evidence, ("codingEntry", "operationDispatch", "apiLookup", "sdkLookup", "scenes", "standards", "specializedReferences")),
134
295
  "signatureTypes": [pick(item, ("class", "import")) for item in evidence.get("signatureTypes", [])],
135
296
  "signatureTypesPath": "/handoff/codingEvidence/signatureTypes",
297
+ "guideSignatures": evidence.get("guideSignatures", []),
136
298
  **({"guide": evidence["guide"]} if isinstance(evidence.get("guide"), str) and len(evidence["guide"]) <= 512 else {}),
137
- "apiContracts": [{**pick(contract, ("class",)),
299
+ "apiContracts": [{**pick(contract, ("class", "import")),
138
300
  "path": f"/handoff/codingEvidence/apiContracts/{index}",
139
- "members": list(dict.fromkeys(member["name"] for member in contract.get("members", []) if "name" in member))}
301
+ "members": list(dict.fromkeys(member["name"] for member in contract.get("members", []) if "name" in member)),
302
+ "signatures": [pick(member, ("name", "signature", "declaredBy", "deprecated")) for member in contract.get("members", [])]}
140
303
  for index, contract in enumerate(evidence.get("apiContracts", []))]},
304
+ "specialized": pick(handoff.get("specialized", {}), ("hitModules", "constraints")),
141
305
  "remainingContext": {"stateFields": "/handoff/stateFields", "specialized": "/handoff/specialized", "reportContract": "/handoff/reportContract"},
142
306
  "evidenceScope": "Startup facts and pointers only; collection membership is not proof of a standalone query root, and source compilation does not establish business coverage",
143
307
  }
144
308
  if len(encoded(start).encode("utf-8")) > MAX_CODING_START_BYTES:
145
- for entity in start["entities"]:
309
+ for entity in start.get("entities", []):
146
310
  entity["fieldCount"] = len(entity.pop("fields"))
147
311
  entity["fieldsOmitted"] = True
312
+ if len(encoded(start).encode("utf-8")) > MAX_CODING_START_BYTES and isinstance(start.get("uiContext"), dict):
313
+ page = start["uiContext"]
314
+ start["uiContext"] = {**pick(page, ("formNumber", "formName", "modelType", "source")),
315
+ "controlsPath": "/handoff/uiContext/controls", "controlCount": len(page.get("controls", [])),
316
+ "pluginsPath": "/handoff/uiContext/plugins", "pluginCount": len(page.get("plugins", []))}
317
+ if len(encoded(start).encode("utf-8")) > MAX_CODING_START_BYTES:
318
+ for contract in start["codingEvidence"]["apiContracts"]:
319
+ contract["signaturesOmitted"] = True
320
+ contract.pop("signatures", None)
321
+ start["codingEvidence"].pop("guideSignatures", None)
148
322
  return start
149
323
 
150
324
  def compact_graph(value: dict) -> dict:
@@ -163,27 +337,31 @@ def query(document: dict, args: argparse.Namespace) -> dict:
163
337
  ],
164
338
  }
165
339
 
166
- return {
340
+ context = {
167
341
  "status": True,
168
342
  "contextMode": "coding-context",
169
343
  "handoffPath": document["handoffPath"],
170
344
  "handoffSha256": hashlib.sha256(Path(document["handoffPath"]).read_bytes()).hexdigest(),
171
- **pick(handoff, ("appRoot", "projectRoot", "formNumber", "className", "sourcePath", "fqcn", "operationKeys", "coreLogic", "specialized", "sourceGate")),
172
- "formGraph": compact_graph(graph),
345
+ **pick(handoff, ("appRoot", "projectRoot", "formNumber", "className", "sourcePath", "fqcn", "operationKeys", "coreLogic", "specialized", "sourceGate", "metadataNeeds")),
346
+ **observed_source_gate(handoff),
347
+ **({"formGraph": compact_graph(graph)} if isinstance(handoff.get("formGraph"), dict) else {}),
348
+ **({"uiContext": ui} if isinstance(ui, dict) else {}),
349
+ **({"sourceContext": source} if source_only else {}),
350
+ **({"operationContext": operation_context} if operation_only else {}),
173
351
  "targetSdk": target_sdk(),
174
- "queryRootEvidence": {
352
+ **({"queryRootEvidence": {
175
353
  "formNumber": graph.get("formNumber"),
176
354
  "nonRootEntityKeys": [entity["entityKey"] for entity in graph["entities"]
177
355
  if entity.get("role") in ("entries", "subentries") and isinstance(entity.get("entityKey"), str)],
178
356
  "boundary": "EntryType and SubEntryType are collection members here, not evidence of standalone QueryServiceHelper roots; runtime query behavior is unverified by this snapshot.",
179
- },
357
+ }} if isinstance(handoff.get("formGraph"), dict) else {"entityEvidenceScope": "Entity fields and query roots were not requested and are unverified."}),
180
358
  "relatedForms": [compact_graph(form) for form in handoff.get("relatedForms", [])],
181
359
  "operationContracts": handoff.get("operationContracts", []),
182
360
  "availableOperations": handoff.get("availableOperations", []),
183
361
  "stateFields": handoff.get("stateFields", []),
184
362
  "stateWarnings": handoff.get("stateWarnings", []),
185
363
  "codingEvidence": {
186
- **pick(evidence, ("codingEntry", "signatureTypes", "operationDispatch", "apiLookup", "sdkLookup")),
364
+ **pick(evidence, ("codingEntry", "signatureTypes", "operationDispatch", "apiLookup", "sdkLookup", "scenes", "standards", "specializedReferences")),
187
365
  **({"guide": evidence["guide"]} if isinstance(evidence.get("guide"), str) and len(evidence["guide"]) <= 512 else {}),
188
366
  "apiContracts": [
189
367
  {
@@ -194,6 +372,12 @@ def query(document: dict, args: argparse.Namespace) -> dict:
194
372
  ],
195
373
  },
196
374
  }
375
+ if len(encoded(context).encode("utf-8")) > MAX_CODING_CONTEXT_BYTES and isinstance(context.get("uiContext"), dict):
376
+ page = context["uiContext"]
377
+ context["uiContext"] = {**pick(page, ("formNumber", "formName", "modelType", "source")),
378
+ "controlsPath": "/handoff/uiContext/controls", "controlCount": len(page.get("controls", [])),
379
+ "pluginsPath": "/handoff/uiContext/plugins", "pluginCount": len(page.get("plugins", []))}
380
+ return context
197
381
  value = resolve_pointer(document, args.path)
198
382
  if args.operation == "get":
199
383
  return {"status": True, "path": args.path, "value": value}
@@ -216,22 +400,25 @@ def query(document: dict, args: argparse.Namespace) -> dict:
216
400
 
217
401
  def main() -> int:
218
402
  parser = argparse.ArgumentParser(description=__doc__)
219
- parser.add_argument("operation", choices=("index", "find", "get", "coding-context", "coding-start"))
403
+ parser.add_argument("operation", choices=("index", "find", "get", "coding-context", "coding-start", "runtime-context"))
220
404
  parser.add_argument("--handoff", required=True, help="Exact handoffPath returned by prepare_plugin_handoff.py")
221
405
  parser.add_argument("--path", default="", help="JSON pointer, such as /handoff/formGraph/entities")
222
406
  parser.add_argument("--offset", type=int, default=0, help="First child for index")
223
407
  parser.add_argument("--limit", type=int, default=DEFAULT_PAGE_SIZE, help="Children per index page")
224
408
  parser.add_argument("--field", help="Direct object field for exact find")
225
409
  parser.add_argument("--equals", help="Exact value for find")
410
+ parser.add_argument("--resolve-entry", action="store_true", help="Resolve a missing runtime entry through the bound platform")
226
411
  args = parser.parse_args()
227
412
  try:
413
+ if args.resolve_entry and args.operation != "runtime-context":
414
+ raise QueryError("HANDOFF_RUNTIME_ARGUMENT_INVALID", "--resolve-entry applies only to runtime-context")
228
415
  if args.offset < 0 or not 1 <= args.limit <= MAX_PAGE_SIZE:
229
416
  raise QueryError("HANDOFF_PAGE_INVALID", "offset must be nonnegative and limit must be 1–50")
230
417
  if args.operation == "find" and (not args.field or args.equals is None or not args.path):
231
418
  raise QueryError("HANDOFF_FIND_INVALID", "find requires --path, --field and --equals")
232
419
  if args.operation != "find" and (args.field is not None or args.equals is not None):
233
420
  raise QueryError("HANDOFF_FIND_INVALID", "--field and --equals apply only to find")
234
- if args.operation in ("coding-context", "coding-start") and (args.path or args.offset or args.limit != DEFAULT_PAGE_SIZE):
421
+ if args.operation in ("coding-context", "coding-start", "runtime-context") and (args.path or args.offset or args.limit != DEFAULT_PAGE_SIZE):
235
422
  raise QueryError("HANDOFF_CODING_CONTEXT_ARGUMENT_INVALID", "coding-context accepts only --handoff")
236
423
  _, document = load_handoff(args.handoff)
237
424
  response = query(document, args)
@@ -78,11 +78,16 @@ def _load_type_owners(root_dir):
78
78
  _REGISTRARS = {
79
79
  'report-pipeline': (
80
80
  'report pipeline (registerReportPlugin)',
81
- 'Type {type} is registered by the report pipeline (cosmic_api.py registerReportPlugin) '
82
- 'into the report metadata ReportQueryPlugin slot, not by this API. This script neither '
83
- 'registered nor verified it: plugin/queryEditable does not return report query plugins. '
84
- 'The evidence is the report read-back — getReportSchema with design-state evidence, where '
85
- '<ReportQueryPlugin> carries the registered class.',
81
+ 'Type {type} is not registered by this API. The report pipeline registers report plugins '
82
+ 'by slot: a reportlist plugin through registerReportPlugin into the report list\'s '
83
+ 'ReportQueryPlugin; a reporttreelist plugin goes into the left tree\'s ReportTreeAp.ReportPlugin '
84
+ 'through setReportProperties --target tree, once a user has added the report tree control. '
85
+ 'A reportform plugin is the report form\'s own form plugin and '
86
+ 'is registered by this script instead, with targetType BILL_FORM. This script neither '
87
+ 'registered nor verified a {type} plugin: '
88
+ 'plugin/queryEditable does not return report query plugins. The evidence is the report '
89
+ 'read-back — getReportSchema with design-state evidence, where <ReportQueryPlugin> carries '
90
+ 'the registered class, and setReportProperties --target tree for the left tree.',
86
91
  ),
87
92
  'platform-class-discovery': (
88
93
  'platform class discovery',
@@ -404,9 +409,9 @@ def main():
404
409
  code, _, message = str(exc).partition(':')
405
410
  print(json.dumps(_failure(code, message.strip(), writeAttempted=False), ensure_ascii=False))
406
411
  return 2
407
- server = _load_config(root_dir)
412
+ server = _load_config(root_dir) if any(type_map.get(c.get('type')) for c in candidates) else None
408
413
 
409
- if not server:
414
+ if not server and any(type_map.get(c.get('type')) for c in candidates):
410
415
  print(json.dumps(_failure(
411
416
  'PLUGIN_SERVER_CONFIG_UNAVAILABLE',
412
417
  '~/.kd/config.json not configured or load failed (kd_env_loader)',
@@ -446,7 +451,9 @@ def main():
446
451
  results.append({
447
452
  'className': c['className'],
448
453
  'type': plugin_type,
449
- 'success': True,
454
+ 'success': False,
455
+ 'status': 'routed',
456
+ 'deferred': True,
450
457
  # 与本脚本真正执行过的注册区分开,避免调用方把 success=true 读成
451
458
  # 「已在此处注册并校验」。
452
459
  'registeredHere': False,
@@ -513,6 +520,9 @@ def main():
513
520
  'success': False,
514
521
  'message': str(e),
515
522
  }
523
+ result['registeredHere'] = result['success']
524
+ result['verifiedHere'] = False
525
+ result['formNumber'] = form_number
516
526
  if not result['success']:
517
527
  has_failure = True
518
528
  else:
@@ -543,13 +553,20 @@ def main():
543
553
  if verification['status'] != 'pass':
544
554
  has_failure = True
545
555
 
556
+ if verification and verification.get('checks'):
557
+ for result in results:
558
+ result['verifiedHere'] = any(check.get('verified') and _registration_identity(result) ==
559
+ {key: check[key] for key in ('formNumber', 'className', 'targetType', 'operationKey')}
560
+ for check in verification['checks'])
561
+ deferred = sum(1 for result in results if result.get('deferred'))
546
562
  output = {
547
- 'status': 'failed' if has_failure else 'success',
563
+ 'status': 'failed' if has_failure else 'routed' if deferred == len(results) else 'partial' if deferred else 'success',
548
564
  'results': results,
549
565
  'summary': {
550
566
  'total': len(results),
551
567
  'success': sum(1 for r in results if r['success']),
552
- 'failed': sum(1 for r in results if not r['success']),
568
+ 'failed': sum(1 for r in results if not r['success'] and not r.get('deferred')),
569
+ **({'deferred': deferred} if deferred else {}),
553
570
  },
554
571
  }
555
572
  if source_checks:
@@ -17,6 +17,7 @@ import json
17
17
  import os
18
18
  import subprocess
19
19
  import sys
20
+ from datetime import datetime, timezone
20
21
  from pathlib import Path
21
22
  from typing import Any
22
23
  from urllib.parse import parse_qs, urlparse
@@ -26,10 +27,12 @@ if str(SCRIPT_DIR) not in sys.path:
26
27
  sys.path.insert(0, str(SCRIPT_DIR))
27
28
 
28
29
  from preview_url import PreviewUrlError, get_form_name, get_preview_url
30
+ from _http_client import _is_transient
29
31
  from test_environment import export_test_environment
30
32
 
31
33
  _DYM_BACKGROUND_NOTICE = None
32
34
  _TEST_ENVIRONMENT_HANDOFF = None
35
+ _PREVIEW_EVIDENCE = None
33
36
 
34
37
 
35
38
  class ReportPluginSourceMissing(Exception):
@@ -224,6 +227,10 @@ def undelivered_report_plugins(
224
227
  except (OSError, ValueError):
225
228
  # 契约读不出来,不能当作插件缺失的证据。
226
229
  continue
230
+ # 类要按哪种类型交付,由注册它的槽位决定。pluginTypes 出现之前写的契约只注册取数插件。
231
+ plugin_types = contract.get("pluginTypes")
232
+ if not isinstance(plugin_types, dict):
233
+ plugin_types = {}
227
234
  for class_name in contract.get("pluginClasses") or []:
228
235
  registered = str(class_name or "").strip()
229
236
  if not registered:
@@ -234,6 +241,7 @@ def undelivered_report_plugins(
234
241
  missing.append({
235
242
  "formNumber": number,
236
243
  "className": registered,
244
+ "type": str(plugin_types.get(registered) or "reportlist"),
237
245
  "expectedSource": "src/main/java/" + registered.replace(".", "/") + ".java",
238
246
  "reason": (
239
247
  "no Java source recorded in archive.json" if not recorded
@@ -251,12 +259,13 @@ def _refuse_undelivered(missing: list[dict[str, str]]) -> None:
251
259
  for item in missing:
252
260
  lines.append(
253
261
  f" {item['formNumber']} registered {item['className']} "
254
- f"({item['reason']}); expected source {item['expectedSource']}"
262
+ f"({item['reason']}); deliver it as type {item.get('type') or 'reportlist'}, "
263
+ f"expected source {item['expectedSource']}"
255
264
  )
256
265
  lines.append(
257
- " Registration is metadata only. Deliver the plugin through "
258
- "references/java-plugin-delivery.md as type reportlist — generate the source, compile it "
259
- "narrowly, and let it reach archive.json — then render the cards."
266
+ " Registration is metadata only. Deliver each plugin through "
267
+ "references/java-plugin-delivery.md as the type named for it — generate the source, "
268
+ "compile it narrowly, and let it reach archive.json — then render the cards."
260
269
  )
261
270
  raise ReportPluginSourceMissing("\n".join(lines))
262
271
 
@@ -279,14 +288,19 @@ def _join_url(base: str, path: str) -> str:
279
288
  return base + path
280
289
 
281
290
 
282
- def _archived_preview_urls(artifacts: list[Any]) -> dict[str, str]:
291
+ def _archived_preview_urls(artifacts: list[Any], base_url: str = "") -> dict[str, str]:
283
292
  """formNumber -> 早前成功运行归档的预览链接(getPreviewUrl 失败时兜底)。"""
284
293
  urls: dict[str, str] = {}
285
294
  for artifact in artifacts:
286
295
  if not isinstance(artifact, dict):
287
296
  continue
288
297
  raw_url = str(artifact.get("url") or "").strip()
289
- if not raw_url:
298
+ parsed = urlparse(raw_url)
299
+ base = urlparse(base_url)
300
+ if not raw_url or parsed.scheme not in ("http", "https") or not parsed.netloc or parsed.username or parsed.password:
301
+ continue
302
+ if base_url and (parsed.scheme != base.scheme or parsed.netloc != base.netloc or
303
+ not (parsed.path == base.path or parsed.path.startswith(base.path.rstrip("/") + "/"))):
290
304
  continue
291
305
  form_number = _artifact_form_number(artifact)
292
306
  if form_number:
@@ -346,8 +360,9 @@ def render(
346
360
  ) -> list[str]:
347
361
  global _DYM_BACKGROUND_NOTICE
348
362
  _DYM_BACKGROUND_NOTICE = None
349
- global _TEST_ENVIRONMENT_HANDOFF
363
+ global _TEST_ENVIRONMENT_HANDOFF, _PREVIEW_EVIDENCE
350
364
  _TEST_ENVIRONMENT_HANDOFF = None
365
+ _PREVIEW_EVIDENCE = {}
351
366
  archive_path = Path(app_root) / "archive.json"
352
367
  if not archive_path.exists():
353
368
  print(f"Warning: {archive_path} does not exist, skipping kdform cards", file=sys.stderr)
@@ -420,7 +435,7 @@ def render(
420
435
  _TEST_ENVIRONMENT_HANDOFF = export_test_environment(
421
436
  app_root, server_config, [number for number, _title in form_entries]
422
437
  )
423
- archived_urls = _archived_preview_urls(artifacts)
438
+ archived_urls = _archived_preview_urls(artifacts, base_url)
424
439
  # 子集渲染不能清掉其他表单已经归档的卡片。
425
440
  requested_set = {fn for fn, _title in form_entries}
426
441
  artifacts = [
@@ -432,34 +447,46 @@ def render(
432
447
 
433
448
  for fn, title in form_entries:
434
449
  if title == fn:
435
- title = get_form_name(server_config, base_url, fn) or title
450
+ try:
451
+ title = get_form_name(server_config, base_url, fn) or title
452
+ except Exception:
453
+ # 名称读取失败只降级标题,不阻断目标报表卡片。
454
+ print(f"Warning: form name lookup failed for {fn}; using its exact form number", file=sys.stderr)
436
455
  api_ids = api_ids_map.get(fn, [])
437
456
  if isinstance(api_ids, list):
438
457
  api_ids_str = ",".join(str(i) for i in api_ids)
439
458
  else:
440
459
  api_ids_str = str(api_ids)
441
460
 
461
+ preview = {"status": "fresh", "runtimeVerified": False,
462
+ "checkedAt": datetime.now(timezone.utc).isoformat()}
442
463
  try:
443
464
  url = get_preview_url(server_config, base_url, fn)
444
465
  except Exception as e:
445
466
  if "FORM_NOT_FOUND" in str(e).upper():
446
467
  deleted_numbers.append(fn)
447
468
  print(
448
- f"提示:{fn} 已不存在;已移除归档卡片且不生成替代卡片",
469
+ f"Notice: {fn} no longer exists; removed its archived card and emitted no replacement",
449
470
  file=sys.stderr,
450
471
  )
451
472
  continue
452
473
  # getPreviewUrl 瞬时失败不能丢掉整个回复的卡片。
453
- url = archived_urls.get(fn, "")
454
- fallback = "reusing archived preview link" if url else "rendering card without preview link"
455
- print(f"Warning: getPreviewUrl({fn}) failed ({e}); {fallback}", file=sys.stderr)
474
+ http_status = getattr(e, "code", None)
475
+ url = archived_urls.get(fn, "") if _is_transient(e) else ""
476
+ preview.update(status="archived" if url else "unavailable", errorType=type(e).__name__)
477
+ if isinstance(http_status, int):
478
+ preview["httpStatus"] = http_status
479
+ preview["retryable"] = _is_transient(e)
480
+ fallback = "reusing archived preview link (not a fresh preview verification)" if url else "rendering card without preview link"
481
+ print(f"Warning: getPreviewUrl({fn}) failed; {fallback}", file=sys.stderr)
456
482
  # 平台忽略 viewType=list 时,由已验证列表元数据恢复标准 bos_list 入口。
457
483
  try:
458
484
  list_url = get_preview_url(
459
485
  server_config, base_url, fn, view_type="list", form_preview_url=url,
460
- )
461
- except Exception:
486
+ ) if preview["status"] == "fresh" else ""
487
+ except Exception as exc:
462
488
  list_url = ""
489
+ preview["listErrorType"] = type(exc).__name__
463
490
  if list_url and list_url == url:
464
491
  print(
465
492
  f"Warning: getPreviewUrl({fn}, viewType=list) returned the editor URL; omitted listUrl",
@@ -474,6 +501,8 @@ def render(
474
501
  card = {"title": title, "url": url, "metadata": metadata, "plugins": plugins, "designer": designer}
475
502
  if list_url:
476
503
  card["listUrl"] = list_url
504
+ preview["listStatus"] = "fresh" if list_url else "unavailable" if preview["status"] == "fresh" else "not-run"
505
+ _PREVIEW_EVIDENCE[fn] = preview
477
506
  browser_entries[fn] = {"formName": title, "previewUrl": url, "listUrl": list_url}
478
507
  if api:
479
508
  card["api"] = api
@@ -489,11 +518,14 @@ def render(
489
518
  )
490
519
 
491
520
  archive["artifacts"] = artifacts
521
+ previous_evidence = archive.get("previewEvidence")
522
+ archive["previewEvidence"] = {**{key: value for key, value in (previous_evidence.items() if isinstance(previous_evidence, dict) else [])
523
+ if key not in {number for number, _title in form_entries}}, **_PREVIEW_EVIDENCE}
492
524
  _write_json(archive_path, archive)
493
525
 
494
526
  if requested_numbers and set(deleted_numbers) == set(requested_numbers):
495
527
  raise DeletedArtifactNoCard(
496
- "DELIVERY_NOT_APPLICABLE: requested forms were deleted; no artifact card was emitted"
528
+ "DELIVERY_NOT_APPLICABLE: every requested form was deleted; no artifact card was emitted"
497
529
  )
498
530
 
499
531
  if blocks and not wait_dym:
@@ -510,8 +542,9 @@ def render(
510
542
 
511
543
 
512
544
  def main() -> int:
513
- global _TEST_ENVIRONMENT_HANDOFF
545
+ global _TEST_ENVIRONMENT_HANDOFF, _PREVIEW_EVIDENCE
514
546
  _TEST_ENVIRONMENT_HANDOFF = None
547
+ _PREVIEW_EVIDENCE = None
515
548
  parser = argparse.ArgumentParser(description="Render output cards")
516
549
  parser.add_argument("--app-root", required=True, help="App root directory")
517
550
  parser.add_argument("--wait-dym", action="store_true",
@@ -572,6 +605,8 @@ def main() -> int:
572
605
  print(block, flush=True)
573
606
  if _TEST_ENVIRONMENT_HANDOFF:
574
607
  print("TEST_ENVIRONMENT_HANDOFF=" + json.dumps(_TEST_ENVIRONMENT_HANDOFF, ensure_ascii=False), flush=True)
608
+ if _PREVIEW_EVIDENCE:
609
+ print("PREVIEW_EVIDENCE=" + json.dumps(_PREVIEW_EVIDENCE, ensure_ascii=False), flush=True)
575
610
  if _DYM_BACKGROUND_NOTICE:
576
611
  # 后台同步是必须交给宿主执行的动作,不是诊断日志。放在卡片块后的
577
612
  # stdout,避免调用方隐藏 stderr 时把同步请求一起丢弃。
@@ -580,9 +615,11 @@ def main() -> int:
580
615
  print(
581
616
  "DELIVERY: Preserve the exact blocks for the final delivery response; do not rerender them. "
582
617
  "These are domain artifacts, not evidence that the whole request or acceptance is complete. "
583
- "For delegated implementation, return the blocks and actual results to the owner and continue "
584
- "its remaining acceptance work. At whole-request finalization, call output_check once if "
585
- "available, then include the blocks without Markdown links or code fences.",
618
+ "Formal Expert Team completion reuses the native cards saved in archive.json and publishes "
619
+ "them at the assigned range's successful endpoint; do not repeat host-published cards. "
620
+ "For other delegated implementation, return the blocks and actual results to the owner. "
621
+ "Standalone finalization calls output_check once if available, then includes the blocks "
622
+ "without Markdown links or code fences.",
586
623
  file=sys.stderr,
587
624
  flush=True,
588
625
  )