@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,403 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Build or incrementally update the L0 plugin-corpus index of a Cosmic library tree.
|
|
3
|
+
|
|
4
|
+
The index records binary facts only: every JAR with its hash and manifest version, every class
|
|
5
|
+
with its hash, hierarchy and annotation types, declared members, and, for the corpus layers,
|
|
6
|
+
the members, classes and dynamic class names each class refers to. It never decompiles, never
|
|
7
|
+
modifies the libraries and never contacts a service.
|
|
8
|
+
|
|
9
|
+
Usage::
|
|
10
|
+
|
|
11
|
+
build_index.py --lib-root <cosmic>/mservice-cosmic/lib --workspace <corpus workspace>
|
|
12
|
+
|
|
13
|
+
Each immediate subdirectory of ``--lib-root`` that contains JARs becomes a layer named after it
|
|
14
|
+
(bos, biz, cus, trd for a Cosmic installation). ``--refs-layer`` selects the layers whose
|
|
15
|
+
references are stored (default biz and cus); the other layers keep classes and members, which
|
|
16
|
+
is what resolving a reference needs. A rerun reuses every JAR whose size and modification time
|
|
17
|
+
are unchanged, and every JAR whose content hash is unchanged.
|
|
18
|
+
|
|
19
|
+
The workspace must be outside the repository and outside anything a tested agent can read; it
|
|
20
|
+
holds ``l0-index.sqlite`` and ``l0-index-summary.json``.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import argparse
|
|
26
|
+
import hashlib
|
|
27
|
+
import json
|
|
28
|
+
import os
|
|
29
|
+
import sqlite3
|
|
30
|
+
import sys
|
|
31
|
+
import time
|
|
32
|
+
import zipfile
|
|
33
|
+
from concurrent.futures import ProcessPoolExecutor
|
|
34
|
+
from pathlib import Path
|
|
35
|
+
|
|
36
|
+
import jvm_classfile
|
|
37
|
+
|
|
38
|
+
SCHEMA_VERSION = 1
|
|
39
|
+
DB_NAME = "l0-index.sqlite"
|
|
40
|
+
SUMMARY_NAME = "l0-index-summary.json"
|
|
41
|
+
DEFAULT_REFS_LAYERS = ("biz", "cus")
|
|
42
|
+
# References into java.* are counted per class instead of stored: the corpus questions are about
|
|
43
|
+
# platform and business APIs, and those rows would roughly double the table. javax.* and com.sun.*
|
|
44
|
+
# are kept because third-party JARs in the library tree define many of them.
|
|
45
|
+
JAVA_PREFIX = "java."
|
|
46
|
+
|
|
47
|
+
_TABLES = """
|
|
48
|
+
CREATE TABLE IF NOT EXISTS meta(key TEXT PRIMARY KEY, value TEXT);
|
|
49
|
+
CREATE TABLE IF NOT EXISTS strings(id INTEGER PRIMARY KEY, value TEXT NOT NULL UNIQUE);
|
|
50
|
+
CREATE TABLE IF NOT EXISTS jars(
|
|
51
|
+
id INTEGER PRIMARY KEY, path TEXT NOT NULL UNIQUE, layer TEXT NOT NULL, size INTEGER NOT NULL,
|
|
52
|
+
mtime_ns INTEGER NOT NULL, sha256 TEXT NOT NULL, implementation_version TEXT, bundle_version TEXT,
|
|
53
|
+
class_count INTEGER NOT NULL, error_count INTEGER NOT NULL, skipped_count INTEGER NOT NULL,
|
|
54
|
+
refs_indexed INTEGER NOT NULL, status TEXT NOT NULL, indexed_at TEXT NOT NULL);
|
|
55
|
+
CREATE TABLE IF NOT EXISTS classes(
|
|
56
|
+
id INTEGER PRIMARY KEY, jar_id INTEGER NOT NULL, name_id INTEGER NOT NULL, super_id INTEGER,
|
|
57
|
+
access INTEGER NOT NULL, major INTEGER NOT NULL, sha256 TEXT NOT NULL, annotations TEXT,
|
|
58
|
+
deprecated INTEGER NOT NULL, source_file TEXT, entry TEXT NOT NULL, java_refs INTEGER);
|
|
59
|
+
CREATE TABLE IF NOT EXISTS interfaces(class_id INTEGER NOT NULL, iface_id INTEGER NOT NULL);
|
|
60
|
+
CREATE TABLE IF NOT EXISTS members(
|
|
61
|
+
class_id INTEGER NOT NULL, kind TEXT NOT NULL, name_id INTEGER NOT NULL, desc_id INTEGER NOT NULL,
|
|
62
|
+
access INTEGER NOT NULL, annotations TEXT, deprecated INTEGER NOT NULL);
|
|
63
|
+
CREATE TABLE IF NOT EXISTS refs(
|
|
64
|
+
class_id INTEGER NOT NULL, kind TEXT NOT NULL, owner_id INTEGER NOT NULL, name_id INTEGER,
|
|
65
|
+
desc_id INTEGER);
|
|
66
|
+
CREATE TABLE IF NOT EXISTS errors(jar_id INTEGER NOT NULL, entry TEXT, message TEXT NOT NULL);
|
|
67
|
+
"""
|
|
68
|
+
_INDEXES = """
|
|
69
|
+
CREATE INDEX IF NOT EXISTS classes_name ON classes(name_id);
|
|
70
|
+
CREATE INDEX IF NOT EXISTS classes_jar ON classes(jar_id);
|
|
71
|
+
CREATE INDEX IF NOT EXISTS interfaces_class ON interfaces(class_id);
|
|
72
|
+
CREATE INDEX IF NOT EXISTS members_class ON members(class_id);
|
|
73
|
+
CREATE INDEX IF NOT EXISTS refs_class ON refs(class_id);
|
|
74
|
+
CREATE INDEX IF NOT EXISTS refs_target ON refs(owner_id, name_id, desc_id);
|
|
75
|
+
CREATE INDEX IF NOT EXISTS errors_jar ON errors(jar_id);
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class CorpusIndexError(RuntimeError):
|
|
80
|
+
"""A usage or workspace problem reported to the caller as a structured error."""
|
|
81
|
+
|
|
82
|
+
def __init__(self, code: str, message: str):
|
|
83
|
+
super().__init__(message)
|
|
84
|
+
self.code = code
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _hash_file(path: str) -> str:
|
|
88
|
+
digest = hashlib.sha256()
|
|
89
|
+
with open(path, "rb") as stream:
|
|
90
|
+
for block in iter(lambda: stream.read(1 << 20), b""):
|
|
91
|
+
digest.update(block)
|
|
92
|
+
return digest.hexdigest()
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _manifest_versions(jar: zipfile.ZipFile) -> tuple[str | None, str | None]:
|
|
96
|
+
try:
|
|
97
|
+
text = jar.read("META-INF/MANIFEST.MF").decode("utf-8", "replace")
|
|
98
|
+
except KeyError:
|
|
99
|
+
return None, None
|
|
100
|
+
attributes: dict[str, str] = {}
|
|
101
|
+
key = None
|
|
102
|
+
for line in text.replace("\r\n", "\n").split("\n"):
|
|
103
|
+
if line.startswith(" ") and key:
|
|
104
|
+
attributes[key] += line[1:]
|
|
105
|
+
elif ":" in line:
|
|
106
|
+
key, _, value = line.partition(":")
|
|
107
|
+
attributes[key.strip()] = value.strip()
|
|
108
|
+
return attributes.get("Implementation-Version") or None, attributes.get("Bundle-Version") or None
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def parse_jar(task: tuple[str, str | None, bool]) -> dict:
|
|
112
|
+
"""Worker: hash one JAR and, unless its hash is unchanged, parse its classes."""
|
|
113
|
+
path, known_sha, with_refs = task
|
|
114
|
+
sha = _hash_file(path)
|
|
115
|
+
if known_sha == sha:
|
|
116
|
+
return {"path": path, "sha256": sha, "unchanged": True}
|
|
117
|
+
classes: list[dict] = []
|
|
118
|
+
errors: list[tuple[str | None, str]] = []
|
|
119
|
+
skipped = 0
|
|
120
|
+
try:
|
|
121
|
+
with zipfile.ZipFile(path) as jar:
|
|
122
|
+
implementation, bundle = _manifest_versions(jar)
|
|
123
|
+
for info in jar.infolist():
|
|
124
|
+
entry = info.filename
|
|
125
|
+
if not entry.endswith(".class"):
|
|
126
|
+
continue
|
|
127
|
+
# Multi-release overlays and module/package descriptors are not classes a plugin
|
|
128
|
+
# extends or calls; they are counted, not indexed.
|
|
129
|
+
if entry.startswith("META-INF/") or entry.endswith(("module-info.class", "package-info.class")):
|
|
130
|
+
skipped += 1
|
|
131
|
+
continue
|
|
132
|
+
data = jar.read(info)
|
|
133
|
+
try:
|
|
134
|
+
parsed = jvm_classfile.parse_class(data)
|
|
135
|
+
except jvm_classfile.ClassFormatError as exc:
|
|
136
|
+
errors.append((entry, str(exc)[:300]))
|
|
137
|
+
continue
|
|
138
|
+
parsed["entry"] = entry
|
|
139
|
+
parsed["sha256"] = hashlib.sha256(data).hexdigest()
|
|
140
|
+
if not with_refs:
|
|
141
|
+
parsed["memberRefs"] = parsed["classRefs"] = parsed["dynamicClassNames"] = None
|
|
142
|
+
classes.append(parsed)
|
|
143
|
+
except (zipfile.BadZipFile, OSError) as exc:
|
|
144
|
+
return {"path": path, "sha256": sha, "unreadable": str(exc)[:300]}
|
|
145
|
+
return {"path": path, "sha256": sha, "implementationVersion": implementation, "bundleVersion": bundle,
|
|
146
|
+
"classes": classes, "errors": errors, "skipped": skipped, "refsIndexed": with_refs}
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
class _Strings:
|
|
150
|
+
"""Intern repeated names in the strings table; ids are assigned here and flushed in bulk."""
|
|
151
|
+
|
|
152
|
+
def __init__(self, db: sqlite3.Connection):
|
|
153
|
+
self.db = db
|
|
154
|
+
self.ids = {value: key for key, value in db.execute("SELECT id, value FROM strings")}
|
|
155
|
+
self.next_id = max(self.ids.values(), default=0) + 1
|
|
156
|
+
self.pending: list[tuple[int, str]] = []
|
|
157
|
+
|
|
158
|
+
def id(self, value: str | None) -> int | None:
|
|
159
|
+
if value is None:
|
|
160
|
+
return None
|
|
161
|
+
known = self.ids.get(value)
|
|
162
|
+
if known is not None:
|
|
163
|
+
return known
|
|
164
|
+
key = self.next_id
|
|
165
|
+
self.next_id += 1
|
|
166
|
+
self.ids[value] = key
|
|
167
|
+
self.pending.append((key, value))
|
|
168
|
+
return key
|
|
169
|
+
|
|
170
|
+
def flush(self) -> None:
|
|
171
|
+
if self.pending:
|
|
172
|
+
self.db.executemany("INSERT INTO strings(id, value) VALUES (?, ?)", self.pending)
|
|
173
|
+
self.pending = []
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def resolve_layers(lib_root: str | None, explicit: list[str]) -> dict[str, Path]:
|
|
177
|
+
layers: dict[str, Path] = {}
|
|
178
|
+
if lib_root:
|
|
179
|
+
root = Path(lib_root).expanduser().resolve()
|
|
180
|
+
if not root.is_dir():
|
|
181
|
+
raise CorpusIndexError("CORPUS_LIB_ROOT_INVALID", f"Library root is not a directory: {root}")
|
|
182
|
+
for child in sorted(root.iterdir()):
|
|
183
|
+
if child.is_dir() and any(child.glob("*.jar")):
|
|
184
|
+
layers[child.name] = child
|
|
185
|
+
for item in explicit:
|
|
186
|
+
name, separator, directory = item.partition("=")
|
|
187
|
+
path = Path(directory).expanduser().resolve()
|
|
188
|
+
if not separator or not name or not path.is_dir():
|
|
189
|
+
raise CorpusIndexError("CORPUS_LAYER_INVALID", f"Use --layer NAME=DIRECTORY with an existing directory: {item}")
|
|
190
|
+
layers[name] = path
|
|
191
|
+
if not layers:
|
|
192
|
+
raise CorpusIndexError("CORPUS_LAYERS_EMPTY", "No layer directory with JARs was found")
|
|
193
|
+
return layers
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _protected_roots() -> list[Path]:
|
|
197
|
+
"""The skill package and, when present, the git checkout that contains this script."""
|
|
198
|
+
here = Path(__file__).resolve()
|
|
199
|
+
roots = [here.parents[2]] # the app-build skill package, which tested agents can read
|
|
200
|
+
for parent in here.parents:
|
|
201
|
+
if (parent / ".git").exists():
|
|
202
|
+
roots.append(parent)
|
|
203
|
+
break
|
|
204
|
+
return roots
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _workspace(path: str) -> Path:
|
|
208
|
+
workspace = Path(path).expanduser().resolve()
|
|
209
|
+
for root in _protected_roots():
|
|
210
|
+
if workspace == root or root in workspace.parents:
|
|
211
|
+
raise CorpusIndexError("CORPUS_WORKSPACE_INSIDE_REPOSITORY",
|
|
212
|
+
f"The corpus workspace must be outside {root}: {workspace}")
|
|
213
|
+
workspace.mkdir(parents=True, exist_ok=True)
|
|
214
|
+
return workspace
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _connect(db_path: Path) -> tuple[sqlite3.Connection, bool]:
|
|
218
|
+
fresh = not db_path.exists()
|
|
219
|
+
db = sqlite3.connect(str(db_path))
|
|
220
|
+
db.execute("PRAGMA journal_mode=WAL")
|
|
221
|
+
db.execute("PRAGMA synchronous=NORMAL")
|
|
222
|
+
db.executescript(_TABLES)
|
|
223
|
+
version = db.execute("SELECT value FROM meta WHERE key='schemaVersion'").fetchone()
|
|
224
|
+
if version and int(version[0]) != SCHEMA_VERSION:
|
|
225
|
+
db.close()
|
|
226
|
+
raise CorpusIndexError("CORPUS_SCHEMA_MISMATCH",
|
|
227
|
+
f"{db_path} uses schema {version[0]}; move it aside to rebuild with schema {SCHEMA_VERSION}")
|
|
228
|
+
db.execute("INSERT OR REPLACE INTO meta(key, value) VALUES ('schemaVersion', ?)", (str(SCHEMA_VERSION),))
|
|
229
|
+
if not fresh:
|
|
230
|
+
db.executescript(_INDEXES)
|
|
231
|
+
return db, fresh
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _delete_jar_rows(db: sqlite3.Connection, jar_id: int) -> None:
|
|
235
|
+
for table in ("refs", "members", "interfaces"):
|
|
236
|
+
db.execute(f"DELETE FROM {table} WHERE class_id IN (SELECT id FROM classes WHERE jar_id=?)", (jar_id,))
|
|
237
|
+
db.execute("DELETE FROM classes WHERE jar_id=?", (jar_id,))
|
|
238
|
+
db.execute("DELETE FROM errors WHERE jar_id=?", (jar_id,))
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def _store_result(db: sqlite3.Connection, strings: _Strings, next_class: list[int], layer: str, stat: os.stat_result,
|
|
242
|
+
existing: dict | None, result: dict) -> str:
|
|
243
|
+
now = time.strftime("%Y-%m-%dT%H:%M:%S%z")
|
|
244
|
+
if result.get("unchanged"):
|
|
245
|
+
db.execute("UPDATE jars SET size=?, mtime_ns=? WHERE id=?", (stat.st_size, stat.st_mtime_ns, existing["id"]))
|
|
246
|
+
return "unchanged"
|
|
247
|
+
if existing:
|
|
248
|
+
_delete_jar_rows(db, existing["id"])
|
|
249
|
+
jar_id = existing["id"]
|
|
250
|
+
else:
|
|
251
|
+
jar_id = db.execute("SELECT COALESCE(MAX(id), 0) + 1 FROM jars").fetchone()[0]
|
|
252
|
+
classes = result.get("classes") or []
|
|
253
|
+
errors = result.get("errors") or []
|
|
254
|
+
if "unreadable" in result:
|
|
255
|
+
errors = [(None, result["unreadable"])]
|
|
256
|
+
row = (jar_id, result["path"], layer, stat.st_size, stat.st_mtime_ns, result["sha256"],
|
|
257
|
+
result.get("implementationVersion"), result.get("bundleVersion"), len(classes), len(errors),
|
|
258
|
+
result.get("skipped", 0), 1 if result.get("refsIndexed") else 0,
|
|
259
|
+
"unreadable" if "unreadable" in result else "indexed", now)
|
|
260
|
+
db.execute("INSERT OR REPLACE INTO jars(id, path, layer, size, mtime_ns, sha256, implementation_version, "
|
|
261
|
+
"bundle_version, class_count, error_count, skipped_count, refs_indexed, status, indexed_at) "
|
|
262
|
+
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", row)
|
|
263
|
+
class_rows, interface_rows, member_rows, ref_rows = [], [], [], []
|
|
264
|
+
for parsed in classes:
|
|
265
|
+
class_id = next_class[0]
|
|
266
|
+
next_class[0] += 1
|
|
267
|
+
member_refs = parsed["memberRefs"]
|
|
268
|
+
java_refs = None
|
|
269
|
+
if member_refs is not None:
|
|
270
|
+
java_refs = 0
|
|
271
|
+
for kind, owner, name, descriptor in member_refs:
|
|
272
|
+
if owner.startswith(JAVA_PREFIX):
|
|
273
|
+
java_refs += 1
|
|
274
|
+
continue
|
|
275
|
+
ref_rows.append((class_id, kind, strings.id(owner), strings.id(name), strings.id(descriptor)))
|
|
276
|
+
ref_rows.extend((class_id, "C", strings.id(name), None, None)
|
|
277
|
+
for name in parsed["classRefs"] if not name.startswith(JAVA_PREFIX))
|
|
278
|
+
ref_rows.extend((class_id, "S", strings.id(name), None, None) for name in parsed["dynamicClassNames"])
|
|
279
|
+
class_rows.append((class_id, jar_id, strings.id(parsed["name"]), strings.id(parsed["super"]),
|
|
280
|
+
parsed["access"], parsed["major"], parsed["sha256"],
|
|
281
|
+
" ".join(parsed["annotations"]) or None, 1 if parsed["deprecated"] else 0,
|
|
282
|
+
parsed["sourceFile"], parsed["entry"], java_refs))
|
|
283
|
+
interface_rows.extend((class_id, strings.id(name)) for name in parsed["interfaces"])
|
|
284
|
+
for kind, members in (("F", parsed["fields"]), ("M", parsed["methods"])):
|
|
285
|
+
member_rows.extend((class_id, kind, strings.id(name), strings.id(descriptor), access,
|
|
286
|
+
" ".join(annotations) or None, 1 if deprecated else 0)
|
|
287
|
+
for name, descriptor, access, annotations, deprecated in members)
|
|
288
|
+
strings.flush()
|
|
289
|
+
db.executemany("INSERT INTO classes VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", class_rows)
|
|
290
|
+
db.executemany("INSERT INTO interfaces VALUES (?, ?)", interface_rows)
|
|
291
|
+
db.executemany("INSERT INTO members VALUES (?, ?, ?, ?, ?, ?, ?)", member_rows)
|
|
292
|
+
db.executemany("INSERT INTO refs VALUES (?, ?, ?, ?, ?)", ref_rows)
|
|
293
|
+
db.executemany("INSERT INTO errors VALUES (?, ?, ?)", [(jar_id, entry, message) for entry, message in errors])
|
|
294
|
+
return "changed" if existing else "new"
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def summarize(db: sqlite3.Connection) -> dict:
|
|
298
|
+
layers = {}
|
|
299
|
+
for layer, jars, classes, errors, unreadable in db.execute(
|
|
300
|
+
"SELECT layer, COUNT(*), SUM(class_count), SUM(error_count), SUM(status='unreadable') "
|
|
301
|
+
"FROM jars GROUP BY layer ORDER BY layer"):
|
|
302
|
+
layers[layer] = {"jars": jars, "classes": classes or 0, "classErrors": errors or 0, "unreadableJars": unreadable or 0}
|
|
303
|
+
duplicates = {"fqcns": 0, "conflicting": 0, "byLayers": {}, "conflictingExamples": []}
|
|
304
|
+
for name, copies, hashes, layer_set in db.execute(
|
|
305
|
+
"SELECT s.value, COUNT(*), COUNT(DISTINCT c.sha256), GROUP_CONCAT(DISTINCT j.layer) "
|
|
306
|
+
"FROM classes c JOIN jars j ON j.id = c.jar_id JOIN strings s ON s.id = c.name_id "
|
|
307
|
+
"GROUP BY c.name_id HAVING COUNT(*) > 1"):
|
|
308
|
+
duplicates["fqcns"] += 1
|
|
309
|
+
key = "+".join(sorted(layer_set.split(",")))
|
|
310
|
+
bucket = duplicates["byLayers"].setdefault(key, {"fqcns": 0, "conflicting": 0})
|
|
311
|
+
bucket["fqcns"] += 1
|
|
312
|
+
if hashes > 1:
|
|
313
|
+
duplicates["conflicting"] += 1
|
|
314
|
+
bucket["conflicting"] += 1
|
|
315
|
+
if len(duplicates["conflictingExamples"]) < 20:
|
|
316
|
+
duplicates["conflictingExamples"].append({"class": name, "copies": copies, "distinctHashes": hashes,
|
|
317
|
+
"layers": sorted(layer_set.split(","))})
|
|
318
|
+
refs = {kind: count for kind, count in db.execute("SELECT kind, COUNT(*) FROM refs GROUP BY kind")}
|
|
319
|
+
return {"schemaVersion": SCHEMA_VERSION, "layers": layers, "duplicateClasses": duplicates, "storedRefs": refs}
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def build(lib_root: str | None, layer_args: list[str], workspace: str, refs_layers: list[str],
|
|
323
|
+
workers: int | None) -> dict:
|
|
324
|
+
started = time.monotonic()
|
|
325
|
+
layers = resolve_layers(lib_root, layer_args)
|
|
326
|
+
unknown = sorted(set(refs_layers) - set(layers) - {"all"})
|
|
327
|
+
if unknown:
|
|
328
|
+
raise CorpusIndexError("CORPUS_REFS_LAYER_UNKNOWN", f"--refs-layer names no indexed layer: {', '.join(unknown)}")
|
|
329
|
+
with_refs = set(layers) if "all" in refs_layers else set(refs_layers)
|
|
330
|
+
target = _workspace(workspace)
|
|
331
|
+
db_path = target / DB_NAME
|
|
332
|
+
db, fresh = _connect(db_path)
|
|
333
|
+
try:
|
|
334
|
+
scanned: dict[str, tuple[str, os.stat_result]] = {}
|
|
335
|
+
for layer, directory in layers.items():
|
|
336
|
+
for jar in sorted(directory.glob("*.jar")):
|
|
337
|
+
scanned[str(jar.resolve())] = (layer, jar.stat())
|
|
338
|
+
existing = {path: {"id": key, "size": size, "mtime_ns": mtime, "sha256": sha, "refs": refs, "layer": layer}
|
|
339
|
+
for key, path, size, mtime, sha, refs, layer in
|
|
340
|
+
db.execute("SELECT id, path, size, mtime_ns, sha256, refs_indexed, layer FROM jars")}
|
|
341
|
+
counts = {"new": 0, "changed": 0, "unchanged": 0, "removed": 0}
|
|
342
|
+
for path in sorted(set(existing) - set(scanned)):
|
|
343
|
+
_delete_jar_rows(db, existing[path]["id"])
|
|
344
|
+
db.execute("DELETE FROM jars WHERE id=?", (existing[path]["id"],))
|
|
345
|
+
counts["removed"] += 1
|
|
346
|
+
tasks = []
|
|
347
|
+
for path, (layer, stat) in scanned.items():
|
|
348
|
+
known = existing.get(path)
|
|
349
|
+
needs_refs = layer in with_refs
|
|
350
|
+
reusable = known and known["layer"] == layer and bool(known["refs"]) == needs_refs
|
|
351
|
+
if reusable and known["size"] == stat.st_size and known["mtime_ns"] == stat.st_mtime_ns:
|
|
352
|
+
counts["unchanged"] += 1
|
|
353
|
+
continue
|
|
354
|
+
tasks.append((path, known["sha256"] if reusable else None, needs_refs))
|
|
355
|
+
strings = _Strings(db)
|
|
356
|
+
next_class = [db.execute("SELECT COALESCE(MAX(id), 0) + 1 FROM classes").fetchone()[0]]
|
|
357
|
+
pool = ProcessPoolExecutor(max_workers=workers or min(8, os.cpu_count() or 1)) if workers != 0 else None
|
|
358
|
+
try:
|
|
359
|
+
results = pool.map(parse_jar, tasks, chunksize=2) if pool else map(parse_jar, tasks)
|
|
360
|
+
for done, result in enumerate(results, start=1):
|
|
361
|
+
layer, stat = scanned[result["path"]]
|
|
362
|
+
outcome = _store_result(db, strings, next_class, layer, stat, existing.get(result["path"]), result)
|
|
363
|
+
counts[outcome] += 1
|
|
364
|
+
if done % 200 == 0:
|
|
365
|
+
db.commit()
|
|
366
|
+
finally:
|
|
367
|
+
if pool:
|
|
368
|
+
pool.shutdown()
|
|
369
|
+
if fresh:
|
|
370
|
+
db.executescript(_INDEXES)
|
|
371
|
+
meta = {"libRoot": str(Path(lib_root).expanduser().resolve()) if lib_root else None,
|
|
372
|
+
"layers": json.dumps({name: str(path) for name, path in layers.items()}),
|
|
373
|
+
"refsLayers": json.dumps(sorted(with_refs)), "updatedAt": time.strftime("%Y-%m-%dT%H:%M:%S%z")}
|
|
374
|
+
db.executemany("INSERT OR REPLACE INTO meta(key, value) VALUES (?, ?)", list(meta.items()))
|
|
375
|
+
db.commit()
|
|
376
|
+
summary = {"status": True, "database": str(db_path), "jars": counts, "refsLayers": sorted(with_refs),
|
|
377
|
+
**summarize(db), "elapsedSec": round(time.monotonic() - started, 1)}
|
|
378
|
+
finally:
|
|
379
|
+
db.close()
|
|
380
|
+
(target / SUMMARY_NAME).write_text(json.dumps(summary, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
381
|
+
return summary
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def main(argv: list[str] | None = None) -> int:
|
|
385
|
+
parser = argparse.ArgumentParser(description=__doc__.split("\n\n")[0])
|
|
386
|
+
parser.add_argument("--lib-root", help="Library root whose JAR subdirectories become layers")
|
|
387
|
+
parser.add_argument("--layer", action="append", default=[], help="Extra or overriding layer as NAME=DIRECTORY")
|
|
388
|
+
parser.add_argument("--workspace", required=True, help="Corpus workspace outside the repository")
|
|
389
|
+
parser.add_argument("--refs-layer", action="append", help="Layer whose references are stored; 'all' for every layer")
|
|
390
|
+
parser.add_argument("--workers", type=int, help="Parser processes (default: min(8, CPU count); 0 parses in-process)")
|
|
391
|
+
args = parser.parse_args(argv)
|
|
392
|
+
try:
|
|
393
|
+
summary = build(args.lib_root, args.layer, args.workspace, args.refs_layer or list(DEFAULT_REFS_LAYERS),
|
|
394
|
+
args.workers)
|
|
395
|
+
except CorpusIndexError as exc:
|
|
396
|
+
print(json.dumps({"status": False, "errorCode": exc.code, "message": str(exc)}, ensure_ascii=False))
|
|
397
|
+
return 2
|
|
398
|
+
print(json.dumps(summary, ensure_ascii=False, indent=2))
|
|
399
|
+
return 0
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
if __name__ == "__main__":
|
|
403
|
+
sys.exit(main())
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
"""Read the structure of a JVM class file for the plugin corpus index.
|
|
2
|
+
|
|
3
|
+
The corpus holds roughly 670k classes, so running javap per class is far too slow; this module
|
|
4
|
+
reads the class file format directly with no JDK. It keeps what the index needs: the class's
|
|
5
|
+
names and hierarchy, declared members, annotation types, and the members, classes and
|
|
6
|
+
dynamic class names it refers to.
|
|
7
|
+
|
|
8
|
+
Method bodies are not decoded. A member reference is read from the constant pool, so it means
|
|
9
|
+
"this class names that member somewhere" (a call, a field access or a method handle) rather
|
|
10
|
+
than a count of call sites. The owner of a reference is the static receiver type the compiler
|
|
11
|
+
wrote at the use site; resolving it to the declaring class needs the whole hierarchy and is
|
|
12
|
+
the index consumer's job.
|
|
13
|
+
|
|
14
|
+
Names are returned in binary form with dots: ``kd.bos.algo.DataSet$Listener``.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import re
|
|
20
|
+
import struct
|
|
21
|
+
|
|
22
|
+
_U2 = struct.Struct(">H")
|
|
23
|
+
_U2U2 = struct.Struct(">HH")
|
|
24
|
+
_HEAD = struct.Struct(">HHH")
|
|
25
|
+
_MEMBER = struct.Struct(">HHHH")
|
|
26
|
+
_ATTRIBUTE = struct.Struct(">HI")
|
|
27
|
+
|
|
28
|
+
_ANNOTATION_ATTRIBUTES = ("RuntimeVisibleAnnotations", "RuntimeInvisibleAnnotations")
|
|
29
|
+
_REF_KINDS = {9: "F", 10: "M", 11: "I"}
|
|
30
|
+
# A string constant that names a platform or business class, e.g. for reflection or a plugin
|
|
31
|
+
# registered by class name. Package-only strings and ordinary field keys do not match.
|
|
32
|
+
_DYNAMIC_CLASS_RE = re.compile(r"^kd(?:\.[a-z_][\w]*)+\.[A-Z][\w$]*$")
|
|
33
|
+
_PRIMITIVES = {"B": "byte", "C": "char", "D": "double", "F": "float", "I": "int", "J": "long",
|
|
34
|
+
"S": "short", "Z": "boolean", "V": "void"}
|
|
35
|
+
|
|
36
|
+
ACC_INTERFACE = 0x0200
|
|
37
|
+
ACC_ABSTRACT = 0x0400
|
|
38
|
+
ACC_ANNOTATION = 0x2000
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class ClassFormatError(ValueError):
|
|
42
|
+
"""The bytes are not a class file this reader can parse."""
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _utf8(raw: bytes) -> str:
|
|
46
|
+
try:
|
|
47
|
+
return raw.decode("utf-8")
|
|
48
|
+
except UnicodeDecodeError:
|
|
49
|
+
# Modified UTF-8 writes NUL as C0 80 and supplementary characters as surrogate pairs.
|
|
50
|
+
try:
|
|
51
|
+
return raw.replace(b"\xc0\x80", b"\x00").decode("utf-8", "surrogatepass")
|
|
52
|
+
except UnicodeDecodeError:
|
|
53
|
+
return raw.decode("utf-8", "replace")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _skip_element_value(data: bytes, pos: int) -> int:
|
|
57
|
+
tag = data[pos]
|
|
58
|
+
pos += 1
|
|
59
|
+
if tag in b"BCDFIJSZsc":
|
|
60
|
+
return pos + 2
|
|
61
|
+
if tag == 0x65: # 'e': enum type and constant name
|
|
62
|
+
return pos + 4
|
|
63
|
+
if tag == 0x40: # '@': nested annotation
|
|
64
|
+
return _skip_annotation_body(data, pos + 2)
|
|
65
|
+
if tag == 0x5B: # '[': array of element values
|
|
66
|
+
count = _U2.unpack_from(data, pos)[0]
|
|
67
|
+
pos += 2
|
|
68
|
+
for _ in range(count):
|
|
69
|
+
pos = _skip_element_value(data, pos)
|
|
70
|
+
return pos
|
|
71
|
+
raise ClassFormatError(f"unknown annotation element tag {tag}")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _skip_annotation_body(data: bytes, pos: int) -> int:
|
|
75
|
+
"""Skip the element pairs of an annotation whose type index was already read."""
|
|
76
|
+
pairs = _U2.unpack_from(data, pos)[0]
|
|
77
|
+
pos += 2
|
|
78
|
+
for _ in range(pairs):
|
|
79
|
+
pos = _skip_element_value(data, pos + 2)
|
|
80
|
+
return pos
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _annotation_types(data: bytes, pos: int, values: list) -> list[str]:
|
|
84
|
+
count = _U2.unpack_from(data, pos)[0]
|
|
85
|
+
pos += 2
|
|
86
|
+
names = []
|
|
87
|
+
for _ in range(count):
|
|
88
|
+
descriptor = values[_U2.unpack_from(data, pos)[0]]
|
|
89
|
+
if isinstance(descriptor, str) and descriptor.startswith("L") and descriptor.endswith(";"):
|
|
90
|
+
names.append(descriptor[1:-1].replace("/", "."))
|
|
91
|
+
pos = _skip_annotation_body(data, pos + 2)
|
|
92
|
+
return names
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def descriptor_types(descriptor: str) -> tuple[list[str], str]:
|
|
96
|
+
"""Split a method or field descriptor into parameter types and a return type.
|
|
97
|
+
|
|
98
|
+
Types use Java notation with dots and keep ``$`` for nested classes:
|
|
99
|
+
``(Ljava/lang/String;[I)V`` -> (["java.lang.String", "int[]"], "void"). A field descriptor
|
|
100
|
+
has no parameters and its type is returned as the "return" type.
|
|
101
|
+
"""
|
|
102
|
+
def read(pos: int) -> tuple[str, int]:
|
|
103
|
+
dims = 0
|
|
104
|
+
while descriptor[pos] == "[":
|
|
105
|
+
dims += 1
|
|
106
|
+
pos += 1
|
|
107
|
+
code = descriptor[pos]
|
|
108
|
+
if code == "L":
|
|
109
|
+
end = descriptor.index(";", pos)
|
|
110
|
+
name = descriptor[pos + 1:end].replace("/", ".")
|
|
111
|
+
pos = end + 1
|
|
112
|
+
elif code in _PRIMITIVES:
|
|
113
|
+
name = _PRIMITIVES[code]
|
|
114
|
+
pos += 1
|
|
115
|
+
else:
|
|
116
|
+
raise ClassFormatError(f"bad descriptor {descriptor!r}")
|
|
117
|
+
return name + "[]" * dims, pos
|
|
118
|
+
|
|
119
|
+
try:
|
|
120
|
+
if not descriptor.startswith("("):
|
|
121
|
+
return [], read(0)[0]
|
|
122
|
+
params = []
|
|
123
|
+
pos = 1
|
|
124
|
+
while descriptor[pos] != ")":
|
|
125
|
+
name, pos = read(pos)
|
|
126
|
+
params.append(name)
|
|
127
|
+
return params, read(pos + 1)[0]
|
|
128
|
+
except (IndexError, ValueError) as exc:
|
|
129
|
+
raise ClassFormatError(f"bad descriptor {descriptor!r}") from exc
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def simple_type(java_type: str) -> str:
|
|
133
|
+
"""``kd.bos.algo.DataSet$Listener[]`` -> ``Listener[]``, as API pages write parameter types."""
|
|
134
|
+
array = ""
|
|
135
|
+
while java_type.endswith("[]"):
|
|
136
|
+
array += "[]"
|
|
137
|
+
java_type = java_type[:-2]
|
|
138
|
+
return java_type.rsplit(".", 1)[-1].rsplit("$", 1)[-1] + array
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def parse_class(data: bytes) -> dict:
|
|
142
|
+
"""Parse one class file. Raises ClassFormatError for anything it cannot read."""
|
|
143
|
+
if len(data) < 10 or data[:4] != b"\xca\xfe\xba\xbe":
|
|
144
|
+
raise ClassFormatError("missing class file magic")
|
|
145
|
+
try:
|
|
146
|
+
return _parse(data)
|
|
147
|
+
except (struct.error, IndexError, KeyError, TypeError, AttributeError) as exc:
|
|
148
|
+
raise ClassFormatError(f"truncated or inconsistent class file: {exc}") from exc
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _parse(data: bytes) -> dict:
|
|
152
|
+
major = _U2.unpack_from(data, 6)[0]
|
|
153
|
+
count = _U2.unpack_from(data, 8)[0]
|
|
154
|
+
tags = [0] * count
|
|
155
|
+
values: list = [None] * count
|
|
156
|
+
pos = 10
|
|
157
|
+
index = 1
|
|
158
|
+
while index < count:
|
|
159
|
+
tag = data[pos]
|
|
160
|
+
if tag == 1:
|
|
161
|
+
length = _U2.unpack_from(data, pos + 1)[0]
|
|
162
|
+
values[index] = _utf8(data[pos + 3:pos + 3 + length])
|
|
163
|
+
pos += 3 + length
|
|
164
|
+
elif tag in (7, 8, 16, 19, 20):
|
|
165
|
+
values[index] = _U2.unpack_from(data, pos + 1)[0]
|
|
166
|
+
pos += 3
|
|
167
|
+
elif tag in (9, 10, 11, 12, 17, 18):
|
|
168
|
+
values[index] = _U2U2.unpack_from(data, pos + 1)
|
|
169
|
+
pos += 5
|
|
170
|
+
elif tag in (3, 4):
|
|
171
|
+
pos += 5
|
|
172
|
+
elif tag in (5, 6):
|
|
173
|
+
# Long and double constants take two pool slots.
|
|
174
|
+
tags[index] = tag
|
|
175
|
+
index += 2
|
|
176
|
+
pos += 9
|
|
177
|
+
continue
|
|
178
|
+
elif tag == 15:
|
|
179
|
+
values[index] = (data[pos + 1], _U2.unpack_from(data, pos + 2)[0])
|
|
180
|
+
pos += 4
|
|
181
|
+
else:
|
|
182
|
+
raise ClassFormatError(f"unknown constant pool tag {tag}")
|
|
183
|
+
tags[index] = tag
|
|
184
|
+
index += 1
|
|
185
|
+
|
|
186
|
+
def class_name(class_index: int) -> str:
|
|
187
|
+
return values[values[class_index]].replace("/", ".")
|
|
188
|
+
|
|
189
|
+
access, this_index, super_index = _HEAD.unpack_from(data, pos)
|
|
190
|
+
pos += 6
|
|
191
|
+
name = class_name(this_index)
|
|
192
|
+
super_name = class_name(super_index) if super_index else None
|
|
193
|
+
interface_count = _U2.unpack_from(data, pos)[0]
|
|
194
|
+
pos += 2
|
|
195
|
+
interfaces = [class_name(_U2.unpack_from(data, pos + 2 * i)[0]) for i in range(interface_count)]
|
|
196
|
+
pos += 2 * interface_count
|
|
197
|
+
|
|
198
|
+
def read_members(pos: int) -> tuple[list[tuple], int]:
|
|
199
|
+
member_count = _U2.unpack_from(data, pos)[0]
|
|
200
|
+
pos += 2
|
|
201
|
+
members = []
|
|
202
|
+
for _ in range(member_count):
|
|
203
|
+
member_access, name_index, descriptor_index, attribute_count = _MEMBER.unpack_from(data, pos)
|
|
204
|
+
pos += 8
|
|
205
|
+
annotations: list[str] = []
|
|
206
|
+
deprecated = False
|
|
207
|
+
for _ in range(attribute_count):
|
|
208
|
+
attribute_index, length = _ATTRIBUTE.unpack_from(data, pos)
|
|
209
|
+
pos += 6
|
|
210
|
+
attribute = values[attribute_index]
|
|
211
|
+
if attribute in _ANNOTATION_ATTRIBUTES:
|
|
212
|
+
annotations.extend(_annotation_types(data, pos, values))
|
|
213
|
+
elif attribute == "Deprecated":
|
|
214
|
+
deprecated = True
|
|
215
|
+
pos += length
|
|
216
|
+
members.append((values[name_index], values[descriptor_index], member_access,
|
|
217
|
+
tuple(annotations), deprecated or "java.lang.Deprecated" in annotations))
|
|
218
|
+
return members, pos
|
|
219
|
+
|
|
220
|
+
fields, pos = read_members(pos)
|
|
221
|
+
methods, pos = read_members(pos)
|
|
222
|
+
|
|
223
|
+
annotations: list[str] = []
|
|
224
|
+
deprecated = False
|
|
225
|
+
source_file = None
|
|
226
|
+
attribute_count = _U2.unpack_from(data, pos)[0]
|
|
227
|
+
pos += 2
|
|
228
|
+
for _ in range(attribute_count):
|
|
229
|
+
attribute_index, length = _ATTRIBUTE.unpack_from(data, pos)
|
|
230
|
+
pos += 6
|
|
231
|
+
attribute = values[attribute_index]
|
|
232
|
+
if attribute in _ANNOTATION_ATTRIBUTES:
|
|
233
|
+
annotations.extend(_annotation_types(data, pos, values))
|
|
234
|
+
elif attribute == "Deprecated":
|
|
235
|
+
deprecated = True
|
|
236
|
+
elif attribute == "SourceFile":
|
|
237
|
+
source_file = values[_U2.unpack_from(data, pos)[0]]
|
|
238
|
+
pos += length
|
|
239
|
+
|
|
240
|
+
member_refs = set()
|
|
241
|
+
class_refs = set()
|
|
242
|
+
dynamic_classes = set()
|
|
243
|
+
for index in range(1, count):
|
|
244
|
+
tag = tags[index]
|
|
245
|
+
if tag in _REF_KINDS:
|
|
246
|
+
class_index, nat_index = values[index]
|
|
247
|
+
owner = values[values[class_index]]
|
|
248
|
+
if owner.startswith("["):
|
|
249
|
+
continue # clone() and friends on an array type
|
|
250
|
+
name_index, descriptor_index = values[nat_index]
|
|
251
|
+
member_refs.add((_REF_KINDS[tag], owner.replace("/", "."), values[name_index], values[descriptor_index]))
|
|
252
|
+
elif tag == 7:
|
|
253
|
+
referenced = values[values[index]]
|
|
254
|
+
if referenced.startswith("["):
|
|
255
|
+
element = referenced.lstrip("[")
|
|
256
|
+
if not (element.startswith("L") and element.endswith(";")):
|
|
257
|
+
continue
|
|
258
|
+
referenced = element[1:-1]
|
|
259
|
+
referenced = referenced.replace("/", ".")
|
|
260
|
+
if referenced != name:
|
|
261
|
+
class_refs.add(referenced)
|
|
262
|
+
elif tag == 8:
|
|
263
|
+
text = values[values[index]]
|
|
264
|
+
if _DYNAMIC_CLASS_RE.match(text):
|
|
265
|
+
dynamic_classes.add(text)
|
|
266
|
+
|
|
267
|
+
return {
|
|
268
|
+
"name": name,
|
|
269
|
+
"major": major,
|
|
270
|
+
"access": access,
|
|
271
|
+
"super": super_name,
|
|
272
|
+
"interfaces": interfaces,
|
|
273
|
+
"annotations": annotations,
|
|
274
|
+
"deprecated": deprecated or "java.lang.Deprecated" in annotations,
|
|
275
|
+
"sourceFile": source_file,
|
|
276
|
+
"fields": fields,
|
|
277
|
+
"methods": methods,
|
|
278
|
+
"memberRefs": sorted(member_refs),
|
|
279
|
+
"classRefs": sorted(class_refs),
|
|
280
|
+
"dynamicClassNames": sorted(dynamic_classes),
|
|
281
|
+
}
|