@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.
- package/package.json +1 -1
- package/skills/cosmic-build/LOCAL-PATCHES.md +43 -14
- package/skills/cosmic-build/SKILL.md +24 -9
- package/skills/cosmic-build/examples/java-rules/CosmicReportAggregation.java +26 -17
- package/skills/cosmic-build/examples/java-rules/CosmicReportFilters.java +96 -2
- package/skills/cosmic-build/knowledge/cosmic/arch/security/security_standard.md +4 -4
- package/skills/cosmic-build/knowledge/cosmic/platform/allimport.md +2 -1
- package/skills/cosmic-build/knowledge/cosmic/platform/core/dynamic-object-collection.md +6 -4
- package/skills/cosmic-build/knowledge/cosmic/platform/core/model.md +9 -9
- package/skills/cosmic-build/knowledge/cosmic/platform/core/view.md +5 -5
- package/skills/cosmic-build/knowledge/cosmic/platform/form-plugin.md +8 -4
- package/skills/cosmic-build/knowledge/cosmic/platform/list-plugin.md +5 -3
- package/skills/cosmic-build/knowledge/cosmic/platform/print-plugin-guide.md +1 -0
- package/skills/cosmic-build/knowledge/cosmic/platform/report-plugin.md +269 -57
- package/skills/cosmic-build/knowledge/cosmic/platform/reportlistdata-plugin.md +130 -23
- package/skills/cosmic-build/knowledge/cosmic/platform/reporttreedata-plugin.md +48 -28
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/_index.md +12 -3
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/common/attachment-name-process.md +17 -13
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/common/dynamic-update-field.md +4 -4
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/control/filter-grid/filter-grid-set-value.md +1 -1
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/control/list/filter-basedatafield.md +2 -3
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/control/list/filter-container-init.md +0 -1
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/control/list/filter-popup.md +0 -1
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/control/list/filter-value-parse.md +5 -6
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/control/list/list-show-form.md +1 -1
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/control/list/query-field-dynamic.md +0 -7
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/data-access/business-data/delete-service.md +24 -29
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/data-access/ksql/db-batch.md +4 -0
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/data-access/ksql/db-direct-access.md +4 -0
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/data-access/qfilter/entity-object-access.md +8 -3
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/data-access/qfilter/qfilter-compose.md +6 -5
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/event/after-bind-data-set-visible.md +5 -1
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/event/click-return-data.md +2 -2
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/event/closed-call-back-set-value.md +2 -2
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/event/register-listener.md +1 -1
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/event/set-filter-tab-org.md +3 -0
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/infra/dispatch-service/microservice-dev.md +18 -11
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/infra/dist-lock/dist-lock-basic.md +3 -6
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/infra/mq/mq-subscribe.md +5 -1
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/infra/thread-pool/async-operation.md +10 -6
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/infra/thread-pool/big-task-async.md +4 -2
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/infra/thread-pool/schedule-task.md +9 -7
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/operation/operation-interactive.md +33 -34
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/operation/operation-plugin-events.md +30 -22
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/report/report-async-query.md +133 -0
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/report/report-batch-query.md +26 -22
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/report/report-chart.md +112 -0
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/report/report-dataset-assembly.md +200 -0
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/report/report-head-filter.md +177 -0
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/report/report-hyperlink-drill.md +216 -0
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/report/report-list-data-query.md +12 -12
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/report/report-merge-column.md +44 -6
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/report/report-print-data.md +163 -0
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/report/report-row-data-toolbar.md +188 -0
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/report/report-subtotal-total.md +139 -0
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/report/report-tree-data-query.md +108 -12
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/report/report-tree-list.md +181 -0
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/system-service/report-plugin-interface.md +3 -0
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/tree/tree-basedata-cascade-confirm.md +14 -11
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/tree/tree-list-before-item-validate.md +1 -1
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/tree/tree-list-build-filter.md +1 -1
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/tree/tree-list-multi-level-filter.md +11 -11
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/tree/tree-list-refresh-node.md +22 -15
- package/skills/cosmic-build/knowledge/cosmic/platform/sdk/sdk-algo.md +2 -2
- package/skills/cosmic-build/knowledge/cosmic/platform/sdk/sdk-entity-model.md +3 -4
- package/skills/cosmic-build/knowledge/cosmic/platform/sdk/sdk-exception.md +9 -5
- package/skills/cosmic-build/knowledge/cosmic/platform/sdk/sdk-orm-access.md +4 -0
- package/skills/cosmic-build/knowledge/cosmic/platform/sdk/sdk-request-context.md +7 -3
- package/skills/cosmic-build/knowledge/cosmic/platform/standards.md +28 -3
- package/skills/cosmic-build/references/compile-guide.md +36 -1
- package/skills/cosmic-build/references/dcs-code-rules.md +180 -0
- package/skills/cosmic-build/references/delivery.md +16 -2
- package/skills/cosmic-build/references/execution-contract.md +15 -3
- package/skills/cosmic-build/references/java-plugin-delivery.md +21 -11
- package/skills/cosmic-build/references/plugin-handoff-input.md +82 -24
- package/skills/cosmic-build/references/plugin-runtime-routing.md +23 -6
- package/skills/cosmic-build/references/report-filter-contract.md +53 -15
- package/skills/cosmic-build/references/sdk-inspection.md +4 -1
- package/skills/cosmic-build/references/type-registry.json +26 -17
- package/skills/cosmic-build/schemas/data-model-authoring.schema.json +1 -1
- package/skills/cosmic-build/scripts/_http_client.py +3 -0
- package/skills/cosmic-build/scripts/compile_plugin_source.py +251 -42
- package/skills/cosmic-build/scripts/configure_list.py +112 -0
- package/skills/cosmic-build/scripts/dcs-rule-catalog.json +799 -0
- package/skills/cosmic-build/scripts/dcs_code_gate.py +1976 -0
- package/skills/cosmic-build/scripts/execution_binding.py +25 -0
- package/skills/cosmic-build/scripts/platform_api_contract.py +352 -7
- package/skills/cosmic-build/scripts/plugin_source_gate.py +9 -1
- package/skills/cosmic-build/scripts/plugin_target_sdk.py +12 -8
- package/skills/cosmic-build/scripts/prepare_plugin_handoff.py +488 -88
- package/skills/cosmic-build/scripts/query_plugin_handoff.py +207 -20
- package/skills/cosmic-build/scripts/register_plugins.py +27 -10
- package/skills/cosmic-build/scripts/render_output_cards.py +57 -20
- package/skills/cosmic-build/scripts/resolve_output_path.py +60 -26
- package/skills/cosmic-build/scripts/run_java_tests.py +162 -0
- package/skills/cosmic-build/scripts/sync_generated_locale_assets.py +28 -0
- package/skills/cosmic-build/scripts/test/fixtures/ReportSdkCheck.java +1 -22
- package/skills/cosmic-build/scripts/test/fixtures/list_column_property_declarations.json +887 -0
- package/skills/cosmic-build/scripts/test/fixtures/operation-entry-field-prepare.java +78 -0
- package/skills/cosmic-build/scripts/test/test_add_field_required_contract.py +12 -0
- package/skills/cosmic-build/scripts/test/test_batch5_drift_governance.py +23 -2
- package/skills/cosmic-build/scripts/test/test_compile_plugin_source.py +287 -4
- package/skills/cosmic-build/scripts/test/test_configure_list_delivery.py +132 -0
- package/skills/cosmic-build/scripts/test/test_cosmic_sdk_examples.py +58 -1
- package/skills/cosmic-build/scripts/test/test_data_model_contract.py +19 -0
- package/skills/cosmic-build/scripts/test/test_dcs_code_gate.py +901 -0
- package/skills/cosmic-build/scripts/test/test_delivery_stage_boundary.py +1 -1
- package/skills/cosmic-build/scripts/test/test_direct_cli_architecture.py +2 -0
- package/skills/cosmic-build/scripts/test/test_http_client.py +6 -0
- package/skills/cosmic-build/scripts/test/test_java_toolchain_feedback.py +206 -0
- package/skills/cosmic-build/scripts/test/test_kd_api.py +21 -0
- package/skills/cosmic-build/scripts/test/test_list_configuration_closure.py +184 -0
- package/skills/cosmic-build/scripts/test/test_mcp_cli.py +139 -2
- package/skills/cosmic-build/scripts/test/test_menu_creation_closure.py +16 -0
- package/skills/cosmic-build/scripts/test/test_modeling_input_recovery.py +55 -1
- package/skills/cosmic-build/scripts/test/test_open_world_cli_catalog.py +1 -1
- package/skills/cosmic-build/scripts/test/test_operation_field_preparation.py +84 -0
- package/skills/cosmic-build/scripts/test/test_platform_api_contract.py +322 -1
- package/skills/cosmic-build/scripts/test/test_plugin_handoff.py +951 -4
- package/skills/cosmic-build/scripts/test/test_plugin_source_gate.py +30 -2
- package/skills/cosmic-build/scripts/test/test_plugin_target_sdk.py +21 -0
- package/skills/cosmic-build/scripts/test/test_property_reroute.py +165 -0
- package/skills/cosmic-build/scripts/test/test_property_schema_first.py +35 -0
- package/skills/cosmic-build/scripts/test/test_query_plugin_handoff.py +221 -0
- package/skills/cosmic-build/scripts/test/test_register_plugins.py +76 -4
- package/skills/cosmic-build/scripts/test/test_render_output_cards.py +75 -3
- package/skills/cosmic-build/scripts/test/test_report_business_contract.py +27 -5
- package/skills/cosmic-build/scripts/test/test_report_doc_contract_sync.py +103 -11
- package/skills/cosmic-build/scripts/test/test_report_plugin_source_gate.py +46 -16
- package/skills/cosmic-build/scripts/test/test_report_properties.py +74 -15
- package/skills/cosmic-build/scripts/test/test_report_query_data_source.py +3 -1
- package/skills/cosmic-build/scripts/test/test_report_sdk_optimization.py +1 -2
- package/skills/cosmic-build/scripts/test/test_test_environment.py +76 -1
- package/skills/cosmic-build/scripts/test_environment.py +67 -1
- package/skills/cosmic-build/skills/cosmic-coding-impl/SKILL.md +52 -10
- package/skills/cosmic-build/skills/cosmic-meta-api/apis/ai_meta.md +16 -3
- package/skills/cosmic-build/skills/cosmic-meta-api/contracts/cli/report-write-contracts.json +59 -26
- package/skills/cosmic-build/skills/cosmic-meta-api/contracts/cli/rule-domain.json +1 -1
- package/skills/cosmic-build/skills/cosmic-meta-api/contracts/schema/report-write.schema.json +75 -18
- package/skills/cosmic-build/skills/cosmic-meta-api/modules/pdm/scripts/pdm_auto.py +39 -0
- package/skills/cosmic-build/skills/cosmic-meta-api/modules/pdm/scripts/pdm_client.py +97 -10
- package/skills/cosmic-build/skills/cosmic-meta-api/modules/verify/SKILL.md +10 -12
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/_core/cmd_apps.py +3 -1
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/_core/cmd_formactions.py +38 -29
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/_core/cmd_listui.py +400 -233
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/_core/cmd_metaops.py +87 -45
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/_core/cmd_properties.py +42 -15
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/_core/cmd_rules.py +135 -5
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/_core/config.py +1 -1
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/_core/execution_errors.py +1 -1
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/_core/formatters.py +17 -11
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/_core/property_reroute.py +15 -5
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/_core/report_ops.py +1429 -238
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/_core/validators.py +56 -28
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/cosmic_api.py +380 -134
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/field_decorator.py +1 -1
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/meta_export.py +12 -11
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/meta_verifier.py +42 -0
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_add_field_placement_closure.py +8 -8
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_build_configuration_readback.py +94 -0
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_build_receipt_resume.py +47 -1
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_metadata_unchanged_fail_closed.py +87 -0
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_report_dym_evidence.py +23 -23
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_report_entity_integrity.py +279 -4
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_report_field_guard.py +1 -1
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_report_plugin_contract.py +509 -32
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_report_readback_derivation.py +178 -1
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_report_recreate_plan.py +125 -10
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_report_serve_contract.py +208 -0
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_report_typed_filters.py +259 -50
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_rule_inventory_perf.py +29 -0
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_rule_write_closure.py +20 -0
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_run_batch_post_build_recovery.py +18 -0
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_run_batch_verification_evidence.py +35 -0
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_run_identity_guard.py +25 -0
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_time_range_rule.py +98 -0
- package/skills/cosmic-build/skills/cosmic-meta-knowledge/domain-model/report-model.md +20 -9
- package/skills/cosmic-build/skills/cosmic-meta-knowledge/generation/report-modeling-guide.md +55 -25
- package/skills/cosmic-build/skills/cosmic-meta-knowledge/property-guides/report-column-properties.md +15 -39
- package/skills/cosmic-build/skills/cosmic-meta-modeling/SKILL.md +19 -17
- package/skills/cosmic-build/skills/cosmic-meta-modeling/modules/report/SKILL.md +82 -38
- package/skills/cosmic-build/skills/cosmic-meta-modeling/modules/report/examples/golden-example-report-batch.md +64 -19
- package/skills/cosmic-build/skills/cosmic-meta-modeling/modules/report/examples/golden-example-report-plugin.md +32 -20
- package/skills/cosmic-build/skills/cosmic-meta-modeling/modules/report/examples/golden-example-report-tree.md +78 -31
- package/skills/cosmic-build/skills/cosmic-meta-modeling/modules/report/report-artifact-schema.json +2 -1
- package/skills/cosmic-build/skills/cosmic-meta-modeling/references/authoring-companion-fields.md +3 -0
- package/skills/cosmic-build/skills/cosmic-meta-modeling/references/authoring-list-configuration.md +2 -0
- package/skills/cosmic-build/skills/cosmic-meta-modeling/scripts/data_model_contract.py +3 -1
- package/skills/cosmic-build/skills/cosmic-meta-modeling/scripts/data_model_emit.py +27 -4
- package/skills/cosmic-build/skills/cosmic-meta-modeling/scripts/report_validator.py +99 -44
- package/skills/cosmic-build/skills/cosmic-meta-modeling/scripts/test_report_examples.py +31 -4
- package/skills/cosmic-build/skills/cosmic-meta-modeling/scripts/test_report_validator.py +129 -29
- package/skills/cosmic-build/skills/cosmic-meta-modifier/SKILL.md +6 -2
- package/skills/cosmic-build/skills/cosmic-meta-modifier/modules/operation/validation.md +2 -0
- package/skills/cosmic-build/skills/cosmic-meta-modifier/modules/property/SKILL.md +1 -1
- package/skills/cosmic-build/skills/cosmic-meta-modifier/modules/report-op/SKILL.md +104 -25
- package/skills/cosmic-build/skills/cosmic-meta-modifier/modules/rule/SKILL.md +9 -0
- package/skills/cosmic-build/skills/cosmic-meta-modifier/modules/verify/usage-guide.md +2 -2
- package/skills/cosmic-build/skills/cosmic-meta-modifier/references/list-filter-modification.md +39 -12
- package/skills/cosmic-build/skills/mcp-server/SKILL.md +39 -24
- package/skills/cosmic-build/skills/mcp-server/scripts/mcp_cli.py +20 -6
- package/skills/cosmic-dev-best-practices/SKILL.md +1 -1
- package/skills/cosmic-dev-best-practices/references/README.md +2 -2
- package/skills/cosmic-dev-best-practices/references/basedata-dev-convention.md +2 -2
- package/skills/cosmic-dev-best-practices/references/bill-dev-convention.md +4 -4
- package/skills/cosmic-dev-best-practices/references/design-document-convention.md +2 -2
- package/skills/cosmic-dev-best-practices/references/design-examples/basedata-design-example.md +4 -4
- package/skills/cosmic-dev-best-practices/references/design-examples/bill-design-example.md +13 -13
- package/skills/cosmic-dev-best-practices/references/elements/CustomerField.md +1 -1
- package/skills/cosmic-dev-best-practices/references/elements/README.md +1 -1
- package/skills/cosmic-dev-best-practices/references/elements/SupplierField.md +1 -1
- package/skills/cosmic-dev-best-practices/references/elements/common-field-property-conventions.md +109 -13
- package/skills/cosmic-dev-best-practices/references/layout-control-convention.md +1 -1
- package/skills/erp-analysis/references/delivery-contract.md +2 -1
- package/skills/erp-analysis/templates/AGENTS.template.md +3 -1
- package/skills/cosmic-build/assets/platform-report-filters/TypedReportFilterFields.java +0 -169
- package/skills/cosmic-build/assets/platform-report-filters/platform.patch +0 -98
- 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
|
|
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
|
-
|
|
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
|
|
234
|
-
|
|
235
|
-
|
|
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
|
|
280
|
-
registered_plugins.append({**form_row,
|
|
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
|
|
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
|
|
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 =
|
|
724
|
+
scope = _external_contract_scope(catalog, entry)
|
|
668
725
|
selectors = _contract_field_selectors(entry)
|
|
669
|
-
resolved = [
|
|
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=
|
|
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
|
-
|
|
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
|
|
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
|
|
898
|
-
|
|
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
|
-
|
|
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
|
-
|
|
915
|
-
|
|
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
|
|
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
|
-
#
|
|
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
|
-
|
|
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(
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
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
|
-
|
|
1013
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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("/", ".")
|
|
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", "
|
|
1372
|
-
"
|
|
1373
|
-
"
|
|
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;
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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
|
-
|
|
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
|
-
|
|
1442
|
-
|
|
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=
|
|
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
|
-
|
|
1454
|
-
|
|
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
|
-
|
|
1473
|
-
|
|
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
|
-
|
|
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
|
|