@shundoo-ai/dsh-cosmic 1.0.19 → 1.0.21
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/cordis.patch.yml +5 -5
- package/package.json +1 -1
- package/skills/cosmic-build/CHANGELOG.md +4 -0
- package/skills/cosmic-build/LOCAL-PATCHES.md +91 -40
- package/skills/cosmic-build/SKILL.md +46 -13
- package/skills/cosmic-build/assets/platform-report-filters/TypedReportFilterFields.java +169 -0
- package/skills/cosmic-build/assets/platform-report-filters/platform.patch +98 -0
- package/skills/cosmic-build/assets/report-algo-benchmark/ReportAlgoBenchmark.java +48 -0
- package/skills/cosmic-build/assets/report-algo-benchmark/ReportBenchmarkLogFactory.java +7 -0
- package/skills/cosmic-build/examples/java-rules/CosmicEntryQuery.java +26 -0
- package/skills/cosmic-build/examples/java-rules/CosmicReportAggregation.java +35 -0
- package/skills/cosmic-build/examples/java-rules/CosmicReportFilters.java +77 -0
- package/skills/cosmic-build/examples/java-rules/CosmicTimeAdapter.java +25 -0
- package/skills/cosmic-build/examples/java-rules/CosmicTimeAdapterCheck.java +26 -0
- package/skills/cosmic-build/knowledge/cosmic/platform/api/algo/GroupbyDataSet.md +47 -0
- package/skills/cosmic-build/knowledge/cosmic/platform/api/entity/report/FilterInfo.md +12 -12
- package/skills/cosmic-build/knowledge/cosmic/platform/api/entity/report/FilterItemInfo.md +2 -0
- package/skills/cosmic-build/knowledge/cosmic/platform/form-plugin.md +2 -4
- package/skills/cosmic-build/knowledge/cosmic/platform/java-type-contracts.md +11 -2
- package/skills/cosmic-build/knowledge/cosmic/platform/list-plugin.md +18 -14
- package/skills/cosmic-build/knowledge/cosmic/platform/operation-plugin.md +4 -4
- package/skills/cosmic-build/knowledge/cosmic/platform/reportlistdata-plugin.md +17 -9
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/operation/cross-entity-aggregate-writeback.md +2 -0
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/operation/pre-post-operation.md +15 -7
- package/skills/cosmic-build/knowledge/cosmic/platform/sdk/sdk-algo.md +24 -68
- package/skills/cosmic-build/knowledge/cosmic/platform/workflow-plugin.md +2 -7
- package/skills/cosmic-build/references/application-context.md +35 -1
- package/skills/cosmic-build/references/common-reference.md +1 -1
- package/skills/cosmic-build/references/compile-guide.md +55 -68
- package/skills/cosmic-build/references/delivery.md +59 -7
- package/skills/cosmic-build/references/execution-contract.md +38 -0
- package/skills/cosmic-build/references/form-discovery.md +7 -1
- package/skills/cosmic-build/references/java-plugin-delivery.md +32 -162
- package/skills/cosmic-build/references/operation-dispatch.md +17 -0
- package/skills/cosmic-build/references/plugin-handoff-input.md +131 -0
- package/skills/cosmic-build/references/plugin-runtime-routing.md +1 -1
- package/skills/cosmic-build/references/report-filter-contract.md +78 -0
- package/skills/cosmic-build/references/sdk-inspection.md +23 -0
- package/skills/cosmic-build/schemas/data-model-authoring.schema.json +2 -3
- package/skills/cosmic-build/scripts/_http_client.py +55 -55
- package/skills/cosmic-build/scripts/appconfig_load.py +2 -2
- package/skills/cosmic-build/scripts/apply_report_filter_extension.py +39 -0
- package/skills/cosmic-build/scripts/bootstrap_cosmic_context.py +37 -2
- package/skills/cosmic-build/scripts/compile_plugin_source.py +279 -156
- package/skills/cosmic-build/scripts/execution_binding.py +40 -0
- package/skills/cosmic-build/scripts/inspect_platform_sdk.py +169 -0
- package/skills/cosmic-build/scripts/kd_api.py +23 -22
- package/skills/cosmic-build/scripts/kd_auth.py +1 -1
- package/skills/cosmic-build/scripts/kd_env_loader.py +19 -10
- package/skills/cosmic-build/scripts/kd_token_cache.py +2 -2
- package/skills/cosmic-build/scripts/kingscript_sdk_sync.py +350 -0
- package/skills/cosmic-build/scripts/kingscript_source_gate.py +11 -8
- package/skills/cosmic-build/scripts/platform_api_contract.py +246 -11
- package/skills/cosmic-build/scripts/plugin_corpus/build_index.py +403 -0
- package/skills/cosmic-build/scripts/plugin_corpus/jvm_classfile.py +281 -0
- package/skills/cosmic-build/scripts/plugin_corpus/measure_api_usage.py +446 -0
- package/skills/cosmic-build/scripts/plugin_source_gate.py +48 -7
- package/skills/cosmic-build/scripts/plugin_target_sdk.py +45 -0
- package/skills/cosmic-build/scripts/prepare_plugin_handoff.py +493 -47
- package/skills/cosmic-build/scripts/preview_url.py +35 -6
- package/skills/cosmic-build/scripts/query_plugin_handoff.py +250 -0
- package/skills/cosmic-build/scripts/register_plugins.py +28 -2
- package/skills/cosmic-build/scripts/render_output_cards.py +42 -9
- package/skills/cosmic-build/scripts/report_algo_benchmark.py +46 -0
- package/skills/cosmic-build/scripts/run.py +2 -2
- package/skills/cosmic-build/scripts/sync_generated_locale_assets.py +43 -0
- package/skills/cosmic-build/scripts/test/fixtures/ReportSdkCheck.java +64 -0
- package/skills/cosmic-build/scripts/test/fixtures/java_context_booking_native.json +541 -0
- package/skills/cosmic-build/scripts/test/test_add_rule_business_contract.py +85 -14
- package/skills/cosmic-build/scripts/test/test_batch5_drift_governance.py +12 -3
- package/skills/cosmic-build/scripts/test/test_build_meta_order.py +0 -16
- package/skills/cosmic-build/scripts/test/test_cli_continuation.py +87 -0
- package/skills/cosmic-build/scripts/test/test_compile_plugin_source.py +224 -3
- package/skills/cosmic-build/scripts/test/test_control_authoring.py +123 -4
- package/skills/cosmic-build/scripts/test/test_cosmic_sdk_examples.py +141 -0
- package/skills/cosmic-build/scripts/test/test_data_model_contract.py +30 -5
- package/skills/cosmic-build/scripts/test/test_delivery_stage_boundary.py +2 -10
- package/skills/cosmic-build/scripts/test/test_dym_sync_background.py +11 -0
- package/skills/cosmic-build/scripts/test/test_dynamic_form_metadata_only_contract.py +59 -1
- package/skills/cosmic-build/scripts/test/test_guide_skeleton_contract.py +87 -43
- package/skills/cosmic-build/scripts/test/test_inspect_platform_sdk.py +71 -0
- package/skills/cosmic-build/scripts/test/test_kd_api.py +67 -19
- package/skills/cosmic-build/scripts/test/test_kingscript_sdk_sync.py +221 -0
- package/skills/cosmic-build/scripts/test/test_kingscript_source_gate.py +10 -0
- package/skills/cosmic-build/scripts/test/test_list_configuration_closure.py +18 -0
- package/skills/cosmic-build/scripts/test/test_long_session_regressions.py +116 -0
- package/skills/cosmic-build/scripts/test/test_mcp_cli.py +524 -0
- package/skills/cosmic-build/scripts/test/test_modeling_input_recovery.py +141 -0
- package/skills/cosmic-build/scripts/test/test_open_world_cli_catalog.py +36 -2
- package/skills/cosmic-build/scripts/test/test_platform_api_contract.py +161 -1
- package/skills/cosmic-build/scripts/test/test_plugin_business_contract.py +91 -0
- package/skills/cosmic-build/scripts/test/test_plugin_corpus.py +195 -0
- package/skills/cosmic-build/scripts/test/test_plugin_handoff.py +346 -1
- package/skills/cosmic-build/scripts/test/test_plugin_target_sdk.py +70 -0
- package/skills/cosmic-build/scripts/test/test_preview_url.py +44 -1
- package/skills/cosmic-build/scripts/test/test_property_batch.py +22 -0
- package/skills/cosmic-build/scripts/test/test_property_input_discovery.py +13 -11
- package/skills/cosmic-build/scripts/test/test_query_plugin_handoff.py +160 -0
- package/skills/cosmic-build/scripts/test/test_register_plugins.py +31 -1
- package/skills/cosmic-build/scripts/test/test_render_output_cards.py +50 -4
- package/skills/cosmic-build/scripts/test/test_report_business_contract.py +27 -0
- package/skills/cosmic-build/scripts/test/test_report_doc_contract_sync.py +5 -4
- package/skills/cosmic-build/scripts/test/test_report_sdk_optimization.py +36 -0
- package/skills/cosmic-build/scripts/test/test_rule_batch_evidence.py +1 -1
- package/skills/cosmic-build/scripts/test/test_rule_domain_contract.py +20 -0
- package/skills/cosmic-build/scripts/test/test_rule_function_formula.py +16 -6
- package/skills/cosmic-build/scripts/test/test_skill_entry_contract.py +1 -0
- package/skills/cosmic-build/scripts/test/test_test_environment.py +45 -0
- package/skills/cosmic-build/scripts/test/test_verify_plugin_loading.py +79 -0
- package/skills/cosmic-build/scripts/test_environment.py +49 -0
- package/skills/cosmic-build/scripts/verify_plugin_loading.py +71 -0
- package/skills/cosmic-build/skills/cosmic-coding-impl/SKILL.md +86 -162
- package/skills/cosmic-build/skills/cosmic-coding-script/ATTRIBUTION.md +1 -1
- package/skills/cosmic-build/skills/cosmic-coding-script/SKILL.md +45 -14
- package/skills/cosmic-build/skills/cosmic-coding-script/references/kwc-controller-authoring.md +6 -0
- package/skills/cosmic-build/skills/cosmic-coding-script/references/sdk/README.md +29 -19
- package/skills/cosmic-build/skills/cosmic-coding-script/references/sdk/docs//350/204/232/346/234/254SDK.md +3 -4
- package/skills/cosmic-build/skills/cosmic-coding-script/references/sdk/indexes/package-index.md +1 -1
- package/skills/cosmic-build/skills/cosmic-coding-script/references/sdk/strategy.md +27 -18
- package/skills/cosmic-build/skills/cosmic-meta-api/SKILL.md +11 -17
- package/skills/cosmic-build/skills/cosmic-meta-api/apis/ai_meta.md +1 -1
- package/skills/cosmic-build/skills/cosmic-meta-api/apis/app_menu.md +1 -1
- package/skills/cosmic-build/skills/cosmic-meta-api/apis/meta_log.md +1 -1
- package/skills/cosmic-build/skills/cosmic-meta-api/contracts/cli/control-authoring.json +45 -4
- package/skills/cosmic-build/skills/cosmic-meta-api/contracts/cli/form-control-traits.json +1 -1
- package/skills/cosmic-build/skills/cosmic-meta-api/contracts/cli/layout-ui.json +18 -0
- package/skills/cosmic-build/skills/cosmic-meta-api/contracts/cli/menu-publish.json +8 -2
- package/skills/cosmic-build/skills/cosmic-meta-api/contracts/cli/property-batch.json +7 -0
- package/skills/cosmic-build/skills/cosmic-meta-api/contracts/cli/property-input-index.json +3 -3
- package/skills/cosmic-build/skills/cosmic-meta-api/contracts/cli/report-write-contracts.json +1 -0
- package/skills/cosmic-build/skills/cosmic-meta-api/contracts/cli/rule-domain.json +37 -5
- package/skills/cosmic-build/skills/cosmic-meta-api/contracts/cli/rule-event-contract.json +24 -3
- package/skills/cosmic-build/skills/cosmic-meta-api/contracts/cli/run-build-meta.json +61 -12
- package/skills/cosmic-build/skills/cosmic-meta-api/contracts/schema/dropdown-menu.schema.json +69 -0
- package/skills/cosmic-build/skills/cosmic-meta-api/contracts/schema/report-write.schema.json +2 -1
- package/skills/cosmic-build/skills/cosmic-meta-api/contracts/schema/rule-set.schema.json +1 -0
- package/skills/cosmic-build/skills/cosmic-meta-api/contracts/schema/rule-write.schema.json +14 -7
- package/skills/cosmic-build/skills/cosmic-meta-api/docs/api-summary.md +2 -2
- package/skills/cosmic-build/skills/cosmic-meta-api/modules/app-action/SKILL.md +6 -3
- package/skills/cosmic-build/skills/cosmic-meta-api/modules/develop/SKILL.md +1 -2
- package/skills/cosmic-build/skills/cosmic-meta-api/modules/export/SKILL.md +5 -6
- package/skills/cosmic-build/skills/cosmic-meta-api/modules/init/SKILL.md +13 -36
- package/skills/cosmic-build/skills/cosmic-meta-api/modules/pdm/SKILL.md +8 -9
- package/skills/cosmic-build/skills/cosmic-meta-api/modules/pdm/scripts/config.json.example +5 -15
- package/skills/cosmic-build/skills/cosmic-meta-api/modules/reset/SKILL.md +6 -6
- package/skills/cosmic-build/skills/cosmic-meta-api/modules/usage/SKILL.md +20 -148
- package/skills/cosmic-build/skills/cosmic-meta-api/modules/verify/SKILL.md +1 -1
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/_core/build_receipts.py +171 -0
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/_core/cmd_apps.py +25 -6
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/_core/cmd_controls.py +55 -21
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/_core/cmd_fields.py +10 -3
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/_core/cmd_formactions.py +63 -6
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/_core/cmd_forms.py +59 -2
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/_core/cmd_listui.py +8 -0
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/_core/cmd_metaops.py +36 -1
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/_core/cmd_plugins.py +115 -90
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/_core/cmd_properties.py +666 -4
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/_core/cmd_rules.py +1415 -87
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/_core/commands.py +6 -1
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/_core/config.py +28 -15
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/_core/execution_errors.py +40 -0
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/_core/formatters.py +32 -5
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/_core/property_policy_contract.py +6 -0
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/_core/report_ops.py +113 -17
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/_core/reset.py +3 -3
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/_core/rule_contract.py +107 -26
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/build_meta_order.py +50 -49
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/cosmic_api.py +856 -150
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/field_decorator.py +47 -14
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_build_receipt_resume.py +374 -0
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_cli_failure_contract.py +24 -0
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_creation_identity_boundary.py +4 -2
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_dropdown_menu_contract.py +382 -0
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_existing_form_ui.py +0 -25
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_field_decorator.py +50 -6
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_functional_group_resolution.py +83 -16
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_identity_template_resolution.py +103 -0
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_report_plugin_contract.py +28 -2
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_report_readback_derivation.py +22 -0
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_report_typed_filters.py +65 -0
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_rule_write_closure.py +1041 -15
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_run_batch_post_build_recovery.py +65 -0
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_run_batch_verification_evidence.py +17 -0
- package/skills/cosmic-build/skills/cosmic-meta-knowledge/generation/api-param-checklist.md +1 -1
- package/skills/cosmic-build/skills/cosmic-meta-knowledge/generation/rule-modeling-guide.md +7 -4
- package/skills/cosmic-build/skills/cosmic-meta-modeling/SKILL.md +61 -13
- package/skills/cosmic-build/skills/cosmic-meta-modeling/modules/app-menu/SKILL.md +8 -0
- package/skills/cosmic-build/skills/cosmic-meta-modeling/modules/formtemplate/SKILL.md +5 -0
- package/skills/cosmic-build/skills/cosmic-meta-modeling/modules/formtemplate/dynamic-form-artifact-schema.md +5 -0
- package/skills/cosmic-build/skills/cosmic-meta-modeling/modules/report/SKILL.md +21 -9
- package/skills/cosmic-build/skills/cosmic-meta-modeling/references/authoring-entity-graphs.md +1 -1
- package/skills/cosmic-build/skills/cosmic-meta-modeling/references/authoring-storage-constraints.md +5 -3
- package/skills/cosmic-build/skills/cosmic-meta-modeling/references/authoring-ui-composition.md +6 -3
- package/skills/cosmic-build/skills/cosmic-meta-modeling/references/creation-recovery.md +13 -0
- package/skills/cosmic-build/skills/cosmic-meta-modeling/references/parse-core.md +3 -0
- package/skills/cosmic-build/skills/cosmic-meta-modeling/scripts/data_model_contract.py +6 -1
- package/skills/cosmic-build/skills/cosmic-meta-modeling/scripts/data_model_emit.py +135 -87
- package/skills/cosmic-build/skills/cosmic-meta-modeling/scripts/field_type_registry.py +11 -1
- package/skills/cosmic-build/skills/cosmic-meta-modeling/scripts/isv_resolver.py +1 -3
- package/skills/cosmic-build/skills/cosmic-meta-modeling/scripts/report_validator.py +29 -0
- package/skills/cosmic-build/skills/cosmic-meta-modifier/SKILL.md +4 -2
- package/skills/cosmic-build/skills/cosmic-meta-modifier/modules/context/SKILL.md +2 -2
- package/skills/cosmic-build/skills/cosmic-meta-modifier/modules/execute/SKILL.md +1 -1
- package/skills/cosmic-build/skills/cosmic-meta-modifier/modules/operation/SKILL.md +5 -4
- package/skills/cosmic-build/skills/cosmic-meta-modifier/modules/operation/operation-write.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 +20 -5
- package/skills/cosmic-build/skills/cosmic-meta-modifier/modules/property/complex-apis.md +1 -1
- package/skills/cosmic-build/skills/cosmic-meta-modifier/modules/property/policy-grammar.md +4 -1
- package/skills/cosmic-build/skills/cosmic-meta-modifier/modules/report-op/SKILL.md +9 -0
- package/skills/cosmic-build/skills/cosmic-meta-modifier/modules/rule/SKILL.md +61 -4
- package/skills/cosmic-build/skills/cosmic-meta-modifier/modules/rule/raw-rule-authoring.md +3 -1
- package/skills/cosmic-build/skills/cosmic-meta-modifier/modules/rule/rule-actions.md +2 -2
- package/skills/cosmic-build/skills/cosmic-meta-modifier/modules/rule/rule-reference.md +6 -6
- package/skills/cosmic-build/skills/cosmic-meta-modifier/modules/rule/state.md +1 -1
- package/skills/cosmic-build/skills/cosmic-meta-modifier/modules/unit/SKILL.md +6 -3
- package/skills/cosmic-build/skills/cosmic-meta-modifier/modules/verify/usage-guide.md +4 -4
- package/skills/cosmic-build/skills/cosmic-meta-modifier/references/list-filter-modification.md +4 -1
- package/skills/cosmic-build/skills/cosmic-meta-ui/SKILL.md +9 -1
- package/skills/cosmic-build/skills/cosmic-meta-ui/modules/button/SKILL.md +3 -0
- package/skills/cosmic-build/skills/mcp-server/SKILL.md +210 -0
- package/skills/cosmic-build/skills/mcp-server/scripts/mcp_cli.py +431 -0
- package/skills/cosmic-build/skills/openapi-gen/SKILL.md +3 -3
- package/skills/cosmic-build/skills/openapi-gen/scripts/api/gen_runner.py +2 -2
- package/skills/cosmic-dev-best-practices/references/bill-dev-convention.md +19 -9
- package/skills/cosmic-dev-best-practices/references/cosmic-meta-api.md +14 -11
- package/skills/cosmic-dev-best-practices/references/design-examples/basedata-design-example.md +1 -1
- package/skills/cosmic-dev-best-practices/references/design-examples/bill-design-example.md +1 -1
- package/skills/cosmic-dev-best-practices/references/elements/AssistantField.md +1 -1
- package/skills/cosmic-dev-best-practices/references/elements/BasedataField.md +2 -2
- package/skills/cosmic-dev-best-practices/references/elements/BasedataPropField.md +3 -3
- package/skills/cosmic-dev-best-practices/references/elements/CreaterField.md +2 -2
- package/skills/cosmic-dev-best-practices/references/elements/CurrencyField.md +2 -2
- package/skills/cosmic-dev-best-practices/references/elements/CustomerField.md +2 -2
- package/skills/cosmic-dev-best-practices/references/elements/FlexField.md +1 -1
- package/skills/cosmic-dev-best-practices/references/elements/MaterielField.md +2 -2
- package/skills/cosmic-dev-best-practices/references/elements/OrgField.md +2 -2
- package/skills/cosmic-dev-best-practices/references/elements/SupplierField.md +2 -2
- package/skills/cosmic-dev-best-practices/references/elements/UnitField.md +1 -1
- package/skills/cosmic-dev-best-practices/references/elements/UserField.md +3 -3
- package/skills/cosmic-dev-best-practices/references/elements/common-field-property-conventions.md +16 -11
- package/skills/cosmic-dev-best-practices/references/layout-control-convention.md +5 -4
- package/skills/erp-analysis/references/delivery-contract.md +4 -2
- package/skills/erp-analysis/templates/AGENTS.template.md +1 -1
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/.dsh/cosmic/.kd/config.json +0 -21
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
2
|
+
"""Prepare current native form context for Java coding, with optional legacy artifact inputs."""
|
|
3
3
|
|
|
4
4
|
from __future__ import annotations
|
|
5
5
|
|
|
@@ -7,11 +7,17 @@ import argparse
|
|
|
7
7
|
import difflib
|
|
8
8
|
import json
|
|
9
9
|
import re
|
|
10
|
+
import subprocess
|
|
11
|
+
import sys
|
|
12
|
+
import uuid
|
|
10
13
|
from pathlib import Path
|
|
11
14
|
from typing import Any
|
|
12
15
|
|
|
13
|
-
from java_source import method_signatures
|
|
16
|
+
from java_source import method_signatures, stripped
|
|
14
17
|
from resolve_output_path import resolve_output_path
|
|
18
|
+
from platform_api_contract import plugin_coding_evidence
|
|
19
|
+
|
|
20
|
+
COSMIC_API = Path(__file__).resolve().parents[1] / "skills" / "cosmic-meta-api" / "scripts" / "cosmic_api.py"
|
|
15
21
|
|
|
16
22
|
|
|
17
23
|
CLASS_RE = re.compile(r"^[A-Za-z][A-Za-z0-9]*$")
|
|
@@ -38,16 +44,41 @@ def _read_json(path: str | Path) -> Any:
|
|
|
38
44
|
raise HandoffError(f"cannot read JSON {target}: {exc}") from exc
|
|
39
45
|
|
|
40
46
|
|
|
47
|
+
def manifest_artifacts(manifest_path: str) -> list[str]:
|
|
48
|
+
"""Use the exact emit receipt; never search for timestamped artifact names."""
|
|
49
|
+
manifest = _read_json(manifest_path)
|
|
50
|
+
items = manifest.get("artifacts") if isinstance(manifest, dict) else None
|
|
51
|
+
if not isinstance(items, list) or not items:
|
|
52
|
+
raise HandoffError("emit manifest must contain non-empty artifacts", "PLUGIN_MANIFEST_INVALID")
|
|
53
|
+
paths = []
|
|
54
|
+
for item in items:
|
|
55
|
+
value = item.get("path") if isinstance(item, dict) else None
|
|
56
|
+
if not isinstance(value, str) or not value:
|
|
57
|
+
raise HandoffError("emit manifest artifact is missing its path", "PLUGIN_MANIFEST_INVALID")
|
|
58
|
+
target = Path(value).expanduser()
|
|
59
|
+
paths.append(str((target if target.is_absolute() else Path(manifest_path).resolve().parent / target).resolve()))
|
|
60
|
+
return paths
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _artifact_external_catalog(entities: list[dict], fields: list[dict]) -> list[dict]:
|
|
64
|
+
"""Reuse native verified emit evidence for entities already in this delivery."""
|
|
65
|
+
return [{
|
|
66
|
+
"formNumber": entity["formNumber"], "formName": entity["formName"],
|
|
67
|
+
"entityKey": entity["entityKey"], "entityName": entity["entityName"],
|
|
68
|
+
"fields": {field["fieldKey"]: {key: field.get(key) for key in ("fieldKey", "fieldName", "fieldType", "refEntity", "comboOptions")}
|
|
69
|
+
for field in fields if field["formNumber"] == entity["formNumber"] and field["entityKey"] == entity["entityKey"]},
|
|
70
|
+
} for entity in entities]
|
|
71
|
+
|
|
72
|
+
|
|
41
73
|
def _exact_one(records: list[dict], selector: str, keys: tuple[str, ...], kind: str) -> dict:
|
|
42
74
|
value = str(selector or "").strip()
|
|
43
75
|
matches = [row for row in records if any(str(row.get(key) or "") == value for key in keys)]
|
|
44
76
|
if len(matches) != 1:
|
|
45
77
|
examples = []
|
|
46
|
-
for row in records:
|
|
47
|
-
candidate = str(
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
).strip()
|
|
78
|
+
for row in matches or records:
|
|
79
|
+
candidate = (str(row.get("entityKey") or "") + "." + str(row["fieldKey"])
|
|
80
|
+
+ " (" + str(row.get("fieldName") or "") + ")" if row.get("fieldKey")
|
|
81
|
+
else str(row.get("entityKey") or row.get("entityName") or row.get("authoringId") or ""))
|
|
51
82
|
if candidate and candidate not in examples:
|
|
52
83
|
examples.append(candidate)
|
|
53
84
|
if len(examples) == 8:
|
|
@@ -161,8 +192,6 @@ def _compile_report_contract(artifact: dict, form_number: str) -> dict | None:
|
|
|
161
192
|
column presentation or a non-persisted filter requirement that reaches Java delivery as
|
|
162
193
|
nothing is exactly how those requirements used to disappear.
|
|
163
194
|
"""
|
|
164
|
-
import sys
|
|
165
|
-
|
|
166
195
|
scripts = Path(__file__).resolve().parents[1] / "skills" / "cosmic-meta-api" / "scripts"
|
|
167
196
|
if str(scripts) not in sys.path:
|
|
168
197
|
sys.path.insert(0, str(scripts))
|
|
@@ -269,6 +298,11 @@ def _catalog(
|
|
|
269
298
|
"entityName": entity_name,
|
|
270
299
|
"entityId": entity.get("id"),
|
|
271
300
|
"authoringId": entity.get("id"),
|
|
301
|
+
"entityType": entity.get("type"),
|
|
302
|
+
"role": "header" if entity is main else "subentries" if entity.get("type") == "SubEntryEntity" else "entries",
|
|
303
|
+
"parentEntityKey": next((row.get("entityKey") for row in authored_entities
|
|
304
|
+
if row.get("id") == entity.get("parentEntityId")), None),
|
|
305
|
+
"collectionProperty": entity_key if entity is not main else None,
|
|
272
306
|
}
|
|
273
307
|
entities.append(entity_row)
|
|
274
308
|
authored_fields = list(entity.get("fields") or [])
|
|
@@ -307,6 +341,7 @@ def _catalog(
|
|
|
307
341
|
# database ("type=DynamicObject") rather than in Java.
|
|
308
342
|
"fieldType": str(field.get("dataType") or "").strip(),
|
|
309
343
|
"refEntity": str(field.get("basedataNumber") or "").strip(),
|
|
344
|
+
"comboOptions": field.get("comboOptions") or [],
|
|
310
345
|
})
|
|
311
346
|
return forms, entities, fields, registered_plugins
|
|
312
347
|
|
|
@@ -316,12 +351,19 @@ def _scope(records: list[dict], selector: dict, *, form_keys: tuple[str, ...]) -
|
|
|
316
351
|
if selector.get("form"):
|
|
317
352
|
value = str(selector["form"]).strip()
|
|
318
353
|
scoped = [row for row in scoped if any(str(row.get(key) or "") == value for key in form_keys)]
|
|
354
|
+
if not scoped:
|
|
355
|
+
raise HandoffError("form scope " + repr(value) + " is unknown; candidates: "
|
|
356
|
+
+ ", ".join(sorted({str(row.get("formNumber") or "") for row in records})))
|
|
319
357
|
if selector.get("entity"):
|
|
320
358
|
value = str(selector["entity"]).strip()
|
|
359
|
+
candidates = scoped
|
|
321
360
|
scoped = [row for row in scoped if value in (
|
|
322
361
|
str(row.get("entityName") or ""), str(row.get("entityKey") or ""),
|
|
323
362
|
str(row.get("entityId") or ""), str(row.get("authoringId") or ""),
|
|
324
|
-
)]
|
|
363
|
+
) or value in {"header", "entries", "subentries"} and row.get("role") == value]
|
|
364
|
+
if not scoped:
|
|
365
|
+
raise HandoffError("entity scope " + repr(value) + " is unknown; candidates: "
|
|
366
|
+
+ ", ".join(sorted({str(row.get("entityKey") or "") for row in candidates})))
|
|
325
367
|
return scoped
|
|
326
368
|
|
|
327
369
|
|
|
@@ -331,6 +373,10 @@ def _resolve_entity(records: list[dict], selector: Any) -> dict:
|
|
|
331
373
|
if not isinstance(selector, dict) or not selector.get("entity"):
|
|
332
374
|
raise HandoffError("entity selector must be a string or {form, entity}")
|
|
333
375
|
scoped = _scope(records, selector, form_keys=("formName", "formNumber"))
|
|
376
|
+
if selector["entity"] in {"header", "entries", "subentries"}:
|
|
377
|
+
if len(scoped) == 1:
|
|
378
|
+
return scoped[0]
|
|
379
|
+
raise HandoffError("entity scope is ambiguous; candidates: " + ", ".join(row["entityKey"] for row in scoped))
|
|
334
380
|
return _exact_one(scoped, selector["entity"], ("entityName", "entityKey", "entityId", "authoringId"), "entity")
|
|
335
381
|
|
|
336
382
|
|
|
@@ -383,6 +429,8 @@ _IMPLICIT_ENTITY_FIELDS = {"id", "masterid", "seq"}
|
|
|
383
429
|
# Cosmic field type -> the Java type its value carries. Only the families that carry the bug this
|
|
384
430
|
# verification exists to stop are mapped: a reference field holds the referenced record's id, and
|
|
385
431
|
# reading that as a String is what produced "operator does not exist: bigint = character varying".
|
|
432
|
+
# The target 8.0 SDK's TimeProp.getPropertyType() is Integer. It stores seconds after midnight;
|
|
433
|
+
# DynamicObject.getDate() converts that Integer to null, silently skipping interval checks.
|
|
386
434
|
# Everything else keeps its verbatim fieldType and no javaType, because a plausible-looking guess
|
|
387
435
|
# here would carry the same authority as a verified one.
|
|
388
436
|
_FIELD_JAVA_TYPES = (
|
|
@@ -411,6 +459,9 @@ def _field_java_type(field_type: str) -> str | None:
|
|
|
411
459
|
# A Mul* field holds a collection, not one value, so the single-value mapping does not apply.
|
|
412
460
|
if field_type.startswith("Mul"):
|
|
413
461
|
return None
|
|
462
|
+
# Match the exact time-only types: DateTimeField also contains "TimeField" as a suffix.
|
|
463
|
+
if field_type in ("TimeProp", "TimeField"):
|
|
464
|
+
return "Integer"
|
|
414
465
|
for marker, java_type in _FIELD_JAVA_TYPES:
|
|
415
466
|
if marker.lower() in field_type.lower():
|
|
416
467
|
return java_type
|
|
@@ -443,15 +494,23 @@ def _field_contract(row: dict) -> dict | None:
|
|
|
443
494
|
"fieldKey": field_key,
|
|
444
495
|
"fieldName": str(row.get("fieldName") or "").strip(),
|
|
445
496
|
"fieldType": field_type,
|
|
497
|
+
**{key: row[key] for key in ("entityKey", "entityName", "parentEntityKey", "collectionPath", "fieldPath", "required") if key in row},
|
|
446
498
|
}
|
|
447
499
|
if row.get("refEntity"):
|
|
448
500
|
contract["refEntity"] = row["refEntity"]
|
|
501
|
+
if row.get("comboOptions"):
|
|
502
|
+
contract["comboOptions"] = row["comboOptions"]
|
|
449
503
|
java_type = _field_java_type(field_type)
|
|
450
504
|
if java_type:
|
|
451
505
|
contract["javaType"] = java_type
|
|
452
|
-
if
|
|
506
|
+
if field_type in ("TimeProp", "TimeField"):
|
|
507
|
+
contract["valueAccessor"] = f'getInt("{field_key}")'
|
|
508
|
+
contract["timeValueUnit"] = "seconds-after-midnight"
|
|
509
|
+
reference = (bool(row.get("refEntity") or row.get("refIdProperty"))
|
|
510
|
+
if field_type.endswith("Prop") else _is_reference_field(field_type))
|
|
511
|
+
if reference and not field_type.startswith("Mul"):
|
|
453
512
|
contract["valueAccessor"] = f'getDynamicObject("{field_key}")'
|
|
454
|
-
contract["idAccessor"] = f'getLong("{field_key
|
|
513
|
+
contract["idAccessor"] = f'getLong("{row.get("refIdProperty") or field_key + "_id"}")'
|
|
455
514
|
contract["filterValue"] = f'getDynamicObject("{field_key}").getPkValue()'
|
|
456
515
|
contract["note"] = (
|
|
457
516
|
f'get("{field_key}") returns the referenced DynamicObject, not its id. A QFilter on '
|
|
@@ -587,6 +646,15 @@ def _verify_external_contracts(contracts: list, catalog: list[dict]) -> list:
|
|
|
587
646
|
Returns the same list with each verified entry enriched by ``verifiedFields``, so the
|
|
588
647
|
coding step receives the real key and its Java value type instead of rediscovering both.
|
|
589
648
|
"""
|
|
649
|
+
missing = list(dict.fromkeys(str(entry["entity"]).strip() for entry in contracts
|
|
650
|
+
if isinstance(entry, dict) and entry.get("entity")
|
|
651
|
+
and not _external_scope(catalog, str(entry["entity"]))))
|
|
652
|
+
if missing:
|
|
653
|
+
raise HandoffError(
|
|
654
|
+
"external contract entities/forms are not verified: " + ", ".join(missing)
|
|
655
|
+
+ "; supply exact getFormSchema read-backs via --external-schema or use verified batch evidence",
|
|
656
|
+
"PLUGIN_EXTERNAL_CONTRACT_UNVERIFIED",
|
|
657
|
+
)
|
|
590
658
|
verified: list = []
|
|
591
659
|
for entry in contracts:
|
|
592
660
|
if not isinstance(entry, dict):
|
|
@@ -597,13 +665,6 @@ def _verify_external_contracts(contracts: list, catalog: list[dict]) -> list:
|
|
|
597
665
|
verified.append(entry)
|
|
598
666
|
continue
|
|
599
667
|
scope = _external_scope(catalog, entity_selector)
|
|
600
|
-
if not scope:
|
|
601
|
-
raise HandoffError(
|
|
602
|
-
f"external contract entity/form {entity_selector!r} is not verified; save its "
|
|
603
|
-
"exact form read-back with cosmic_api.py --output <path> getFormSchema "
|
|
604
|
-
"--formNumber <formNumber> and pass that file as --external-schema",
|
|
605
|
-
"PLUGIN_EXTERNAL_CONTRACT_UNVERIFIED",
|
|
606
|
-
)
|
|
607
668
|
selectors = _contract_field_selectors(entry)
|
|
608
669
|
resolved = [_external_field(scope, selector) for selector in selectors]
|
|
609
670
|
entities = {row["entityKey"] for row, _key, _field in resolved}
|
|
@@ -648,16 +709,25 @@ def _verified_field(field_key: str, field: dict | None, entity: dict | None = No
|
|
|
648
709
|
result.update({"entityKey": entity["entityKey"], "entityName": entity["entityName"]})
|
|
649
710
|
if field.get("refEntity"):
|
|
650
711
|
result["refEntity"] = field["refEntity"]
|
|
712
|
+
if field.get("comboOptions"):
|
|
713
|
+
result["comboOptions"] = field["comboOptions"]
|
|
651
714
|
java_type = _field_java_type(field_type)
|
|
652
715
|
if java_type:
|
|
653
716
|
result["javaType"] = java_type
|
|
717
|
+
if field_type in ("TimeProp", "TimeField"):
|
|
718
|
+
result["valueAccessor"] = f'getInt("{field_key}")'
|
|
719
|
+
result["timeValueUnit"] = "seconds-after-midnight"
|
|
654
720
|
return result
|
|
655
721
|
|
|
656
722
|
|
|
657
723
|
def _verified_operation_keys(value: Any) -> set[str]:
|
|
658
724
|
"""Extract operation keys from a listOperations response without assuming one envelope."""
|
|
659
|
-
if isinstance(value, dict)
|
|
660
|
-
|
|
725
|
+
if isinstance(value, dict):
|
|
726
|
+
data = value.get("data")
|
|
727
|
+
if (any(layer.get("status") is False or layer.get("success") is False
|
|
728
|
+
for layer in (value, data) if isinstance(layer, dict))
|
|
729
|
+
or value.get("errorCode") not in (None, "", 0, "0")):
|
|
730
|
+
raise HandoffError("listOperations did not succeed")
|
|
661
731
|
found: set[str] = set()
|
|
662
732
|
|
|
663
733
|
def visit(node: Any) -> None:
|
|
@@ -680,6 +750,195 @@ def _verified_operation_keys(value: Any) -> set[str]:
|
|
|
680
750
|
return found
|
|
681
751
|
|
|
682
752
|
|
|
753
|
+
def operation_contracts(value: Any, keys: list[str]) -> list[dict]:
|
|
754
|
+
"""Preserve actual operation parameters instead of guessing business state transitions."""
|
|
755
|
+
if isinstance(value, list):
|
|
756
|
+
return [entry for child in value for entry in operation_contracts(child, keys)]
|
|
757
|
+
if not isinstance(value, dict):
|
|
758
|
+
return []
|
|
759
|
+
key = value.get("operationKey") or value.get("Key")
|
|
760
|
+
if key in keys:
|
|
761
|
+
return [{"operationKey": key, **{name: value[name] for name in
|
|
762
|
+
("operationId", "operationName", "operationType", "parameter") if name in value}}]
|
|
763
|
+
return [entry for child in value.values() if isinstance(child, (dict, list))
|
|
764
|
+
for entry in operation_contracts(child, keys)]
|
|
765
|
+
|
|
766
|
+
|
|
767
|
+
def read_platform(app_root: str, command: str, selector: str, flag: str = "--formNumber") -> Any:
|
|
768
|
+
import hashlib
|
|
769
|
+
destination = Path(app_root).resolve() / "_runtime" / "plugin-evidence" / (
|
|
770
|
+
command + "." + hashlib.sha256(selector.encode()).hexdigest()[:12] + "." + uuid.uuid4().hex + ".json")
|
|
771
|
+
destination.parent.mkdir(parents=True, exist_ok=True)
|
|
772
|
+
# Do not let a previous successful file stand in for a failed current read.
|
|
773
|
+
destination.unlink(missing_ok=True)
|
|
774
|
+
try:
|
|
775
|
+
completed = subprocess.run([sys.executable, str(COSMIC_API), "--output", str(destination), command,
|
|
776
|
+
"--app-root", str(Path(app_root).resolve()), flag, selector],
|
|
777
|
+
capture_output=True, text=True, encoding="utf-8", errors="replace", timeout=60)
|
|
778
|
+
except (OSError, subprocess.TimeoutExpired) as exc:
|
|
779
|
+
raise HandoffError(f"{command} could not complete for {selector}", "PLUGIN_EVIDENCE_READ_FAILED") from exc
|
|
780
|
+
if completed.returncode or not destination.is_file():
|
|
781
|
+
raise HandoffError(f"{command} failed for {selector}; no fresh evidence saved",
|
|
782
|
+
"PLUGIN_EVIDENCE_READ_FAILED")
|
|
783
|
+
value = _read_json(destination)
|
|
784
|
+
if isinstance(value, dict) and (value.get("status") is False
|
|
785
|
+
or value.get("errorCode") not in (None, "", 0, "0")
|
|
786
|
+
or isinstance(value.get("data"), dict) and value["data"].get("success") is False):
|
|
787
|
+
raise HandoffError(f"{command} failed for {selector}: {value.get('message') or value.get('errorCode')}",
|
|
788
|
+
"PLUGIN_EVIDENCE_READ_FAILED")
|
|
789
|
+
return value
|
|
790
|
+
|
|
791
|
+
|
|
792
|
+
def prepare_inputs(*, app_root: str, intent: dict, plugin_type: str, artifact_paths: list[str],
|
|
793
|
+
run_result: Any, operation_result: Any, external_schemas: list[Any]) -> tuple[Any, list[Any]]:
|
|
794
|
+
"""Collect only missing read-only platform evidence for this verified target."""
|
|
795
|
+
forms, entities, fields, _ = _catalog(artifact_paths, run_result)
|
|
796
|
+
main = _exact_one(forms, intent.get("form"), ("formName", "formNumber"), "form")
|
|
797
|
+
_string_list(intent.get("coreLogic"), "coreLogic", required=True)
|
|
798
|
+
_string_list(intent.get("scenes"), "scenes")
|
|
799
|
+
keys = _string_list(intent.get("operationKeys"), "operationKeys")
|
|
800
|
+
if plugin_type == "operation" and not keys:
|
|
801
|
+
raise HandoffError("type=operation requires operationKeys")
|
|
802
|
+
if plugin_type != "operation" and keys:
|
|
803
|
+
raise HandoffError("operationKeys are only valid for type=operation")
|
|
804
|
+
if isinstance(run_result, dict) and run_result.get("appRoot") and Path(run_result["appRoot"]).resolve() != Path(app_root).resolve():
|
|
805
|
+
raise HandoffError("write result belongs to another application", "PLUGIN_HANDOFF_UNRESOLVED")
|
|
806
|
+
specialized = intent.get("specialized") or {}
|
|
807
|
+
if not isinstance(specialized, dict) or "unresolvedDependencies" not in specialized:
|
|
808
|
+
raise HandoffError("declare specialized.unresolvedDependencies before platform reads")
|
|
809
|
+
if specialized.get("unresolvedDependencies"):
|
|
810
|
+
raise HandoffError("resolve declared dependencies before platform reads", "PLUGIN_EXTERNAL_CONTRACT_UNRESOLVED")
|
|
811
|
+
catalog = _external_entity_catalog(external_schemas)
|
|
812
|
+
if run_result is not None:
|
|
813
|
+
catalog += _artifact_external_catalog(entities, fields)
|
|
814
|
+
missing = [entry for name in ("externalContracts", "externalApis")
|
|
815
|
+
for entry in _contract_list(specialized.get(name), name)
|
|
816
|
+
if isinstance(entry, dict) and entry.get("entity")
|
|
817
|
+
and not _external_scope(catalog, str(entry["entity"]))]
|
|
818
|
+
selectors = list(dict.fromkeys(str(entry.get("formNumber") or entry["entity"]) for entry in missing))
|
|
819
|
+
invalid = [selector for selector in selectors if not re.fullmatch(r"[A-Za-z0-9_]+", selector)]
|
|
820
|
+
if invalid:
|
|
821
|
+
raise HandoffError("automatic schema reads require exact formNumber for: " + ", ".join(invalid),
|
|
822
|
+
"PLUGIN_EXTERNAL_CONTRACT_UNVERIFIED")
|
|
823
|
+
|
|
824
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
825
|
+
with ThreadPoolExecutor(max_workers=4) as pool:
|
|
826
|
+
schemas = list(pool.map(lambda selector: read_platform(app_root, "getFormSchema", selector), selectors))
|
|
827
|
+
if plugin_type == "operation" and operation_result is None:
|
|
828
|
+
operation_result = read_platform(app_root, "listOperations", main["formNumber"])
|
|
829
|
+
return operation_result, external_schemas + schemas
|
|
830
|
+
|
|
831
|
+
|
|
832
|
+
def runtime_catalog(snapshots: list[Any]) -> tuple[list[dict], list[dict], list[dict], list[dict]]:
|
|
833
|
+
"""Project getEntityType's native Properties/ItemType graph without authoring-key derivation."""
|
|
834
|
+
forms, entities, fields = [], [], []
|
|
835
|
+
|
|
836
|
+
def text(value: Any) -> str:
|
|
837
|
+
if isinstance(value, dict):
|
|
838
|
+
return str(value.get("zh_CN") or value.get("en_US") or "")
|
|
839
|
+
return str(value or "")
|
|
840
|
+
|
|
841
|
+
def visit(node: dict, form: dict, parent: dict | None = None, prop: dict | None = None) -> None:
|
|
842
|
+
key = str(node.get("Name") or "").strip()
|
|
843
|
+
properties = node.get("Properties")
|
|
844
|
+
if not key or not isinstance(properties, list):
|
|
845
|
+
raise HandoffError("runtime entity has no Name/Properties", "PLUGIN_CONTEXT_INCOMPLETE")
|
|
846
|
+
role = "header" if parent is None else "entries" if parent["role"] == "header" else "subentries"
|
|
847
|
+
collection = str((prop or {}).get("Name") or "")
|
|
848
|
+
path = parent["collectionPath"] + [collection] if parent else []
|
|
849
|
+
if parent and not collection:
|
|
850
|
+
raise HandoffError("runtime collection property has no Name", "PLUGIN_CONTEXT_INCOMPLETE")
|
|
851
|
+
entity = {**form, "entityKey": key, "entityName": text((prop or {}).get("DisplayName") or node.get("DisplayName")) or key,
|
|
852
|
+
"entityId": key, "authoringId": key, "entityType": node.get("_Type_"), "role": role,
|
|
853
|
+
"parentEntityKey": parent["entityKey"] if parent else None,
|
|
854
|
+
"collectionProperty": collection or None, "collectionPath": path}
|
|
855
|
+
entities.append(entity)
|
|
856
|
+
for property in properties:
|
|
857
|
+
if not isinstance(property, dict) or not property.get("Name"):
|
|
858
|
+
continue
|
|
859
|
+
kind = str(property.get("_Type_") or "")
|
|
860
|
+
if kind in {"EntryProp", "SubEntryProp"}:
|
|
861
|
+
item = property.get("ItemType")
|
|
862
|
+
if not isinstance(item, dict):
|
|
863
|
+
raise HandoffError("runtime collection has no ItemType", "PLUGIN_CONTEXT_INCOMPLETE")
|
|
864
|
+
visit(item, form, entity, property)
|
|
865
|
+
continue
|
|
866
|
+
name = str(property["Name"])
|
|
867
|
+
if not kind:
|
|
868
|
+
raise HandoffError(f"runtime field {name} has no type", "PLUGIN_CONTEXT_INCOMPLETE")
|
|
869
|
+
fields.append({**entity, "fieldKey": name, "fieldName": text(property.get("DisplayName")) or name,
|
|
870
|
+
"fieldType": kind, "fieldId": property.get("Id"), "authoringId": name,
|
|
871
|
+
"fieldPath": ".".join(path + [name]), "refEntity": property.get("BaseEntityId") or "",
|
|
872
|
+
"required": property.get("MustInput") is True,
|
|
873
|
+
"refIdProperty": property.get("RefIdPropName"),
|
|
874
|
+
"metadataResourceId": (property.get("DisplayName") or {}).get("resId")
|
|
875
|
+
if isinstance(property.get("DisplayName"), dict) else None,
|
|
876
|
+
"comboOptions": [{"label": text(option.get("Name")), "value": option.get("Value")}
|
|
877
|
+
for option in property.get("ComboItems") or []]})
|
|
878
|
+
|
|
879
|
+
for snapshot in snapshots:
|
|
880
|
+
if not isinstance(snapshot, dict) or snapshot.get("status") is False:
|
|
881
|
+
raise HandoffError("runtime metadata read did not succeed", "PLUGIN_CONTEXT_INCOMPLETE")
|
|
882
|
+
data = snapshot.get("data", snapshot)
|
|
883
|
+
if isinstance(data, str):
|
|
884
|
+
data = json.loads(data)
|
|
885
|
+
if not isinstance(data, dict) or not data.get("Name"):
|
|
886
|
+
raise HandoffError("runtime metadata carries no entity definition", "PLUGIN_CONTEXT_INCOMPLETE")
|
|
887
|
+
form = {"formNumber": data["Name"], "formName": text(data.get("DisplayName")) or data["Name"],
|
|
888
|
+
"appCode": data.get("BizAppNumber"), "metadataVersion": data.get("Version"),
|
|
889
|
+
"metadataSource": "getEntityType"}
|
|
890
|
+
forms.append(form)
|
|
891
|
+
visit(data, form)
|
|
892
|
+
if not any(row["formNumber"] == form["formNumber"] for row in fields):
|
|
893
|
+
raise HandoffError("runtime entity carries no named fields", "PLUGIN_CONTEXT_INCOMPLETE")
|
|
894
|
+
return forms, entities, fields, []
|
|
895
|
+
|
|
896
|
+
|
|
897
|
+
def prepare_runtime_inputs(app_root: str, form_number: str, intent: dict, plugin_type: str) -> tuple[list[Any], Any]:
|
|
898
|
+
"""Read current main/explicitly referenced forms once, independently of modeling artifacts."""
|
|
899
|
+
_string_list(intent.get("coreLogic"), "coreLogic", required=True)
|
|
900
|
+
keys = _string_list(intent.get("operationKeys"), "operationKeys")
|
|
901
|
+
if plugin_type == "operation" and not keys:
|
|
902
|
+
raise HandoffError("type=operation requires operationKeys")
|
|
903
|
+
if plugin_type != "operation" and keys:
|
|
904
|
+
raise HandoffError("operationKeys are only valid for type=operation")
|
|
905
|
+
specialized = intent.get("specialized") or {}
|
|
906
|
+
if not isinstance(specialized, dict) or "unresolvedDependencies" not in specialized:
|
|
907
|
+
raise HandoffError("declare specialized.unresolvedDependencies before platform reads")
|
|
908
|
+
if specialized["unresolvedDependencies"]:
|
|
909
|
+
raise HandoffError("resolve declared dependencies before platform reads", "PLUGIN_EXTERNAL_CONTRACT_UNRESOLVED")
|
|
910
|
+
main = read_platform(app_root, "getEntityType", form_number, "--entityId")
|
|
911
|
+
forms, entities, fields, _ = runtime_catalog([main])
|
|
912
|
+
if forms[0]["formNumber"] != form_number:
|
|
913
|
+
raise HandoffError("runtime metadata belongs to another form", "PLUGIN_CONTEXT_INCOMPLETE")
|
|
914
|
+
archive = Path(app_root) / "archive.json"
|
|
915
|
+
identity = _read_json(archive) if archive.is_file() else {}
|
|
916
|
+
if forms[0].get("appCode") and identity.get("appCode") and forms[0]["appCode"] != identity["appCode"]:
|
|
917
|
+
raise HandoffError("runtime main form belongs to another application", "PLUGIN_CONTEXT_INCOMPLETE")
|
|
918
|
+
references = {field["refEntity"] for field in fields if field.get("refEntity")}
|
|
919
|
+
requests = []
|
|
920
|
+
catalog = _artifact_external_catalog(entities, fields)
|
|
921
|
+
for name in ("externalContracts", "externalApis"):
|
|
922
|
+
for entry in _contract_list(specialized.get(name), name):
|
|
923
|
+
if not isinstance(entry, dict) or not entry.get("entity") or _external_scope(catalog, str(entry["entity"])):
|
|
924
|
+
continue
|
|
925
|
+
selector = str(entry.get("formNumber") or entry["entity"])
|
|
926
|
+
if not re.fullmatch(r"[A-Za-z0-9_]+", selector):
|
|
927
|
+
# Resolve a unique declared reference's display name from its native metadata.
|
|
928
|
+
# Fetching declared reference targets is bounded to this form, never a global form tour.
|
|
929
|
+
requests += sorted(references)
|
|
930
|
+
else:
|
|
931
|
+
requests.append(selector)
|
|
932
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
933
|
+
with ThreadPoolExecutor(max_workers=4) as pool:
|
|
934
|
+
related = list(pool.map(lambda selector: read_platform(app_root, "getEntityType", selector, "--entityId"),
|
|
935
|
+
dict.fromkeys(requests)))
|
|
936
|
+
snapshots = [main] + related
|
|
937
|
+
runtime_catalog(snapshots) # Fail before coding when any requested graph is incomplete.
|
|
938
|
+
operations = read_platform(app_root, "listOperations", form_number) if plugin_type == "operation" else None
|
|
939
|
+
return snapshots, operations
|
|
940
|
+
|
|
941
|
+
|
|
683
942
|
def _bind_unique_mentions(
|
|
684
943
|
records: list[dict], text: str, existing: dict[str, str],
|
|
685
944
|
*, persisted_key: str, mention_keys: tuple[str, ...],
|
|
@@ -731,10 +990,16 @@ def prepare_handoff(
|
|
|
731
990
|
*, app_root: str, project_root: str | None, plugin_type: str,
|
|
732
991
|
intent: dict, artifact_paths: list[str], run_result: Any = None,
|
|
733
992
|
operation_result: Any = None, external_schemas: list[Any] | None = None,
|
|
993
|
+
runtime_metadata: list[Any] | None = None,
|
|
734
994
|
) -> dict:
|
|
735
995
|
if not isinstance(intent, dict):
|
|
736
996
|
raise HandoffError("intent must be a JSON object")
|
|
737
|
-
|
|
997
|
+
if runtime_metadata is not None:
|
|
998
|
+
if not runtime_metadata:
|
|
999
|
+
raise HandoffError("runtime metadata is empty", "PLUGIN_CONTEXT_INCOMPLETE")
|
|
1000
|
+
artifact_paths = []
|
|
1001
|
+
forms, entities, fields, registered_plugins = (runtime_catalog(runtime_metadata) if runtime_metadata
|
|
1002
|
+
else _catalog(artifact_paths, run_result))
|
|
738
1003
|
main_form = _exact_one(forms, intent.get("form"), ("formName", "formNumber"), "form")
|
|
739
1004
|
related = None
|
|
740
1005
|
if intent.get("relatedForm"):
|
|
@@ -748,6 +1013,12 @@ def prepare_handoff(
|
|
|
748
1013
|
if operation_result is None:
|
|
749
1014
|
raise HandoffError("type=operation requires a listOperations read-back via --operations-file")
|
|
750
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")
|
|
751
1022
|
missing_operations = [key for key in operation_keys if key not in verified_operations]
|
|
752
1023
|
if missing_operations:
|
|
753
1024
|
raise HandoffError(
|
|
@@ -851,7 +1122,15 @@ def prepare_handoff(
|
|
|
851
1122
|
"PLUGIN_SOURCE_NOT_FOUND",
|
|
852
1123
|
)
|
|
853
1124
|
class_name = class_name or existing.stem
|
|
854
|
-
|
|
1125
|
+
source_text = existing.read_text(encoding="utf-8-sig")
|
|
1126
|
+
existing_methods = method_signatures(source_text)
|
|
1127
|
+
declared_package = re.search(r"\bpackage\s+([A-Za-z_][\w.]*)\s*;", stripped(source_text))
|
|
1128
|
+
if declared_package:
|
|
1129
|
+
source_package = declared_package.group(1).replace(".", "/")
|
|
1130
|
+
if form_plugins and source_package != package_path:
|
|
1131
|
+
raise HandoffError("existing source package conflicts with the registered plugin class", "PLUGIN_CLASS_NOT_REGISTERED")
|
|
1132
|
+
package_path = source_package
|
|
1133
|
+
package_source = "existing source declaration"
|
|
855
1134
|
else:
|
|
856
1135
|
# An existing file at the create-new target means the class is already in this project,
|
|
857
1136
|
# so generating it again would either duplicate its methods or overwrite its logic. The
|
|
@@ -870,6 +1149,8 @@ def prepare_handoff(
|
|
|
870
1149
|
f'ownershipMode=modify with sourcePath "{target}"',
|
|
871
1150
|
"PLUGIN_OWNERSHIP_CONFLICT",
|
|
872
1151
|
)
|
|
1152
|
+
class_name = candidate or class_name
|
|
1153
|
+
source_path = str(target) if target else source_path
|
|
873
1154
|
|
|
874
1155
|
entity_selectors = intent.get("entities") or {}
|
|
875
1156
|
field_selectors = intent.get("fields") or {}
|
|
@@ -900,6 +1181,11 @@ def prepare_handoff(
|
|
|
900
1181
|
"PLUGIN_EXTERNAL_CONTRACT_UNRESOLVED",
|
|
901
1182
|
)
|
|
902
1183
|
external_catalog = _external_entity_catalog(external_schemas or [])
|
|
1184
|
+
if run_result is not None or runtime_metadata:
|
|
1185
|
+
external_catalog += [
|
|
1186
|
+
row for row in _artifact_external_catalog(entities, fields)
|
|
1187
|
+
if not any(existing["entityKey"] == row["entityKey"] for existing in external_catalog)
|
|
1188
|
+
]
|
|
903
1189
|
for key in ("externalContracts", "externalApis"):
|
|
904
1190
|
specialized[key] = _verify_external_contracts(specialized[key], external_catalog)
|
|
905
1191
|
entity_map = {
|
|
@@ -911,14 +1197,14 @@ def prepare_handoff(
|
|
|
911
1197
|
for label, selector in field_selectors.items()
|
|
912
1198
|
}
|
|
913
1199
|
logic_text = "\n".join(core_logic)
|
|
914
|
-
auto_entities = _bind_unique_mentions(
|
|
1200
|
+
auto_entities = ([] if runtime_metadata else _bind_unique_mentions(
|
|
915
1201
|
entities, logic_text, entity_map,
|
|
916
1202
|
persisted_key="entityKey", mention_keys=("entityKey", "entityName", "authoringId"),
|
|
917
|
-
)
|
|
918
|
-
auto_fields = _bind_unique_mentions(
|
|
1203
|
+
))
|
|
1204
|
+
auto_fields = ([] if runtime_metadata else _bind_unique_mentions(
|
|
919
1205
|
fields, logic_text, field_map,
|
|
920
1206
|
persisted_key="fieldKey", mention_keys=("fieldKey", "fieldName", "authoringId"),
|
|
921
|
-
)
|
|
1207
|
+
))
|
|
922
1208
|
# Built after auto-binding so a field the core logic mentioned by name carries the same type
|
|
923
1209
|
# evidence as one the intent selected explicitly.
|
|
924
1210
|
contracts_by_key: dict[str, dict | None] = {}
|
|
@@ -931,11 +1217,9 @@ def prepare_handoff(
|
|
|
931
1217
|
contracts_by_key[key] = None
|
|
932
1218
|
continue
|
|
933
1219
|
contracts_by_key[key] = contract
|
|
934
|
-
field_contracts = [
|
|
935
|
-
contracts_by_key[key]
|
|
936
|
-
|
|
937
|
-
if contracts_by_key.get(key)
|
|
938
|
-
]
|
|
1220
|
+
field_contracts = ([_field_contract(row) for row in fields if row["fieldKey"] in field_map.values()] if runtime_metadata else [
|
|
1221
|
+
contracts_by_key[key] for key in dict.fromkeys(field_map.values()) if contracts_by_key.get(key)
|
|
1222
|
+
])
|
|
939
1223
|
handoff = {
|
|
940
1224
|
"appRoot": str(Path(app_root).resolve()),
|
|
941
1225
|
"projectRoot": path_result["projectRoot"],
|
|
@@ -950,7 +1234,54 @@ def prepare_handoff(
|
|
|
950
1234
|
"fields": field_map,
|
|
951
1235
|
"coreLogic": core_logic,
|
|
952
1236
|
"specialized": specialized,
|
|
1237
|
+
"description": (str(intent.get("description") or "").strip() or core_logic[0])[:20],
|
|
953
1238
|
}
|
|
1239
|
+
if runtime_metadata:
|
|
1240
|
+
def graph(form: dict) -> dict:
|
|
1241
|
+
return {key: form[key] for key in ("formNumber", "formName", "metadataSource", "metadataVersion")} | {
|
|
1242
|
+
"entities": [{key: entity[key] for key in
|
|
1243
|
+
("entityKey", "entityName", "entityType", "role", "parentEntityKey", "collectionProperty", "collectionPath")}
|
|
1244
|
+
| {"fields": [_field_contract(row) for row in fields
|
|
1245
|
+
if row["formNumber"] == form["formNumber"] and row["entityKey"] == entity["entityKey"]]}
|
|
1246
|
+
for entity in entities if entity["formNumber"] == form["formNumber"]]}
|
|
1247
|
+
handoff["formGraph"] = graph(main_form)
|
|
1248
|
+
handoff["relatedForms"] = [graph(form) for form in forms if form["formNumber"] != main_form["formNumber"]]
|
|
1249
|
+
handoff["entities"].update({row["entityName"]: row["entityKey"] for row in entities
|
|
1250
|
+
if row["formNumber"] == main_form["formNumber"]})
|
|
1251
|
+
if operation_keys:
|
|
1252
|
+
handoff["operationContracts"] = operation_contracts(operation_result, operation_keys)
|
|
1253
|
+
if runtime_metadata:
|
|
1254
|
+
handoff["availableOperations"] = [{key: operation[key] for key in
|
|
1255
|
+
("operationKey", "operationName", "operationType") if key in operation}
|
|
1256
|
+
for operation in operation_contracts(operation_result, sorted(verified_operations))]
|
|
1257
|
+
handoff["stateFields"] = [{key: row[key] for key in
|
|
1258
|
+
("fieldKey", "fieldName", "fieldId", "entityKey", "comboOptions")}
|
|
1259
|
+
for row in fields if row.get("comboOptions")]
|
|
1260
|
+
warnings = []
|
|
1261
|
+
for operation in handoff["operationContracts"]:
|
|
1262
|
+
parameter = operation.get("parameter")
|
|
1263
|
+
if not isinstance(parameter, dict):
|
|
1264
|
+
continue
|
|
1265
|
+
target_id = parameter.get("StatusFieldId")
|
|
1266
|
+
if not target_id or "Value" not in parameter:
|
|
1267
|
+
continue
|
|
1268
|
+
targets = [row for row in fields if row["formNumber"] == main_form["formNumber"] and
|
|
1269
|
+
(row.get("fieldId") == target_id or str(row.get("metadataResourceId") or "").endswith("." + str(target_id) + ".Name"))]
|
|
1270
|
+
target = targets[0] if len(targets) == 1 else None
|
|
1271
|
+
if target:
|
|
1272
|
+
operation["statusFieldKey"] = target["fieldKey"]
|
|
1273
|
+
if target is None:
|
|
1274
|
+
warnings.append({"operationKey": operation["operationKey"], "statusFieldId": target_id,
|
|
1275
|
+
"value": parameter["Value"], "code": "OPERATION_STATE_TARGET_UNRESOLVED",
|
|
1276
|
+
"message": "Do not equate this operation with a business state until its status field is resolved."})
|
|
1277
|
+
continue
|
|
1278
|
+
options = target.get("comboOptions") or []
|
|
1279
|
+
if options and not any(str(option.get("value")) == str(parameter["Value"]) for option in options):
|
|
1280
|
+
warnings.append({"operationKey": operation["operationKey"], "fieldKey": target["fieldKey"],
|
|
1281
|
+
"value": parameter["Value"], "code": "OPERATION_STATE_VALUE_MISMATCH",
|
|
1282
|
+
"message": "Operation value is absent from the verified field options; resolve the business mapping before coding."})
|
|
1283
|
+
if warnings:
|
|
1284
|
+
handoff["stateWarnings"] = warnings
|
|
954
1285
|
if field_contracts:
|
|
955
1286
|
# `fields` stays a bare {label: fieldKey} map because it is what the coding step turns
|
|
956
1287
|
# into constants. The value types travel alongside it so a reference field cannot be
|
|
@@ -973,6 +1304,12 @@ def prepare_handoff(
|
|
|
973
1304
|
handoff["className"] = class_name
|
|
974
1305
|
if source_path:
|
|
975
1306
|
handoff["sourcePath"] = str(Path(source_path).expanduser().resolve())
|
|
1307
|
+
if class_name:
|
|
1308
|
+
handoff["fqcn"] = package_path.replace("/", ".") + "." + class_name
|
|
1309
|
+
try:
|
|
1310
|
+
handoff["codingEvidence"] = plugin_coding_evidence(plugin_type, _string_list(intent.get("scenes"), "scenes"))
|
|
1311
|
+
except ValueError as exc:
|
|
1312
|
+
raise HandoffError(str(exc), "PLUGIN_CODING_EVIDENCE_INVALID") from exc
|
|
976
1313
|
if ownership_mode == "modify":
|
|
977
1314
|
# The methods the target class already declares, always present for modify so an empty
|
|
978
1315
|
# list means an empty class body rather than an unread file. New logic belongs inside
|
|
@@ -998,18 +1335,86 @@ def prepare_handoff(
|
|
|
998
1335
|
}
|
|
999
1336
|
|
|
1000
1337
|
|
|
1338
|
+
def handoff_response(result: dict, *, full_output: bool = False) -> dict:
|
|
1339
|
+
"""Keep the CLI below the host output limit; the saved context remains complete."""
|
|
1340
|
+
if full_output:
|
|
1341
|
+
return {key: result[key] for key in ("status", "handoffPath", "handoff")}
|
|
1342
|
+
handoff = result["handoff"]
|
|
1343
|
+
coding = handoff.get("codingEvidence", {})
|
|
1344
|
+
graphs = [handoff["formGraph"], *handoff.get("relatedForms", [])] if handoff.get("formGraph") else []
|
|
1345
|
+
return {
|
|
1346
|
+
"status": result["status"],
|
|
1347
|
+
"handoffPath": result["handoffPath"],
|
|
1348
|
+
"contextMode": "reference-only",
|
|
1349
|
+
"handoff": {
|
|
1350
|
+
**{key: handoff[key] for key in (
|
|
1351
|
+
"appRoot", "projectRoot", "packagePath", "type", "ownershipMode", "formNumber",
|
|
1352
|
+
"className", "sourcePath", "fqcn", "operationKeys", "sourceGate",
|
|
1353
|
+
) if key in handoff},
|
|
1354
|
+
**({'targetSdk': {key: handoff['targetSdk'][key] for key in
|
|
1355
|
+
('status', 'projectRoot', 'module', 'evidenceScope') if key in handoff['targetSdk']}}
|
|
1356
|
+
if 'targetSdk' in handoff else {}),
|
|
1357
|
+
"codingEvidence": {
|
|
1358
|
+
**{key: coding[key] for key in ("codingEntry", "guide", "typeKnowledge", "apiLookup", "sdkLookup", "operationDispatch") if key in coding},
|
|
1359
|
+
"signatureTypes": coding.get("signatureTypes", [])[:12],
|
|
1360
|
+
"signatureTypeCount": len(coding.get("signatureTypes", [])),
|
|
1361
|
+
},
|
|
1362
|
+
},
|
|
1363
|
+
"metadata": {
|
|
1364
|
+
"formCount": len(graphs),
|
|
1365
|
+
"forms": [{
|
|
1366
|
+
**{key: graph[key] for key in ("formNumber", "metadataSource", "metadataVersion") if key in graph},
|
|
1367
|
+
"entityCount": len(graph.get("entities", [])),
|
|
1368
|
+
"fieldCount": sum(len(entity.get("fields", [])) for entity in graph.get("entities", [])),
|
|
1369
|
+
} for graph in graphs[:8]],
|
|
1370
|
+
},
|
|
1371
|
+
"contextSections": ["handoff.formGraph", "handoff.relatedForms", "handoff.specialized",
|
|
1372
|
+
"handoff.operationContracts", "handoff.stateFields", "handoff.stateWarnings",
|
|
1373
|
+
"handoff.codingEvidence", "handoff.reportContract", "handoff.targetSdk"],
|
|
1374
|
+
"instruction": (
|
|
1375
|
+
"This response is a summary, not the coding context. Keep the complete saved handoffPath "
|
|
1376
|
+
"for native compilation; begin with query_plugin_handoff.py coding-start, then query only missing field paths, collection hierarchy, external contracts, "
|
|
1377
|
+
"operation values, warnings and SDK signatures needed for this class with query_plugin_handoff.py. "
|
|
1378
|
+
"Index or find exact paths before get; do not read the whole JSON into the model. Use its exact codingEntry "
|
|
1379
|
+
"and verified signatureTypes imports; missing or ambiguous SDK types require the supplied API lookup. "
|
|
1380
|
+
"Pass handoffPath directly to compile_plugin_source.py --handoff; do not search for another file."
|
|
1381
|
+
),
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
|
|
1385
|
+
def assert_handoff_replace(destination: Path, result: dict, replace: bool = False) -> None:
|
|
1386
|
+
if not destination.is_file() or replace:
|
|
1387
|
+
return
|
|
1388
|
+
existing = _read_json(destination)
|
|
1389
|
+
previous = existing.get("handoff") if isinstance(existing, dict) and existing.get("status") is True else None
|
|
1390
|
+
current = result["handoff"]
|
|
1391
|
+
fields = ("projectRoot", "appRoot", "sourcePath", "fqcn", "formNumber", "type", "coreLogic")
|
|
1392
|
+
if not isinstance(previous, dict) or any(previous.get(key) != current.get(key) for key in fields) or sorted(
|
|
1393
|
+
previous.get("operationKeys", [])) != sorted(current.get("operationKeys", [])):
|
|
1394
|
+
raise HandoffError(
|
|
1395
|
+
f"Prepared class already has a different snapshot at {destination}. "
|
|
1396
|
+
"Review the existing snapshot and reconcile the class identity and business scope before explicitly replacing it with --replace-handoff.",
|
|
1397
|
+
"PLUGIN_HANDOFF_CONFLICT")
|
|
1398
|
+
|
|
1399
|
+
|
|
1001
1400
|
def main() -> None:
|
|
1002
1401
|
parser = argparse.ArgumentParser(description="Prepare a verified compact Java-plugin handoff")
|
|
1003
1402
|
parser.add_argument("--app-root", required=True)
|
|
1004
1403
|
parser.add_argument("--java-project-root", "--project-root", dest="project_root")
|
|
1005
1404
|
parser.add_argument("--type", required=True, dest="plugin_type")
|
|
1006
|
-
parser.add_argument("--
|
|
1405
|
+
parser.add_argument("--replace-handoff", action="store_true")
|
|
1406
|
+
artifacts = parser.add_mutually_exclusive_group()
|
|
1407
|
+
artifacts.add_argument("--artifact", action="append", dest="artifact_paths")
|
|
1408
|
+
artifacts.add_argument("--emit-manifest", help="Exact manifest path returned by the emitter; includes every declared artifact")
|
|
1007
1409
|
parser.add_argument("--run-result")
|
|
1008
|
-
parser.add_argument("--
|
|
1410
|
+
parser.add_argument("--form-number", help="Exact existing form number; independently reads runtime metadata")
|
|
1411
|
+
parser.add_argument("--operations-file", help="Reuse an existing listOperations result; otherwise read once automatically")
|
|
1412
|
+
parser.add_argument("--output", help="Save prepared context here; default is the app-local class handoff file")
|
|
1413
|
+
parser.add_argument("--full-output", action="store_true", help="Print the complete handoff JSON instead of its compact summary")
|
|
1414
|
+
parser.add_argument("--prepare-source", action="store_true", help="Mark this verified class writable for implementation; does not write Java source")
|
|
1009
1415
|
parser.add_argument(
|
|
1010
1416
|
"--external-schema", action="append", dest="external_schemas",
|
|
1011
|
-
help="JSON
|
|
1012
|
-
"every specialized.externalContracts entry that names an entity. Repeatable.",
|
|
1417
|
+
help="JSON from getFormSchema for an entity contract outside the verified artifact batch. Repeatable.",
|
|
1013
1418
|
)
|
|
1014
1419
|
intent_group = parser.add_mutually_exclusive_group(required=True)
|
|
1015
1420
|
intent_group.add_argument("--intent-json")
|
|
@@ -1017,21 +1422,62 @@ def main() -> None:
|
|
|
1017
1422
|
args = parser.parse_args()
|
|
1018
1423
|
try:
|
|
1019
1424
|
intent = json.loads(args.intent_json) if args.intent_json else _read_json(args.intent_file)
|
|
1425
|
+
if not isinstance(intent, dict):
|
|
1426
|
+
raise HandoffError("intent must be a JSON object")
|
|
1020
1427
|
run_result = _read_json(args.run_result) if args.run_result else None
|
|
1428
|
+
if run_result is not None and not isinstance(run_result, dict):
|
|
1429
|
+
raise HandoffError("write result must be a JSON object")
|
|
1021
1430
|
operation_result = _read_json(args.operations_file) if args.operations_file else None
|
|
1431
|
+
artifact_paths = (manifest_artifacts(args.emit_manifest) if args.emit_manifest else args.artifact_paths
|
|
1432
|
+
or (run_result or {}).get("artifactPaths"))
|
|
1433
|
+
runtime_metadata = None
|
|
1434
|
+
if args.form_number or not artifact_paths or args.plugin_type == "operation":
|
|
1435
|
+
form_number = args.form_number or str(intent.get("form") or "")
|
|
1436
|
+
if not args.form_number and artifact_paths:
|
|
1437
|
+
hints = (run_result or {}).get("forms") or _catalog(artifact_paths, run_result)[0]
|
|
1438
|
+
form_number = _exact_one(hints, intent.get("form"), ("formName", "formNumber"), "form")["formNumber"]
|
|
1439
|
+
if not re.fullmatch(r"[A-Za-z0-9_]+", form_number):
|
|
1440
|
+
raise HandoffError("independent preparation requires --form-number with an exact existing form code")
|
|
1441
|
+
runtime_metadata, operation_result = prepare_runtime_inputs(args.app_root, form_number, intent, args.plugin_type)
|
|
1442
|
+
intent["form"] = form_number
|
|
1443
|
+
artifact_paths = []
|
|
1444
|
+
else:
|
|
1445
|
+
operation_result, external_schemas = prepare_inputs(
|
|
1446
|
+
app_root=args.app_root, intent=intent, plugin_type=args.plugin_type,
|
|
1447
|
+
artifact_paths=artifact_paths, run_result=run_result, operation_result=operation_result,
|
|
1448
|
+
external_schemas=[_read_json(path) for path in args.external_schemas or []])
|
|
1022
1449
|
result = prepare_handoff(
|
|
1023
|
-
app_root=args.app_root,
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
run_result=run_result,
|
|
1029
|
-
operation_result=operation_result,
|
|
1030
|
-
external_schemas=[_read_json(path) for path in args.external_schemas or []],
|
|
1450
|
+
app_root=args.app_root, project_root=args.project_root, plugin_type=args.plugin_type,
|
|
1451
|
+
intent=intent, artifact_paths=artifact_paths, run_result=run_result,
|
|
1452
|
+
operation_result=operation_result, runtime_metadata=runtime_metadata,
|
|
1453
|
+
external_schemas=([_read_json(path) for path in args.external_schemas or []]
|
|
1454
|
+
if runtime_metadata else external_schemas),
|
|
1031
1455
|
)
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1456
|
+
destination = (Path(args.output).resolve() if args.output else Path(args.app_root).resolve()
|
|
1457
|
+
/ "_runtime" / "plugin-handoffs" / (result["handoff"]["className"] + ".json"))
|
|
1458
|
+
assert_handoff_replace(destination, result, args.replace_handoff)
|
|
1459
|
+
if args.prepare_source:
|
|
1460
|
+
from plugin_target_sdk import verify_target_sdk
|
|
1461
|
+
result['handoff']['targetSdk'] = verify_target_sdk(result['handoff']['projectRoot'],
|
|
1462
|
+
result['handoff']['codingEvidence'], intent.get('sdkUsage'))
|
|
1463
|
+
from plugin_source_gate import write_state, gate_path
|
|
1464
|
+
import os
|
|
1465
|
+
handoff = result["handoff"]
|
|
1466
|
+
if not handoff.get("className") or not handoff.get("sourcePath"):
|
|
1467
|
+
raise HandoffError("source preparation requires a resolved className and sourcePath")
|
|
1468
|
+
target = Path(handoff["sourcePath"]).resolve()
|
|
1469
|
+
if not target.is_relative_to(Path(handoff["projectRoot"]).resolve() / "src" / "main" / "java"):
|
|
1470
|
+
raise HandoffError("source preparation target is outside the verified Java module", "PLUGIN_SOURCE_INVALID")
|
|
1471
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
1472
|
+
write_state(handoff["projectRoot"], handoff["className"], "coding",
|
|
1473
|
+
os.environ.get("LINGEE_SESSION_ID") or os.environ.get("OPENCODE_SESSION_ID"), emit=False)
|
|
1474
|
+
handoff["sourceGate"] = {"state": "coding", "path": gate_path(handoff["projectRoot"], handoff["className"])}
|
|
1475
|
+
destination.parent.mkdir(parents=True, exist_ok=True)
|
|
1476
|
+
result["handoffPath"] = str(destination)
|
|
1477
|
+
destination.write_text(json.dumps(result, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
1478
|
+
print(json.dumps(handoff_response(result, full_output=args.full_output), ensure_ascii=False))
|
|
1479
|
+
except (HandoffError, json.JSONDecodeError, ValueError) as exc:
|
|
1480
|
+
error_code = getattr(exc, "error_code", getattr(exc, "code", "PLUGIN_HANDOFF_UNRESOLVED"))
|
|
1035
1481
|
print(json.dumps({"status": False, "errorCode": error_code, "message": str(exc)}, ensure_ascii=False))
|
|
1036
1482
|
raise SystemExit(2)
|
|
1037
1483
|
|