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