@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
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"""Durable per-artifact progress inside the existing runBatch result contract."""
|
|
2
|
+
import copy
|
|
3
|
+
import csv
|
|
4
|
+
import hashlib
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
import sys
|
|
9
|
+
import threading
|
|
10
|
+
import subprocess
|
|
11
|
+
import time
|
|
12
|
+
|
|
13
|
+
from .execution_errors import classify_execution_error
|
|
14
|
+
from execution_binding import execution_binding
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _atomic_json(path, value):
|
|
18
|
+
temporary = path.with_name(path.name + ".tmp")
|
|
19
|
+
temporary.write_text(json.dumps(value, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
20
|
+
os.replace(temporary, path)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _path(value):
|
|
24
|
+
return os.path.normcase(os.path.realpath(value))
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _targets(artifact):
|
|
28
|
+
return sorted(str(row.get("entityKey") or "").strip() for row in artifact.get("entities", [])
|
|
29
|
+
if isinstance(row, dict) and not row.get("parentEntityId"))
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class BuildReceipts:
|
|
33
|
+
def __init__(self, app_root, app_id, environment, artifacts, *, resume_result=None, options=None, reconcile_only=False):
|
|
34
|
+
self.root = Path(app_root).resolve()
|
|
35
|
+
self.output = self.root / "_runtime" / "output"
|
|
36
|
+
self.output.mkdir(parents=True, exist_ok=True)
|
|
37
|
+
self.lock = threading.Lock()
|
|
38
|
+
self.artifacts = artifacts
|
|
39
|
+
self.options = options or []
|
|
40
|
+
self.identity = {"appRoot": str(self.root), "bizAppId": str(app_id), "environment": environment, "executionOptions": self.options}
|
|
41
|
+
self.previous = {}
|
|
42
|
+
if resume_result:
|
|
43
|
+
prior = json.loads(Path(resume_result).read_text(encoding="utf-8-sig"))
|
|
44
|
+
if reconcile_only and not prior.get("executionFinished"):
|
|
45
|
+
pid = prior.get("processId")
|
|
46
|
+
if not isinstance(pid, int):
|
|
47
|
+
raise ValueError("BATCH_WRITE_RECONCILIATION_REQUIRED: legacy receipt has no process boundary")
|
|
48
|
+
if os.name == "nt":
|
|
49
|
+
tasks = subprocess.check_output(["tasklist", "/FI", f"PID eq {pid}", "/FO", "CSV", "/NH"], text=True)
|
|
50
|
+
active = any(len(row) > 1 and row[1] == str(pid) for row in csv.reader(tasks.splitlines()))
|
|
51
|
+
else:
|
|
52
|
+
try:
|
|
53
|
+
os.kill(pid, 0)
|
|
54
|
+
active = True
|
|
55
|
+
except ProcessLookupError:
|
|
56
|
+
active = False
|
|
57
|
+
if active:
|
|
58
|
+
raise ValueError("BATCH_WRITE_RECONCILIATION_REQUIRED: original execution is still active")
|
|
59
|
+
if prior.get("batchIdentity") != self.identity:
|
|
60
|
+
raise ValueError("BATCH_INPUT_IDENTITY_CHANGED: resume requires the same application and environment")
|
|
61
|
+
if prior.get("executionBinding") and prior["executionBinding"] != execution_binding() and not reconcile_only:
|
|
62
|
+
raise ValueError("SKILL_REVISION_MISMATCH: reconcile the old receipt with the current bound source first")
|
|
63
|
+
self.previous = {_path(row["artifactPath"]): row for row in prior.get("results", [])}
|
|
64
|
+
if set(self.previous) != {_path(p) for p in artifacts}:
|
|
65
|
+
raise ValueError("BATCH_INPUT_ARTIFACTS_CHANGED: resume with the original paths; correct only unwritten inputs in place")
|
|
66
|
+
self.receipt = {"processId": os.getpid(), "executionBinding": execution_binding(), "batchIdentity": self.identity, "appRoot": str(self.root),
|
|
67
|
+
"artifactPaths": list(artifacts), "results": [],
|
|
68
|
+
"resultPath": str(self.output / f"runBatch.{time.time_ns()}.json")}
|
|
69
|
+
self.result_path = Path(self.receipt["resultPath"])
|
|
70
|
+
seen = set()
|
|
71
|
+
for source, artifact in artifacts.items():
|
|
72
|
+
targets = _targets(artifact)
|
|
73
|
+
if (not targets or any(not target for target in targets)
|
|
74
|
+
or len({number.casefold() for number in targets}) != len(targets)
|
|
75
|
+
or seen.intersection(number.casefold() for number in targets)):
|
|
76
|
+
raise ValueError("BATCH_INPUT_TARGET_CONFLICT: each artifact needs unique exact root entity keys")
|
|
77
|
+
seen.update(number.casefold() for number in targets)
|
|
78
|
+
revision = hashlib.sha256(Path(source).read_bytes()).hexdigest()
|
|
79
|
+
prior = self.previous.get(_path(source))
|
|
80
|
+
if prior and (prior.get("ok") or prior.get("writeAttempted") is not False):
|
|
81
|
+
if not reconcile_only and (not prior.get("ok") or (prior.get("verification") or {}).get("status") != "pass"):
|
|
82
|
+
raise ValueError("BATCH_WRITE_RECONCILIATION_REQUIRED: inspect retained/unknown targets from " + str(resume_result))
|
|
83
|
+
if prior.get("inputRevision") != revision:
|
|
84
|
+
raise ValueError("BATCH_INPUT_SAVED_TARGET_CHANGED: use an existing-form writer for the affected object and references")
|
|
85
|
+
row = copy.deepcopy(prior) if prior and (prior.get("ok") or reconcile_only) else {
|
|
86
|
+
"artifact": Path(source).name, "artifactPath": source, "inputRevision": revision,
|
|
87
|
+
"targetNumbers": targets, "ok": False, "phase": "notStarted",
|
|
88
|
+
"writeAttempted": False, "elapsed_sec": 0.0,
|
|
89
|
+
}
|
|
90
|
+
row["reusedReceipt"] = bool(prior and prior.get("ok"))
|
|
91
|
+
self.receipt["results"].append(row)
|
|
92
|
+
self._save()
|
|
93
|
+
|
|
94
|
+
def _save(self):
|
|
95
|
+
rows = self.receipt["results"]
|
|
96
|
+
self.receipt["success"] = sum(row.get("ok") is True for row in rows)
|
|
97
|
+
self.receipt["failed"] = len(rows) - self.receipt["success"]
|
|
98
|
+
self.receipt["total"] = len(rows)
|
|
99
|
+
self.receipt["resume"] = {"python": sys.executable, "entry": execution_binding()["cosmicApi"], "command": "runBatch",
|
|
100
|
+
"arguments": ["--stdout", "runBatch", "--app-root", str(self.root),
|
|
101
|
+
"--resume-result", str(self.result_path), "--verify",
|
|
102
|
+
*self.options, "--artifacts", *self.artifacts]}
|
|
103
|
+
unresolved = [row for row in rows if row.get("writeAttempted") is not False
|
|
104
|
+
and (not row.get("ok") or (row.get("verification") or {}).get("status") != "pass")]
|
|
105
|
+
self.receipt["resume"]["ready"] = not unresolved
|
|
106
|
+
self.receipt["reconcile"] = {"python": sys.executable, "entry": execution_binding()["cosmicApi"],
|
|
107
|
+
"arguments": [*self.receipt["resume"]["arguments"], "--reconcile-only"],
|
|
108
|
+
"requiresOriginalExecutionFinished": True, "platformEffect": "read"}
|
|
109
|
+
self.receipt["reconciliation"] = [{
|
|
110
|
+
"python": sys.executable, "entry": execution_binding()["cosmicApi"],
|
|
111
|
+
"artifactPath": row["artifactPath"],
|
|
112
|
+
"arguments": ["--stdout", "queryForms", "--app-root", str(self.root), "--keyword", number],
|
|
113
|
+
"instruction": "Read exact identity first; an arbitrary same-number object is not this batch's success receipt",
|
|
114
|
+
} for row in unresolved for number in row["targetNumbers"]]
|
|
115
|
+
self.receipt["notStartedArtifacts"] = [row["artifactPath"] for row in rows if row.get("writeAttempted") is False and not row.get("ok")]
|
|
116
|
+
_atomic_json(self.result_path, self.receipt)
|
|
117
|
+
|
|
118
|
+
def current(self, source):
|
|
119
|
+
return next(copy.deepcopy(row) for row in self.receipt["results"] if _path(row["artifactPath"]) == _path(source))
|
|
120
|
+
|
|
121
|
+
def reused(self, source):
|
|
122
|
+
return next((copy.deepcopy(row) for row in self.receipt["results"]
|
|
123
|
+
if _path(row["artifactPath"]) == _path(source) and row.get("reusedReceipt")), None)
|
|
124
|
+
|
|
125
|
+
def start(self, source):
|
|
126
|
+
"""Claim exact target identities before dispatch; interrupted claims stay unknown."""
|
|
127
|
+
with self.lock:
|
|
128
|
+
row = next(row for row in self.receipt["results"] if _path(row["artifactPath"]) == _path(source))
|
|
129
|
+
claims = []
|
|
130
|
+
for number in row["targetNumbers"]:
|
|
131
|
+
key = hashlib.sha256(json.dumps([{key: value for key, value in self.identity.items() if key != "executionOptions"}, number.casefold()], sort_keys=True).encode()).hexdigest()
|
|
132
|
+
claim = self.output / f"runBatch.target.{key}.json"
|
|
133
|
+
if claim.exists():
|
|
134
|
+
owner = json.loads(claim.read_text(encoding="utf-8"))
|
|
135
|
+
raise ValueError("BATCH_WRITE_RECONCILIATION_REQUIRED: previous write receipt " + owner["resultPath"])
|
|
136
|
+
claims.append(claim)
|
|
137
|
+
row.update(phase="writeUnknown", writeAttempted=None,
|
|
138
|
+
claimPaths=[str(p) for p in claims], claimOwner=str(self.result_path))
|
|
139
|
+
self._save()
|
|
140
|
+
# O_EXCL protects independent processes as well as threads.
|
|
141
|
+
acquired = []
|
|
142
|
+
try:
|
|
143
|
+
for claim in claims:
|
|
144
|
+
with claim.open("x", encoding="utf-8") as handle:
|
|
145
|
+
json.dump({"resultPath": str(self.result_path), "artifactPath": source}, handle)
|
|
146
|
+
acquired.append(claim)
|
|
147
|
+
except FileExistsError:
|
|
148
|
+
for claim in acquired:
|
|
149
|
+
claim.unlink()
|
|
150
|
+
raise ValueError("BATCH_WRITE_RECONCILIATION_REQUIRED: concurrent creation owns this target")
|
|
151
|
+
|
|
152
|
+
def finish(self, source, result):
|
|
153
|
+
with self.lock:
|
|
154
|
+
row = next(row for row in self.receipt["results"] if _path(row["artifactPath"]) == _path(source))
|
|
155
|
+
row.update(result)
|
|
156
|
+
row["artifactPath"] = source
|
|
157
|
+
if row.get("writeAttempted") is False:
|
|
158
|
+
for claim in row.get("claimPaths", []):
|
|
159
|
+
path = Path(claim)
|
|
160
|
+
if path.is_file() and json.loads(path.read_text(encoding="utf-8")).get("resultPath") == row.get("claimOwner"):
|
|
161
|
+
path.unlink()
|
|
162
|
+
row.pop("claimPaths", None)
|
|
163
|
+
row.pop("claimOwner", None)
|
|
164
|
+
if row.get("ok"):
|
|
165
|
+
row.pop("errorCategory", None)
|
|
166
|
+
row.pop("capabilityLimitConfirmed", None)
|
|
167
|
+
if not row.get("ok"):
|
|
168
|
+
row["errorCategory"] = classify_execution_error(row)
|
|
169
|
+
row["capabilityLimitConfirmed"] = row["errorCategory"] == "capability_limit"
|
|
170
|
+
self._save()
|
|
171
|
+
return copy.deepcopy(row)
|
|
@@ -1280,13 +1280,32 @@ def do_add_unit(cfg, token, biz_app_id=None, biz_app_number=None,
|
|
|
1280
1280
|
else:
|
|
1281
1281
|
raise ValueError("addUnit 需要 --bizAppId 或 --bizAppNumber 之一")
|
|
1282
1282
|
|
|
1283
|
+
if not str(number or "").strip():
|
|
1284
|
+
return {
|
|
1285
|
+
"status": False, "errorCode": "FUNCTIONAL_GROUP_CODE_REQUIRED",
|
|
1286
|
+
"message": "addUnit requires a non-empty number; preserve the requested functional group code.",
|
|
1287
|
+
"writeAttempted": False,
|
|
1288
|
+
}
|
|
1283
1289
|
payload["name"] = name
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
if
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
+
payload["number"] = number
|
|
1291
|
+
payload["description"] = name if description is None else description
|
|
1292
|
+
if seq is None:
|
|
1293
|
+
if parent_unit_id not in (None, ""):
|
|
1294
|
+
return {"status": False, "errorCode": "FUNCTIONAL_GROUP_SEQ_UNAVAILABLE", "message": "A nested group needs an explicit sibling --seq", "writeAttempted": False}
|
|
1295
|
+
try:
|
|
1296
|
+
existing = do_list_units(cfg, token, biz_app_id=biz_app_id, biz_app_number=biz_app_number)
|
|
1297
|
+
except Exception as exc:
|
|
1298
|
+
return {"status": False, "errorCode": "FUNCTIONAL_GROUP_SEQ_UNAVAILABLE", "message": f"Cannot choose the next group sequence: {exc}", "writeAttempted": False}
|
|
1299
|
+
units = (existing.get("data") or {}).get("units") if isinstance(existing, dict) else None
|
|
1300
|
+
if not isinstance(units, list) or not _is_api_success(existing):
|
|
1301
|
+
return {"status": False, "errorCode": "FUNCTIONAL_GROUP_SEQ_UNAVAILABLE", "message": "Cannot read existing groups to choose the next sequence", "writeAttempted": False}
|
|
1302
|
+
known = [int(unit["seq"]) if isinstance(unit, dict) and str(unit.get("seq") if unit.get("seq") is not None else "").isdigit() else None for unit in units]
|
|
1303
|
+
if any(value is None for value in known):
|
|
1304
|
+
return {"status": False, "errorCode": "FUNCTIONAL_GROUP_SEQ_UNAVAILABLE", "message": "Existing group sequence is unavailable; pass an explicit --seq", "writeAttempted": False}
|
|
1305
|
+
seq = max(known, default=0) + 1
|
|
1306
|
+
if not isinstance(seq, int) or not 1 <= seq < 1000:
|
|
1307
|
+
return {"status": False, "errorCode": "FUNCTIONAL_GROUP_SEQ_INVALID", "message": "Group sequence must be an integer from 1 to 999", "writeAttempted": False}
|
|
1308
|
+
payload["seq"] = seq
|
|
1290
1309
|
if parent_unit_id is not None:
|
|
1291
1310
|
payload["parentUnitId"] = parent_unit_id
|
|
1292
1311
|
if is_leaf is not None:
|
|
@@ -27,6 +27,14 @@ _PARENT_TYPES = {
|
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
|
|
30
|
+
class ControlInputError(ValueError):
|
|
31
|
+
def __init__(self, missing_fields):
|
|
32
|
+
super().__init__(
|
|
33
|
+
"CONTROL_INPUT_INVALID: required fields missing: " + ", ".join(missing_fields)
|
|
34
|
+
)
|
|
35
|
+
self.recovery = {"missingFields": missing_fields}
|
|
36
|
+
|
|
37
|
+
|
|
30
38
|
@lru_cache(maxsize=1)
|
|
31
39
|
def _control_contract():
|
|
32
40
|
with (_CONTRACT or contract_path("control-authoring.json")).open("r", encoding="utf-8-sig") as stream:
|
|
@@ -132,7 +140,22 @@ def validate_control_batch_input(intent):
|
|
|
132
140
|
unknown = sorted(set(intent) - {"parent", "panel", "controls"})
|
|
133
141
|
if unknown:
|
|
134
142
|
raise ValueError(f"CONTROL_INPUT_INVALID: unknown top-level keys {unknown}")
|
|
143
|
+
controls = intent.get("controls")
|
|
144
|
+
missing_fields = (["controls"] if controls is None else []) + [
|
|
145
|
+
f"controls[{index}].{field}"
|
|
146
|
+
for index, item in enumerate(controls if isinstance(controls, list) else [])
|
|
147
|
+
if isinstance(item, dict)
|
|
148
|
+
for field in ("type", "name") if not str(item.get(field) or "").strip()
|
|
149
|
+
]
|
|
135
150
|
panel_input = intent.get("panel")
|
|
151
|
+
if isinstance(panel_input, dict) and not str(panel_input.get("name") or "").strip():
|
|
152
|
+
missing_fields.append("panel.name")
|
|
153
|
+
if missing_fields:
|
|
154
|
+
raise ControlInputError(missing_fields)
|
|
155
|
+
if not isinstance(controls, list) or not controls:
|
|
156
|
+
raise ValueError("CONTROL_INPUT_INVALID: controls must be a non-empty array")
|
|
157
|
+
if len(controls) > 50:
|
|
158
|
+
raise ValueError("CONTROL_BATCH_TOO_LARGE: at most 50 controls per call")
|
|
136
159
|
if panel_input is not None and intent.get("parent"):
|
|
137
160
|
raise ValueError(
|
|
138
161
|
"CONTROL_INPUT_INVALID: use panel.parent when panel is declared"
|
|
@@ -148,10 +171,6 @@ def validate_control_batch_input(intent):
|
|
|
148
171
|
)
|
|
149
172
|
panel_name = str(panel_input.get("name") or "").strip()
|
|
150
173
|
panel_parent = str(panel_input.get("parent") or "").strip()
|
|
151
|
-
if not panel_name or not panel_parent:
|
|
152
|
-
raise ValueError(
|
|
153
|
-
"CONTROL_INPUT_INVALID: panel.name and panel.parent are required"
|
|
154
|
-
)
|
|
155
174
|
panel_key = str(panel_input.get("key") or "").strip() or (
|
|
156
175
|
"panel_" + hashlib.sha1(panel_name.encode("utf-8")).hexdigest()[:12]
|
|
157
176
|
)
|
|
@@ -172,11 +191,6 @@ def validate_control_batch_input(intent):
|
|
|
172
191
|
"controlDefaults": presentation,
|
|
173
192
|
}
|
|
174
193
|
default_parent = panel["key"] if panel else str(intent.get("parent") or "").strip()
|
|
175
|
-
controls = intent.get("controls")
|
|
176
|
-
if not isinstance(controls, list) or not controls:
|
|
177
|
-
raise ValueError("CONTROL_INPUT_INVALID: controls must be a non-empty array")
|
|
178
|
-
if len(controls) > 50:
|
|
179
|
-
raise ValueError("CONTROL_BATCH_TOO_LARGE: at most 50 controls per call")
|
|
180
194
|
|
|
181
195
|
contract = _control_contract()
|
|
182
196
|
normalized = []
|
|
@@ -191,8 +205,6 @@ def validate_control_batch_input(intent):
|
|
|
191
205
|
)
|
|
192
206
|
control_type = _resolve_control_type(item.get("type"), contract)
|
|
193
207
|
name = str(item.get("name") or "").strip()
|
|
194
|
-
if not name:
|
|
195
|
-
raise ValueError(f"CONTROL_INPUT_INVALID: controls[{index}].name is required")
|
|
196
208
|
key = str(item.get("key") or "").strip() or (
|
|
197
209
|
"ctl_" + hashlib.sha1(
|
|
198
210
|
f"{control_type}:{name}".encode("utf-8")
|
|
@@ -207,11 +219,6 @@ def validate_control_batch_input(intent):
|
|
|
207
219
|
raise ValueError(f"CONTROL_KEY_DUPLICATE: {key}")
|
|
208
220
|
seen.add(key.casefold())
|
|
209
221
|
parent = str(item.get("parent") or default_parent).strip()
|
|
210
|
-
if not parent:
|
|
211
|
-
raise ValueError(
|
|
212
|
-
f"CONTROL_PARENT_REQUIRED: controls[{index}] requires parent "
|
|
213
|
-
"or one top-level parent"
|
|
214
|
-
)
|
|
215
222
|
if panel and parent != panel["key"]:
|
|
216
223
|
raise ValueError(
|
|
217
224
|
f"CONTROL_PANEL_CHILD_PARENT_CONFLICT: controls[{index}] "
|
|
@@ -257,6 +264,29 @@ def validate_control_batch_input(intent):
|
|
|
257
264
|
return {"panel": panel, "controls": normalized}
|
|
258
265
|
|
|
259
266
|
|
|
267
|
+
def control_input_schema():
|
|
268
|
+
"""Publish the request shape and type properties from the validator's contract."""
|
|
269
|
+
contract = _control_contract()
|
|
270
|
+
return {
|
|
271
|
+
**contract["inputSchema"],
|
|
272
|
+
"x-commonProperties": {
|
|
273
|
+
name: {key: value for key, value in declaration.items() if key != "readProperty"}
|
|
274
|
+
for name, declaration in contract["commonProperties"].items()
|
|
275
|
+
},
|
|
276
|
+
"x-controlTypes": {
|
|
277
|
+
control_type: {
|
|
278
|
+
"aliases": spec.get("aliases") or [],
|
|
279
|
+
"properties": {
|
|
280
|
+
name: {key: value for key, value in declaration.items() if key != "readProperty"}
|
|
281
|
+
for name, declaration in _property_contract(control_type, contract).items()
|
|
282
|
+
if name not in contract["commonProperties"]
|
|
283
|
+
},
|
|
284
|
+
}
|
|
285
|
+
for control_type, spec in contract["releasedTypes"].items()
|
|
286
|
+
},
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
|
|
260
290
|
def control_catalog():
|
|
261
291
|
"""Return the released/specialized/deferred screenshot-control surface."""
|
|
262
292
|
contract = _control_contract()
|
|
@@ -291,8 +321,10 @@ def _layout_index(root):
|
|
|
291
321
|
return nodes
|
|
292
322
|
|
|
293
323
|
|
|
294
|
-
def _resolve_parent(selector, nodes):
|
|
295
|
-
wanted = str(selector or "").strip()
|
|
324
|
+
def _resolve_parent(selector, nodes, root_key=""):
|
|
325
|
+
wanted = str(selector or root_key or "").strip()
|
|
326
|
+
if not wanted:
|
|
327
|
+
raise ValueError("CONTROL_PARENT_UNRESOLVED: live form root has no key")
|
|
296
328
|
if wanted in nodes:
|
|
297
329
|
matches = [wanted]
|
|
298
330
|
else:
|
|
@@ -347,12 +379,14 @@ def do_add_controls(cfg, token, form_id, form_number, intent):
|
|
|
347
379
|
"CONTROL_FORM_READ_FAILED: "
|
|
348
380
|
+ (_extract_error_message(before) or "getFormSchema failed")
|
|
349
381
|
)
|
|
350
|
-
|
|
382
|
+
before_layout = (before.get("data") or {}).get("formLayout") or {}
|
|
383
|
+
before_nodes = _layout_index(before_layout)
|
|
384
|
+
root_key = str(before_layout.get("key") or "").strip()
|
|
351
385
|
panel = normalized.get("panel")
|
|
352
386
|
panel_parent = None
|
|
353
387
|
structural_ops = []
|
|
354
388
|
if panel:
|
|
355
|
-
panel_parent = _resolve_parent(panel["parent"], before_nodes)
|
|
389
|
+
panel_parent = _resolve_parent(panel["parent"], before_nodes, root_key)
|
|
356
390
|
current_panel = before_nodes.get(panel["key"])
|
|
357
391
|
if current_panel:
|
|
358
392
|
node = current_panel.get("node") or {}
|
|
@@ -396,7 +430,7 @@ def do_add_controls(cfg, token, form_id, form_number, intent):
|
|
|
396
430
|
existing = []
|
|
397
431
|
moved = []
|
|
398
432
|
for item in managed:
|
|
399
|
-
parent = panel["key"] if panel else _resolve_parent(item["parent"], before_nodes)
|
|
433
|
+
parent = panel["key"] if panel else _resolve_parent(item["parent"], before_nodes, root_key)
|
|
400
434
|
if not panel:
|
|
401
435
|
parent_type = str(
|
|
402
436
|
((before_nodes.get(parent) or {}).get("node") or {}).get("type") or ""
|
|
@@ -813,7 +813,7 @@ def _qf_resolve_target_entity(entity_key, entry, entity_name, entity_list, form_
|
|
|
813
813
|
}
|
|
814
814
|
|
|
815
815
|
if entity_name:
|
|
816
|
-
matched = [e for e in entity_list if e.get("entityName")
|
|
816
|
+
matched = [e for e in entity_list if entity_name in {e.get("entityName"), e.get("entityKey")}]
|
|
817
817
|
if len(matched) == 1:
|
|
818
818
|
e = matched[0]
|
|
819
819
|
return e["entityKey"], {
|
|
@@ -1486,6 +1486,14 @@ def _combo_verify_after_modify(cfg, token, form_number, field_key,
|
|
|
1486
1486
|
observed_options = []
|
|
1487
1487
|
read = _api_call(cfg, token, "loadFullMetadata",
|
|
1488
1488
|
{"formNumber": form_number}, method="GET")
|
|
1489
|
+
if not _is_api_success(read):
|
|
1490
|
+
return {
|
|
1491
|
+
"status": "readback_failed",
|
|
1492
|
+
"exit_code": QUICKFIELD_EXIT["api_error"],
|
|
1493
|
+
"message": "Combo options could not be read back; their saved state is unknown. "
|
|
1494
|
+
+ (_extract_error_message(read) or "loadFullMetadata failed"),
|
|
1495
|
+
"cause": read,
|
|
1496
|
+
}
|
|
1489
1497
|
if _is_api_success(read):
|
|
1490
1498
|
section = ((read.get("data") or {}).get("entitymeta") or {})
|
|
1491
1499
|
items = section.get("Items") or []
|
|
@@ -1525,8 +1533,7 @@ def _combo_verify_after_modify(cfg, token, form_number, field_key,
|
|
|
1525
1533
|
"status": "modify_failed",
|
|
1526
1534
|
"exit_code": QUICKFIELD_EXIT["api_error"],
|
|
1527
1535
|
"message": f"modifyMeta reported success but these options are absent on read-back: {missing}. "
|
|
1528
|
-
"
|
|
1529
|
-
"value with the field key.",
|
|
1536
|
+
"Check the saved target and options before retrying.",
|
|
1530
1537
|
"options": observed_options,
|
|
1531
1538
|
}
|
|
1532
1539
|
return {"status": "ok", "exit_code": QUICKFIELD_EXIT["ok"],
|
|
@@ -1779,6 +1779,12 @@ def do_add_field(cfg, token, form_name, field_name, biz_word,
|
|
|
1779
1779
|
# modelType, accepting the shortened spelling some deployments return. `writeAttempted`
|
|
1780
1780
|
# covers the field write only: target preparation above may already have extended the form,
|
|
1781
1781
|
# so report that separately rather than implying nothing happened.
|
|
1782
|
+
if field_type == "RefBillField" and str(editable.get("modelType") or "").strip().casefold() in {"baseformmodel", "base"}:
|
|
1783
|
+
return _extension_terminal(
|
|
1784
|
+
"REF_BILL_OWNER_UNSUPPORTED",
|
|
1785
|
+
"The current RefBillField writer requires a bill form. Resolve model/reference semantics; do not substitute a field type automatically.",
|
|
1786
|
+
"preflight", extra={"editableTarget": editable, "writeAttempted": False,
|
|
1787
|
+
"extensionPerformed": bool(prepared_data.get("extensionPerformed"))})
|
|
1782
1788
|
if str(editable.get("modelType") or "").strip().casefold() in {"reportformmodel", "report"}:
|
|
1783
1789
|
return _extension_terminal(
|
|
1784
1790
|
"REPORT_FIELD_NOT_ADDABLE",
|
|
@@ -1952,6 +1958,17 @@ def do_add_field(cfg, token, form_name, field_name, biz_word,
|
|
|
1952
1958
|
"verification": detail_verification,
|
|
1953
1959
|
},
|
|
1954
1960
|
}
|
|
1961
|
+
_BATCH_FIELD_OPTIONS = {
|
|
1962
|
+
"basedata": "basedata", "mulTable": "mul_table", "unitField": "unit_field",
|
|
1963
|
+
"flexType": "flex_type", "refBaseField": "ref_base_field",
|
|
1964
|
+
"refDisplayProp": "ref_display_prop", "asstTypeId": "asst_type_id", "asstType": "asst_type",
|
|
1965
|
+
}
|
|
1966
|
+
|
|
1967
|
+
|
|
1968
|
+
def _batch_field_options(field):
|
|
1969
|
+
return {argument: field[key] for key, argument in _BATCH_FIELD_OPTIONS.items() if key in field}
|
|
1970
|
+
|
|
1971
|
+
|
|
1955
1972
|
def validate_add_fields_input(fields):
|
|
1956
1973
|
if not isinstance(fields, list) or not fields:
|
|
1957
1974
|
raise ValueError("FIELD_BATCH_INVALID: expected a non-empty array")
|
|
@@ -1967,8 +1984,20 @@ def validate_add_fields_input(fields):
|
|
|
1967
1984
|
if "combo" in field or "comboJson" in field:
|
|
1968
1985
|
raise ValueError(f"FIELD_BATCH_INVALID: fields[{index}] comboOptions conflicts with combo/comboJson")
|
|
1969
1986
|
field["comboJson"] = field.pop("comboOptions")
|
|
1970
|
-
if
|
|
1971
|
-
|
|
1987
|
+
if "reference" in field:
|
|
1988
|
+
reference = field["reference"]
|
|
1989
|
+
if (field.get("fieldType") not in {"BasedataField", "MulBasedataField"}
|
|
1990
|
+
or not isinstance(reference, dict) or reference.get("mode") != "platform"
|
|
1991
|
+
or set(reference) - {"mode", "formNumber"}
|
|
1992
|
+
or not isinstance(reference.get("formNumber"), str) or not reference["formNumber"].strip()):
|
|
1993
|
+
raise ValueError(f"FIELD_BATCH_INVALID: fields[{index}].reference requires a platform formNumber for BasedataField/MulBasedataField")
|
|
1994
|
+
if "basedata" in field and field["basedata"] != reference["formNumber"]:
|
|
1995
|
+
raise ValueError(f"FIELD_BATCH_INVALID: fields[{index}].reference conflicts with basedata")
|
|
1996
|
+
field["basedata"] = reference["formNumber"]
|
|
1997
|
+
field.pop("reference")
|
|
1998
|
+
unknown = set(field) - {"name", "bizWord", "fieldType", "combo", "comboJson", "required", "panel", "before", "after", "fullLine"} - set(_BATCH_FIELD_OPTIONS)
|
|
1999
|
+
if unknown:
|
|
2000
|
+
raise ValueError(f"FIELD_BATCH_INVALID: fields[{index}] has unsupported keys: {sorted(unknown)}")
|
|
1972
2001
|
if "required" in field and not isinstance(field["required"], bool):
|
|
1973
2002
|
raise ValueError(f"FIELD_BATCH_INVALID: fields[{index}].required must be boolean")
|
|
1974
2003
|
if "fullLine" in field and not isinstance(field["fullLine"], bool):
|
|
@@ -1989,7 +2018,8 @@ def validate_add_fields_input(fields):
|
|
|
1989
2018
|
"comboJson/comboOptions with explicit stable values"
|
|
1990
2019
|
)
|
|
1991
2020
|
result = validate_add_field_input(field.get("name"), field.get("bizWord"), field.get("fieldType", "TextField"),
|
|
1992
|
-
combo=field.get("combo"), combo_json=field.get("comboJson")
|
|
2021
|
+
combo=field.get("combo"), combo_json=field.get("comboJson"),
|
|
2022
|
+
**_batch_field_options(field))
|
|
1993
2023
|
if not result.get("status"):
|
|
1994
2024
|
raise ValueError(f"FIELD_BATCH_INVALID: fields[{index}]: {result.get('message')}")
|
|
1995
2025
|
if field["bizWord"] in words:
|
|
@@ -2016,7 +2046,7 @@ def do_add_fields(cfg, token, form_name, fields, *, app_root=None, project_root=
|
|
|
2016
2046
|
field_type=field.get("fieldType", "TextField"), combo=field.get("combo"), combo_json=field.get("comboJson"),
|
|
2017
2047
|
required=field.get("required", False), app_root=app_root, project_root=project_root,
|
|
2018
2048
|
app_name=app_name, app_code=app_code, entry=entry, entity_name=entity_name, entity_key=entity_key,
|
|
2019
|
-
_prepared=prepared)
|
|
2049
|
+
_prepared=prepared, **_batch_field_options(field))
|
|
2020
2050
|
results.append(result)
|
|
2021
2051
|
if not _is_api_success(result) or not (result.get("data") or {}).get("verified"):
|
|
2022
2052
|
return {"status": False, "errorCode": "FIELD_BATCH_INCOMPLETE",
|
|
@@ -2062,6 +2092,32 @@ def summary_delete_dependencies(metadata, fields):
|
|
|
2062
2092
|
return dependencies
|
|
2063
2093
|
|
|
2064
2094
|
|
|
2095
|
+
def rule_delete_dependencies(metadata, fields):
|
|
2096
|
+
"""Find persisted rule references, excluding prose and unrelated layout links."""
|
|
2097
|
+
keys = {field["fieldKey"] for field in fields}
|
|
2098
|
+
patterns = {key: _re.compile(r"(?<![A-Za-z0-9_])" + _re.escape(key) + r"(?![A-Za-z0-9_])") for key in keys}
|
|
2099
|
+
reference_slots = {"precondition", "condition", "triggerfield", "fieldnames", "fieldname",
|
|
2100
|
+
"targetfield", "srcfield", "expression", "fieldkey"}
|
|
2101
|
+
found = []
|
|
2102
|
+
|
|
2103
|
+
def visit(value, rule=None, path=""):
|
|
2104
|
+
if isinstance(value, list):
|
|
2105
|
+
for index, child in enumerate(value):
|
|
2106
|
+
visit(child, rule, f"{path}/{index}")
|
|
2107
|
+
if not isinstance(value, dict):
|
|
2108
|
+
return
|
|
2109
|
+
if str(value.get("_Type_") or "").rsplit(".", 1)[-1] in {"BizRule", "FormRule"}:
|
|
2110
|
+
rule = value.get("Id") or value.get("Key") or path
|
|
2111
|
+
for slot, child in value.items():
|
|
2112
|
+
if rule is not None and slot.lower() in reference_slots and isinstance(child, str):
|
|
2113
|
+
for key, pattern in patterns.items():
|
|
2114
|
+
if pattern.search(child):
|
|
2115
|
+
found.append({"ruleId": rule, "property": f"{path}/{slot}", "targetField": key})
|
|
2116
|
+
visit(child, rule, f"{path}/{slot}")
|
|
2117
|
+
visit(metadata)
|
|
2118
|
+
return found
|
|
2119
|
+
|
|
2120
|
+
|
|
2065
2121
|
def do_remove_field(cfg, token, form_name, field_name, *, app_root=None,
|
|
2066
2122
|
project_root=None, app_name=None, app_code=None,
|
|
2067
2123
|
entity_key=None, before_delete=None):
|
|
@@ -2155,10 +2211,11 @@ def do_remove_field(cfg, token, form_name, field_name, *, app_root=None,
|
|
|
2155
2211
|
if not _is_api_success(metadata) or not isinstance(metadata.get("data"), dict):
|
|
2156
2212
|
return _extension_terminal("FIELD_DEPENDENCY_READ_FAILED", "删除前无法校验字段依赖关系",
|
|
2157
2213
|
"preflight", cause=metadata, extra={"writeAttempted": False})
|
|
2158
|
-
dependencies = summary_delete_dependencies(metadata.get("data"), candidates)
|
|
2214
|
+
dependencies = (summary_delete_dependencies(metadata.get("data"), candidates)
|
|
2215
|
+
+ rule_delete_dependencies(metadata.get("data"), candidates))
|
|
2159
2216
|
if dependencies:
|
|
2160
2217
|
return _extension_terminal("FIELD_DELETE_DEPENDENCY_BLOCKED",
|
|
2161
|
-
"
|
|
2218
|
+
"Surviving fields or rules reference the requested deletion. Resolve the intended business relationship before deleting any fields; do not clear it or create a temporary target as a workaround.",
|
|
2162
2219
|
"preflight", extra={"writeAttempted": False, "dependencies": dependencies, "fields": candidates,
|
|
2163
2220
|
"editableTarget": editable})
|
|
2164
2221
|
field = candidates[0]
|
|
@@ -31,17 +31,73 @@ def do_query_forms_by_app(cfg, token, app_number=None, cloud_number=None, keywor
|
|
|
31
31
|
return _api_call(cfg, token, "queryFormsByApp", payload, method="GET")
|
|
32
32
|
# ─── queryPublicTemplates / queryBizTemplates ────────────────────────────────
|
|
33
33
|
|
|
34
|
+
_TEMPLATE_MODEL_ALIASES = {
|
|
35
|
+
"FormModel_Bill": "BillFormModel",
|
|
36
|
+
"FormModel_Base": "BaseFormModel",
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _template_model_type(value):
|
|
41
|
+
return _TEMPLATE_MODEL_ALIASES.get(value, value)
|
|
42
|
+
|
|
43
|
+
def normalize_template_caption(value):
|
|
44
|
+
text = str(value or "").strip().replace("模版", "模板")
|
|
45
|
+
text = _re.sub(r"[\s()()]+", "", text)
|
|
46
|
+
return text[:-2] if text.endswith("模板") else text
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def resolve_template_selector(selector, templates, model_type=None):
|
|
50
|
+
"""Resolve an exact live template code or unique name within the model type."""
|
|
51
|
+
wanted = str(selector or "").strip()
|
|
52
|
+
model_type = _template_model_type(model_type)
|
|
53
|
+
rows = [row for row in templates if isinstance(row, dict)
|
|
54
|
+
and (not model_type or _template_model_type(row.get("modelType")) == model_type)]
|
|
55
|
+
coded = [row for row in rows if str(row.get("number") or "").strip() == wanted]
|
|
56
|
+
if len(coded) == 1:
|
|
57
|
+
return coded[0]["number"]
|
|
58
|
+
exact_names = {str(row["number"]) for row in rows if row.get("number")
|
|
59
|
+
and str(row.get("name") or "").strip() == wanted}
|
|
60
|
+
if len(exact_names) == 1:
|
|
61
|
+
return next(iter(exact_names))
|
|
62
|
+
# 模版 and 模板 are spelling variants, not different template identities.
|
|
63
|
+
normalize = normalize_template_caption
|
|
64
|
+
matched = {str(row["number"]) for row in rows
|
|
65
|
+
if row.get("number") and normalize(row.get("name")) == normalize(wanted)}
|
|
66
|
+
if len(matched) == 1:
|
|
67
|
+
return next(iter(matched))
|
|
68
|
+
if matched:
|
|
69
|
+
raise ValueError(f"TEMPLATE_NAME_AMBIGUOUS: {wanted}: {sorted(matched)}")
|
|
70
|
+
raise ValueError(f"TEMPLATE_NOT_FOUND: {wanted}")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def resolve_template_input(cfg, token, selector, *, biz_app_id=None, biz_app_number=None, model_type=None):
|
|
74
|
+
wanted = str(selector or "").strip()
|
|
75
|
+
if not wanted:
|
|
76
|
+
return wanted
|
|
77
|
+
rows = do_get_template_list(cfg, token,
|
|
78
|
+
model_types=[model_type] if model_type else None,
|
|
79
|
+
biz_app_id=biz_app_id, biz_app_number=biz_app_number)
|
|
80
|
+
if not rows:
|
|
81
|
+
return wanted
|
|
82
|
+
try:
|
|
83
|
+
return resolve_template_selector(wanted, rows, model_type)
|
|
84
|
+
except ValueError as exc:
|
|
85
|
+
if str(exc).startswith("TEMPLATE_NOT_FOUND") and _re.fullmatch(r"[A-Za-z][A-Za-z0-9_]*", wanted):
|
|
86
|
+
return wanted
|
|
87
|
+
raise
|
|
88
|
+
|
|
89
|
+
|
|
34
90
|
def do_query_public_templates(cfg, token, model_type="BillFormModel",
|
|
35
91
|
biz_app_number=None):
|
|
36
92
|
"""Query public template list."""
|
|
37
|
-
payload = {"modelType": model_type}
|
|
93
|
+
payload = {"modelType": _template_model_type(model_type)}
|
|
38
94
|
if biz_app_number:
|
|
39
95
|
payload["bizAppNumber"] = biz_app_number
|
|
40
96
|
return _api_call(cfg, token, "queryPublicTemplates", payload, method="POST")
|
|
41
97
|
def do_query_biz_templates(cfg, token, model_type="BillFormModel",
|
|
42
98
|
biz_app_id=None, biz_app_number=None):
|
|
43
99
|
"""Query business template list (app required)."""
|
|
44
|
-
payload = {"modelType": model_type}
|
|
100
|
+
payload = {"modelType": _template_model_type(model_type)}
|
|
45
101
|
if biz_app_id:
|
|
46
102
|
payload["bizAppId"] = biz_app_id
|
|
47
103
|
if biz_app_number:
|
|
@@ -73,6 +129,7 @@ def do_get_template_list(cfg, token, model_types=None, biz_app_id=None, biz_app_
|
|
|
73
129
|
"""
|
|
74
130
|
if model_types is None:
|
|
75
131
|
model_types = ["BillFormModel", "BaseFormModel"]
|
|
132
|
+
model_types = list(dict.fromkeys(_template_model_type(mt) for mt in model_types))
|
|
76
133
|
all_templates: list[dict] = []
|
|
77
134
|
seen_numbers: set[str] = set()
|
|
78
135
|
for mt in model_types:
|
|
@@ -1488,6 +1488,14 @@ def do_configure_list(
|
|
|
1488
1488
|
return _list_configuration_preflight_failure(
|
|
1489
1489
|
"LIST_CONFIGURATION_BASELINE_FAILED",
|
|
1490
1490
|
"写入列表前无法同时读取设计态和运行态元数据",
|
|
1491
|
+
failedReads={
|
|
1492
|
+
name: item
|
|
1493
|
+
for name, item in (
|
|
1494
|
+
("listElements", field_result), ("getFormSchema", schema_before),
|
|
1495
|
+
("loadFullMetadata", metadata_before), ("getEntityType", entity_type),
|
|
1496
|
+
)
|
|
1497
|
+
if not _is_api_success(item)
|
|
1498
|
+
},
|
|
1491
1499
|
)
|
|
1492
1500
|
|
|
1493
1501
|
field_rows = field_result.get("data") or []
|