@thinkingai/ae-cli 6.1.20 → 6.1.23
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/README.md +2 -1
- package/README.zh.md +2 -1
- package/dist/{auth-FUM37MHF.js → auth-XZNXFRJS.js} +5 -5
- package/dist/{capability-AXFQW5WM.js → capability-FYTU3X6L.js} +42 -12
- package/dist/{chunk-LNZBEQXW.js → chunk-4NRCTUZJ.js} +27041 -474
- package/dist/{chunk-VXNMYUXU.js → chunk-57RRFUEZ.js} +23 -7
- package/dist/{chunk-WZ6YVQSF.js → chunk-7FTTULED.js} +1 -1
- package/dist/{chunk-ATSM5XAW.js → chunk-7XGFSUOM.js} +2 -2
- package/dist/chunk-CCS66K4K.js +155 -0
- package/dist/{chunk-I4WQAEYB.js → chunk-DVMLWQD4.js} +8 -186
- package/dist/{chunk-CZU3V3DQ.js → chunk-DX5CDO34.js} +27 -6
- package/dist/chunk-EQ255HKO.js +38248 -0
- package/dist/{chunk-BBEFP4SB.js → chunk-HBGADCKA.js} +3 -3
- package/dist/{chunk-QJQZH5GQ.js → chunk-HFGZGYCG.js} +10 -6
- package/dist/{chunk-ECBLHAZO.js → chunk-JOCDD4ON.js} +1 -1
- package/dist/{chunk-TKHSULJT.js → chunk-KPQY44JQ.js} +6 -0
- package/dist/{chunk-4P355ZWB.js → chunk-MAPZ7VB6.js} +21 -1
- package/dist/chunk-V2NUHPXF.js +196 -0
- package/dist/{chunk-RSG4ONOI.js → chunk-VYXXOKYS.js} +5 -5
- package/dist/{community-report-client-XXWGSBSD.js → community-report-client-CU22W53K.js} +1 -1
- package/dist/{config-EXUGQN5T.js → config-XO3LI3SA.js} +4 -4
- package/dist/context-IFJI3LIT.js +46 -0
- package/dist/{data-integration-4NZ53OMT.js → data-integration-HNGUKFIC.js} +44 -35
- package/dist/index.js +101 -64
- package/dist/{local-data-upload-client-KYOKVYZV.js → local-data-upload-client-GTRIT64F.js} +3 -2
- package/dist/{memory-ATNKZNW5.js → memory-QGPSNCQS.js} +4 -4
- package/dist/{metadata-VZVC7YMH.js → metadata-DI5BMYFU.js} +8 -8
- package/dist/{model-E4JMQ4V2.js → model-RNPQXIBI.js} +4 -4
- package/dist/{personal-semantic-preference-7S2SQ3UV.js → personal-semantic-preference-5C3WUXFI.js} +11 -8
- package/dist/{project-semantic-2SZP2OPO.js → project-semantic-3LF6Q6JZ.js} +27 -21
- package/dist/{sync-YV3E66IF.js → sync-O45NXEQY.js} +7 -6
- package/dist/{te-agent-JB5T3PO7.js → te-agent-K5MSPNHJ.js} +5 -5
- package/dist/{te-analysis-3YJAAT2D.js → te-analysis-AISWCOOZ.js} +1086 -354
- package/dist/{te-community-UDBI672N.js → te-community-4OYU3BDA.js} +4 -4
- package/dist/{te-dataops-ZLYOCXZ4.js → te-dataops-NFIPB6FX.js} +5 -5
- package/dist/{te-engage-4XG6OJML.js → te-engage-BAM3GBFX.js} +7 -7
- package/dist/{te-experiment-VXUWPINJ.js → te-experiment-GUTZXZ5Z.js} +132 -14
- package/dist/{te-kb-WYQWHFSC.js → te-kb-3TS73NO6.js} +454 -194
- package/dist/{te-system-7G6F2LJA.js → te-system-STMHLS5J.js} +12 -7
- package/dist/{te-team-E7FBBXMQ.js → te-team-3I5AOX3N.js} +6 -6
- package/dist/{update-D47BUG25.js → update-RZXLPCRH.js} +4 -4
- package/package.json +21 -14
- package/skills/ae-analysis/SKILL.md +91 -78
- package/skills/ae-analysis/references/adhoc_export.md +11 -18
- package/skills/ae-analysis/references/adhoc_run.md +15 -7
- package/skills/ae-analysis/references/agent_review_detail.md +11 -0
- package/skills/ae-analysis/references/agent_review_evidence.md +46 -0
- package/skills/ae-analysis/references/agent_review_list.md +9 -0
- package/skills/ae-analysis/references/agent_review_preflight.md +96 -0
- package/skills/ae-analysis/references/agent_review_priorities_comparisons.md +76 -0
- package/skills/ae-analysis/references/agent_review_records.md +9 -0
- package/skills/ae-analysis/references/agent_review_retry.md +12 -0
- package/skills/ae-analysis/references/agent_review_review.md +16 -0
- package/skills/ae-analysis/references/agent_review_submit_to_page.md +184 -0
- package/skills/ae-analysis/references/ai_models/attribution.md +29 -0
- package/skills/ae-analysis/references/ai_models/distribution.md +40 -0
- package/skills/ae-analysis/references/ai_models/event.md +108 -0
- package/skills/ae-analysis/references/ai_models/funnel.md +46 -0
- package/skills/ae-analysis/references/ai_models/heat_map.md +26 -0
- package/skills/ae-analysis/references/ai_models/interval.md +20 -0
- package/skills/ae-analysis/references/ai_models/path.md +55 -0
- package/skills/ae-analysis/references/ai_models/prop_analysis.md +43 -0
- package/skills/ae-analysis/references/ai_models/rank_list.md +25 -0
- package/skills/ae-analysis/references/ai_models/retention.md +70 -0
- package/skills/ae-analysis/references/ai_models/revenue.md +28 -0
- package/skills/ae-analysis/references/ai_models/sql.md +74 -0
- package/skills/ae-analysis/references/ai_models/tag.md +19 -0
- package/skills/ae-analysis/references/ai_models.md +32 -473
- package/skills/ae-analysis/references/alert_create.md +7 -7
- package/skills/ae-analysis/references/alert_delete.md +4 -6
- package/skills/ae-analysis/references/alert_detail_list.md +7 -9
- package/skills/ae-analysis/references/alert_get.md +5 -7
- package/skills/ae-analysis/references/alert_job_list.md +4 -6
- package/skills/ae-analysis/references/alert_list.md +6 -8
- package/skills/ae-analysis/references/alert_notice_config_list.md +4 -6
- package/skills/ae-analysis/references/alert_start.md +5 -7
- package/skills/ae-analysis/references/alert_stop.md +5 -7
- package/skills/ae-analysis/references/alert_update.md +8 -8
- package/skills/ae-analysis/references/analysis_data_export.md +72 -0
- package/skills/ae-analysis/references/analysis_data_retrieval.md +32 -196
- package/skills/ae-analysis/references/analysis_gateway_assets.md +23 -180
- package/skills/ae-analysis/references/analysis_interpretation.md +37 -17
- package/skills/ae-analysis/references/asset_abnormal_get.md +0 -1
- package/skills/ae-analysis/references/asset_abnormal_list.md +0 -1
- package/skills/ae-analysis/references/asset_authentication_export.md +4 -0
- package/skills/ae-analysis/references/asset_authentication_list.md +7 -1
- package/skills/ae-analysis/references/asset_authentication_update.md +5 -1
- package/skills/ae-analysis/references/asset_batch_dashboard_schedule_freeze.md +8 -6
- package/skills/ae-analysis/references/asset_batch_delete.md +6 -6
- package/skills/ae-analysis/references/asset_batch_disable_auto_backup.md +4 -5
- package/skills/ae-analysis/references/asset_batch_disable_auto_update.md +5 -6
- package/skills/ae-analysis/references/asset_batch_handover.md +4 -5
- package/skills/ae-analysis/references/asset_batch_info_export.md +4 -5
- package/skills/ae-analysis/references/asset_batch_sql_export.md +5 -6
- package/skills/ae-analysis/references/asset_dependency_list.md +4 -5
- package/skills/ae-analysis/references/asset_export.md +4 -5
- package/skills/ae-analysis/references/asset_impact_list.md +5 -9
- package/skills/ae-analysis/references/asset_lineage_get.md +5 -6
- package/skills/ae-analysis/references/asset_list.md +4 -5
- package/skills/ae-analysis/references/asset_query_history_list.md +4 -8
- package/skills/ae-analysis/references/asset_recent_list.md +0 -1
- package/skills/ae-analysis/references/asset_search.md +12 -9
- package/skills/ae-analysis/references/asset_url_get.md +3 -4
- package/skills/ae-analysis/references/bi_panel_copy.md +2 -2
- package/skills/ae-analysis/references/bi_panel_page_data_export.md +4 -12
- package/skills/ae-analysis/references/bi_panel_page_data_run.md +2 -0
- package/skills/ae-analysis/references/catalog_export.md +4 -17
- package/skills/ae-analysis/references/catalog_list.md +4 -4
- package/skills/ae-analysis/references/check_delete.md +5 -7
- package/skills/ae-analysis/references/check_export.md +11 -11
- package/skills/ae-analysis/references/check_get.md +8 -10
- package/skills/ae-analysis/references/check_list.md +4 -6
- package/skills/ae-analysis/references/check_retry.md +8 -10
- package/skills/ae-analysis/references/check_run.md +2 -4
- package/skills/ae-analysis/references/command_index.md +44 -26
- package/skills/ae-analysis/references/dashboard_report_data_export.md +5 -13
- package/skills/ae-analysis/references/dashboard_report_data_run.md +1 -1
- package/skills/ae-analysis/references/dashboard_update.md +14 -3
- package/skills/ae-analysis/references/datatable_columns_get.md +0 -1
- package/skills/ae-analysis/references/datatable_influence_list.md +0 -1
- package/skills/ae-analysis/references/datatable_version_get.md +0 -1
- package/skills/ae-analysis/references/datatable_version_list.md +0 -1
- package/skills/ae-analysis/references/debug_device_add.md +1 -2
- package/skills/ae-analysis/references/drilldown_entities_export.md +3 -3
- package/skills/ae-analysis/references/drilldown_events_export.md +3 -2
- package/skills/ae-analysis/references/drilldown_user_events_export.md +2 -2
- package/skills/ae-analysis/references/entity_detail_export.md +3 -2
- package/skills/ae-analysis/references/entity_detail_run.md +1 -0
- package/skills/ae-analysis/references/event_blacklist_add.md +5 -7
- package/skills/ae-analysis/references/event_blacklist_list.md +4 -6
- package/skills/ae-analysis/references/event_blacklist_update.md +6 -8
- package/skills/ae-analysis/references/event_changelog_list.md +0 -1
- package/skills/ae-analysis/references/event_create.md +0 -1
- package/skills/ae-analysis/references/event_detail_export.md +3 -2
- package/skills/ae-analysis/references/event_detail_run.md +1 -0
- package/skills/ae-analysis/references/event_export.md +0 -1
- package/skills/ae-analysis/references/event_get.md +0 -1
- package/skills/ae-analysis/references/event_hide_update.md +0 -1
- package/skills/ae-analysis/references/event_influence_list.md +0 -1
- package/skills/ae-analysis/references/event_list.md +0 -1
- package/skills/ae-analysis/references/event_property_bundle_export.md +5 -14
- package/skills/ae-analysis/references/event_property_bundle_import.md +2 -6
- package/skills/ae-analysis/references/event_relation_update.md +0 -1
- package/skills/ae-analysis/references/event_update.md +0 -1
- package/skills/ae-analysis/references/exchange_config_update.md +0 -1
- package/skills/ae-analysis/references/exchange_rate_refresh.md +0 -1
- package/skills/ae-analysis/references/exchange_rule_list.md +0 -1
- package/skills/ae-analysis/references/exchange_rule_update.md +0 -1
- package/skills/ae-analysis/references/exchange_rule_validate.md +0 -1
- package/skills/ae-analysis/references/governance_recommendation_decisions.md +23 -0
- package/skills/ae-analysis/references/governance_recommendation_export.md +108 -0
- package/skills/ae-analysis/references/governance_recommendation_submit.md +41 -0
- package/skills/ae-analysis/references/history_tag_data_drilldown_export.md +4 -4
- package/skills/ae-analysis/references/history_tag_data_export.md +3 -3
- package/skills/ae-analysis/references/ingest_error_list.md +7 -9
- package/skills/ae-analysis/references/ingest_summary.md +6 -8
- package/skills/ae-analysis/references/live_data_export.md +10 -10
- package/skills/ae-analysis/references/live_data_list.md +7 -9
- package/skills/ae-analysis/references/metadata_resolution.md +78 -0
- package/skills/ae-analysis/references/metric_export.md +0 -1
- package/skills/ae-analysis/references/metric_get.md +0 -1
- package/skills/ae-analysis/references/metric_list.md +4 -5
- package/skills/ae-analysis/references/metric_update.md +0 -1
- package/skills/ae-analysis/references/operation_record_export.md +4 -5
- package/skills/ae-analysis/references/operation_record_list.md +4 -5
- package/skills/ae-analysis/references/personal_semantic_preference_list.md +18 -1
- package/skills/ae-analysis/references/plan_change_log_export.md +10 -10
- package/skills/ae-analysis/references/plan_change_log_list.md +4 -6
- package/skills/ae-analysis/references/plan_delete_items.md +2 -4
- package/skills/ae-analysis/references/plan_export.md +9 -9
- package/skills/ae-analysis/references/plan_generate.md +2 -4
- package/skills/ae-analysis/references/plan_get.md +4 -6
- package/skills/ae-analysis/references/plan_import_excel.md +6 -8
- package/skills/ae-analysis/references/plan_save_items.md +2 -4
- package/skills/ae-analysis/references/plan_sync_from_meta.md +4 -6
- package/skills/ae-analysis/references/project_access_detail_get.md +0 -1
- package/skills/ae-analysis/references/project_data_power_get.md +0 -1
- package/skills/ae-analysis/references/project_data_power_list.md +0 -1
- package/skills/ae-analysis/references/project_data_power_upsert.md +0 -1
- package/skills/ae-analysis/references/project_entity_create.md +0 -1
- package/skills/ae-analysis/references/project_entity_event_list.md +0 -1
- package/skills/ae-analysis/references/project_entity_get.md +0 -1
- package/skills/ae-analysis/references/project_entity_list.md +0 -1
- package/skills/ae-analysis/references/project_entity_update.md +0 -1
- package/skills/ae-analysis/references/project_function_list.md +0 -1
- package/skills/ae-analysis/references/project_info_get.md +0 -1
- package/skills/ae-analysis/references/project_info_list.md +0 -1
- package/skills/ae-analysis/references/project_info_update.md +0 -1
- package/skills/ae-analysis/references/project_mark_time_create.md +0 -1
- package/skills/ae-analysis/references/project_mark_time_list.md +0 -1
- package/skills/ae-analysis/references/project_mark_time_update.md +0 -1
- package/skills/ae-analysis/references/project_member_add.md +6 -5
- package/skills/ae-analysis/references/project_member_batch_update.md +0 -1
- package/skills/ae-analysis/references/project_member_candidate_list.md +4 -3
- package/skills/ae-analysis/references/project_member_handover_run.md +2 -3
- package/skills/ae-analysis/references/project_member_list.md +0 -1
- package/skills/ae-analysis/references/project_member_receiver_list.md +0 -1
- package/skills/ae-analysis/references/project_member_update.md +0 -1
- package/skills/ae-analysis/references/project_owner_update.md +0 -1
- package/skills/ae-analysis/references/project_permission_binding_list.md +0 -1
- package/skills/ae-analysis/references/project_role_function_list.md +0 -1
- package/skills/ae-analysis/references/project_role_get.md +0 -1
- package/skills/ae-analysis/references/project_role_list.md +0 -1
- package/skills/ae-analysis/references/project_role_upsert.md +32 -2
- package/skills/ae-analysis/references/project_role_user_list.md +0 -1
- package/skills/ae-analysis/references/project_semantic_knowledge_wiki.md +214 -0
- package/skills/ae-analysis/references/project_semantic_knowledge_wiki_plan_schema.md +93 -0
- package/skills/ae-analysis/references/project_timezone_get.md +0 -1
- package/skills/ae-analysis/references/project_timezone_overview.md +0 -1
- package/skills/ae-analysis/references/project_timezone_update.md +0 -1
- package/skills/ae-analysis/references/project_user_id_items_update.md +0 -1
- package/skills/ae-analysis/references/property_changelog_list.md +0 -1
- package/skills/ae-analysis/references/property_create.md +0 -1
- package/skills/ae-analysis/references/property_export.md +0 -1
- package/skills/ae-analysis/references/property_get.md +0 -1
- package/skills/ae-analysis/references/property_hide_update.md +0 -1
- package/skills/ae-analysis/references/property_influence_list.md +0 -1
- package/skills/ae-analysis/references/property_list.md +3 -4
- package/skills/ae-analysis/references/property_related_events.md +0 -1
- package/skills/ae-analysis/references/property_relation_update.md +0 -1
- package/skills/ae-analysis/references/property_update.md +0 -1
- package/skills/ae-analysis/references/report_create.md +4 -2
- package/skills/ae-analysis/references/report_data_export.md +11 -9
- package/skills/ae-analysis/references/report_data_run.md +14 -7
- package/skills/ae-analysis/references/report_get.md +5 -3
- package/skills/ae-analysis/references/report_list.md +2 -0
- package/skills/ae-analysis/references/report_update.md +5 -3
- package/skills/ae-analysis/references/rule_create.md +5 -6
- package/skills/ae-analysis/references/rule_delete.md +5 -5
- package/skills/ae-analysis/references/rule_list.md +3 -4
- package/skills/ae-analysis/references/rule_schema.md +3 -4
- package/skills/ae-analysis/references/rule_update.md +5 -6
- package/skills/ae-analysis/references/sdk_sample_generate.md +2 -4
- package/skills/ae-analysis/references/sql_table_columns.md +1 -1
- package/skills/ae-analysis/references/super_metadata_batch_create.md +1 -1
- package/skills/ae-analysis/references/super_metadata_batch_edit.md +1 -1
- package/skills/ae-analysis/references/system_member_add.md +1 -2
- package/skills/ae-analysis/references/system_member_status_update.md +1 -1
- package/skills/ae-analysis/references/system_ops_alert_contact_list.md +1 -1
- package/skills/ae-analysis/references/system_ops_alert_contact_test.md +0 -1
- package/skills/ae-analysis/references/system_query_monitor_overview.md +3 -3
- package/skills/ae-analysis/references/system_query_task_export.md +4 -14
- package/skills/ae-analysis/references/system_query_task_get.md +1 -1
- package/skills/ae-analysis/references/system_query_task_list.md +5 -6
- package/skills/ae-analysis/references/system_receiver_address_upsert.md +3 -3
- package/skills/ae-analysis/references/system_receiver_detection_get.md +1 -1
- package/skills/ae-analysis/references/system_third_party_login_disable.md +1 -1
- package/skills/ae-analysis/references/system_usage_trend_export.md +3 -12
- package/skills/ae-analysis/references/user_cluster_list.md +1 -1
- package/skills/ae-analysis/references/user_cluster_member_export.md +4 -4
- package/skills/ae-analysis/references/user_tag_list.md +1 -1
- package/skills/ae-analysis/references/user_tag_member_export.md +4 -4
- package/skills/ae-analysis/references/virtual_event_create.md +10 -5
- package/skills/ae-analysis/references/virtual_event_get.md +0 -1
- package/skills/ae-analysis/references/virtual_property_create.md +0 -1
- package/skills/ae-analysis/references/virtual_property_sql_rule_update.md +0 -1
- package/skills/ae-analysis/scripts/project-semantic-knowledge-wiki/build-project-semantic-wiki.mjs +1054 -0
- package/skills/ae-analysis/scripts/project-semantic-knowledge-wiki/company-kb.mjs +112 -0
- package/skills/ae-analysis/scripts/project-semantic-knowledge-wiki/default-compile-rules.md +30 -0
- package/skills/ae-analysis/scripts/project-semantic-knowledge-wiki/generate-build-ir.mjs +2735 -0
- package/skills/ae-analysis/scripts/project-semantic-knowledge-wiki/package-wiki-source-zip.mjs +376 -0
- package/skills/ae-analysis/scripts/project-semantic-knowledge-wiki/plan-kb-source-sync.mjs +264 -0
- package/skills/ae-analysis/scripts/project-semantic-knowledge-wiki/precompiled-source.mjs +49 -0
- package/skills/ae-analysis/scripts/project-semantic-knowledge-wiki/upload-kb-sources-concurrently.mjs +200 -0
- package/skills/ae-analysis/scripts/read_results.py +197 -0
- package/skills/ae-analysis-global/SKILL.md +1 -1
- package/skills/ae-capability/SKILL.md +1 -1
- package/skills/ae-community/SKILL.md +1 -1
- package/skills/ae-current-context/SKILL.md +53 -0
- package/skills/ae-data-integration/SKILL.md +5 -4
- package/skills/ae-data-integration/references/lark-bitable-source.md +288 -0
- package/skills/ae-data-integration/references/lark-sheet-source.md +130 -0
- package/skills/ae-data-integration/references/manual-export-source.md +83 -0
- package/skills/ae-data-integration/references/reuse.md +2 -0
- package/skills/ae-data-integration/references/sink-upload.md +32 -0
- package/skills/ae-data-integration/references/tracking-plan.md +4 -1
- package/skills/ae-data-integration/references/ue-mapping.md +2 -2
- package/skills/ae-data-integration-helper/SKILL.md +5 -5
- package/skills/ae-data-integration-helper/references/android_sdk_faq.md +3 -3
- package/skills/ae-data-integration-helper/references/c_sdk_faq.md +3 -3
- package/skills/ae-data-integration-helper/references/cocoscreator_sdk_faq.md +1 -1
- package/skills/ae-data-integration-helper/references/cpp_server_sdk_faq.md +3 -3
- package/skills/ae-data-integration-helper/references/ios_sdk_faq.md +1 -1
- package/skills/ae-data-integration-helper/references/java_sdk_faq.md +3 -3
- package/skills/ae-data-integration-helper/references/javascript_sdk_faq.md +1 -1
- package/skills/ae-data-integration-helper/references/logbus2_guide.md +1 -1
- package/skills/ae-data-integration-helper/references/logbus2_parser_plugin.md +2 -2
- package/skills/ae-data-integration-helper/references/minigame_sdk_faq.md +2 -2
- package/skills/ae-data-integration-helper/references/miniprogram_sdk_faq.md +1 -1
- package/skills/ae-data-integration-helper/references/python_sdk_faq.md +2 -2
- package/skills/ae-data-integration-helper/references/restful_api_notes.md +2 -2
- package/skills/ae-data-integration-helper/references/sdk_log_guide.md +3 -3
- package/skills/ae-data-integration-helper/references/sdk_usage_notes.md +11 -11
- package/skills/ae-data-integration-helper/references/unity_sdk_faq.md +1 -1
- package/skills/ae-dataops/SKILL.md +1 -1
- package/skills/ae-engage/SKILL.md +4 -3
- package/skills/ae-engage/references/build-task-save-guide.md +3 -0
- package/skills/ae-engage/references/flow-metric-update.md +115 -1
- package/skills/ae-engage/references/save-flow.md +3 -0
- package/skills/ae-engage/references/save-task.md +21 -0
- package/skills/ae-engage/references/task-metric-update.md +151 -0
- package/skills/ae-experiment/SKILL.md +14 -7
- package/skills/ae-experiment/references/save_submit_external_experiment.md +52 -0
- package/skills/ae-experiment-design/SKILL.md +3 -3
- package/skills/ae-experiment-design/references/platform-operations.md +1 -1
- package/skills/ae-experiment-insight/SKILL.md +2 -2
- package/skills/ae-experiment-insight/references/platform-operations.md +1 -1
- package/skills/ae-generate-tracking-code/SKILL.md +2 -1
- package/skills/ae-generate-tracking-code/references/client-sdk-insert.md +1 -1
- package/skills/ae-generate-tracking-code/references/datax-config.md +1 -1
- package/skills/ae-generate-tracking-code/references/logbus-config.md +1 -1
- package/skills/ae-generate-tracking-code/references/modes.md +1 -1
- package/skills/ae-generate-tracking-code/references/server-sdk-insert.md +5 -5
- package/skills/ae-generate-tracking-code/references/snippet-delivery.md +3 -3
- package/skills/ae-generate-tracking-plan/SKILL.md +2 -2
- package/skills/ae-generate-tracking-plan/references/te-api.md +1 -0
- package/skills/ae-generate-tracking-plan/references/xlsx-schema.md +3 -2
- package/skills/ae-kb/SKILL.md +41 -2
- package/skills/ae-kb/references/analysis-workflow.md +292 -0
- package/skills/ae-kb/references/source-mutations.md +95 -0
- package/skills/ae-kb-discovery/SKILL.md +5 -1
- package/skills/ae-metadata/SKILL.md +1 -1
- package/skills/ae-migrate-tracking-code/SKILL.md +195 -0
- package/skills/ae-migrate-tracking-code/references/ae-preset-properties.md +76 -0
- package/skills/ae-migrate-tracking-code/references/ir.md +167 -0
- package/skills/ae-migrate-tracking-code/references/mapping-framework.md +112 -0
- package/skills/ae-migrate-tracking-code/references/providers/README.md +58 -0
- package/skills/ae-migrate-tracking-code/references/providers/amplitude.md +208 -0
- package/skills/ae-migrate-tracking-code/references/providers/firebase.md +160 -0
- package/skills/ae-migrate-tracking-code/references/providers/ga4.md +92 -0
- package/skills/ae-migrate-tracking-code/references/providers/mixpanel.md +150 -0
- package/skills/ae-migrate-tracking-code/references/providers/sensors-data.md +146 -0
- package/skills/ae-project-semantic/SKILL.md +1 -1
- package/skills/ae-system/SKILL.md +4 -4
- package/skills/ae-team/SKILL.md +2 -2
- package/skills/ae-use-agent/references/local-agent.md +1 -1
- package/dist/chunk-TAL6CZH6.js +0 -72
- package/skills/ae-analysis/metadata_resolution.md +0 -184
|
@@ -0,0 +1,2735 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { createHash, randomBytes } from 'node:crypto';
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
|
|
7
|
+
const args = parseArgs(process.argv.slice(2));
|
|
8
|
+
const packageRoot = path.resolve(requiredArg(args, 'asset-package'));
|
|
9
|
+
const domainPlanPath = path.resolve(requiredArg(args, 'domain-plan'));
|
|
10
|
+
const outputRoot = path.resolve(requiredArg(args, 'output'));
|
|
11
|
+
const force = Boolean(args.force);
|
|
12
|
+
const allowTruncated = Boolean(args['allow-truncated']);
|
|
13
|
+
const allowAllVisible = Boolean(args['allow-all-visible']);
|
|
14
|
+
|
|
15
|
+
assertSafePaths(packageRoot, outputRoot);
|
|
16
|
+
|
|
17
|
+
const descriptor = readObject(path.join(packageRoot, '.asset-package.json'), 'asset package descriptor');
|
|
18
|
+
const manifest = readObject(path.join(packageRoot, 'manifest.json'), 'asset package manifest');
|
|
19
|
+
if (String(descriptor.schema_version ?? manifest.schema_version) !== '3.0') {
|
|
20
|
+
fail('Project semantic asset package schema 3.0 is required. Re-export it from the updated Common service.');
|
|
21
|
+
}
|
|
22
|
+
if ((descriptor.truncated === true || manifest.truncated === true) && !allowTruncated) {
|
|
23
|
+
fail('The asset package is truncated. Re-export a complete package or pass --allow-truncated explicitly.');
|
|
24
|
+
}
|
|
25
|
+
const assetScope = text(descriptor.asset_scope ?? manifest.asset_scope);
|
|
26
|
+
const filterPolicy = object(descriptor.filter_policy ?? manifest.filter_policy);
|
|
27
|
+
if (assetScope === 'governed') {
|
|
28
|
+
if (filterPolicy.authenticated_entry_assets_only !== true || filterPolicy.dependency_closure !== true) {
|
|
29
|
+
fail('The governed asset package must declare authenticated_entry_assets_only=true and dependency_closure=true.');
|
|
30
|
+
}
|
|
31
|
+
} else if (assetScope === 'all_visible') {
|
|
32
|
+
if (!allowAllVisible) {
|
|
33
|
+
fail('The primary knowledge corpus defaults to asset_scope=governed. Pass --allow-all-visible only when the user explicitly requested all visible assets.');
|
|
34
|
+
}
|
|
35
|
+
} else {
|
|
36
|
+
fail(`The primary knowledge corpus supports asset_scope=governed by default or asset_scope=all_visible with --allow-all-visible, received ${assetScope || 'unknown'}.`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const projectId = Number(descriptor.project_id ?? manifest.project_id);
|
|
40
|
+
if (!Number.isInteger(projectId) || projectId < 1) fail('The asset package is missing a valid project_id.');
|
|
41
|
+
const snapshotHash = text(descriptor.snapshot_hash);
|
|
42
|
+
if (!/^[0-9a-f]{64}$/i.test(snapshotHash)) fail('The asset package descriptor is missing a valid snapshot_hash.');
|
|
43
|
+
const projectName = text(args['project-name'] ?? descriptor.project_name ?? manifest.project_name)
|
|
44
|
+
|| `Project ${projectId}`;
|
|
45
|
+
|
|
46
|
+
const containers = rows('asset-containers');
|
|
47
|
+
const dashboards = rows('dashboard-catalog');
|
|
48
|
+
const reports = rows('report-catalog');
|
|
49
|
+
const metrics = rows('metric-catalog');
|
|
50
|
+
const metadata = readJsonl(path.join(packageRoot, 'metadata', 'analysis-selectable.jsonl'))
|
|
51
|
+
.filter((row) => row.record_type !== 'header')
|
|
52
|
+
.map(normalizeMetadata);
|
|
53
|
+
if (dashboards.length + reports.length + metrics.length === 0) fail('The knowledge catalogs are empty.');
|
|
54
|
+
const unauthenticatedDashboards = dashboards.filter((row) => row.authenticated !== true);
|
|
55
|
+
if (assetScope === 'governed' && unauthenticatedDashboards.length) {
|
|
56
|
+
fail(`The governed asset package contains ${unauthenticatedDashboards.length} unauthenticated dashboard entry asset(s): `
|
|
57
|
+
+ `${unauthenticatedDashboards.slice(0, 20).map((row) => text(row.resource_key)).join(', ')}.`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const dashboardById = index(dashboards, 'resource_key');
|
|
61
|
+
const reportById = index(reports, 'resource_key');
|
|
62
|
+
validatePackageReferenceIntegrity();
|
|
63
|
+
const dashboardFiles = fileMap(dashboards, 'resource_key', 'dashboard');
|
|
64
|
+
const reportFiles = fileMap(reports, 'resource_key', 'report');
|
|
65
|
+
const metricFiles = fileMap(metrics, 'resource_key', 'metric');
|
|
66
|
+
const containerFiles = fileMap(containers, 'container_key', 'container');
|
|
67
|
+
const metadataFiles = fileMap(metadata, 'evidence_id', 'metadata');
|
|
68
|
+
const metadataByResourceKey = groupBy(metadata, 'resource_key');
|
|
69
|
+
const metadataUsageByEvidenceId = metadataUsage();
|
|
70
|
+
const metadataMetricUsageByEvidenceId = metadataMetricUsage();
|
|
71
|
+
const packageJsonCache = new Map();
|
|
72
|
+
|
|
73
|
+
const domainPlan = readObject(domainPlanPath, 'business domain plan');
|
|
74
|
+
const classifications = validateAndEnrichDomainPlan(domainPlan);
|
|
75
|
+
const domains = classifications.domains;
|
|
76
|
+
const appendices = classifications.appendices;
|
|
77
|
+
const domainByDashboardId = new Map();
|
|
78
|
+
for (const domain of domains) {
|
|
79
|
+
for (const id of domain.dashboard_ids) domainByDashboardId.set(id, domain);
|
|
80
|
+
}
|
|
81
|
+
const appendixByDashboardId = new Map();
|
|
82
|
+
for (const appendix of appendices) {
|
|
83
|
+
for (const id of appendix.dashboard_ids) appendixByDashboardId.set(id, appendix);
|
|
84
|
+
}
|
|
85
|
+
const domainFiles = fileMap(domains, 'domain_id', 'domain');
|
|
86
|
+
const appendixFiles = fileMap(appendices, 'appendix_id', 'appendix');
|
|
87
|
+
const standaloneReports = reports.filter((row) => row.standalone === true);
|
|
88
|
+
const unassignedDashboards = dashboards.filter((row) =>
|
|
89
|
+
text(object(row.source_container_ref).container_kind) === 'unassigned');
|
|
90
|
+
const physicalContainers = containers.filter((row) => text(row.container_kind) === 'dashboard_space');
|
|
91
|
+
const technicalContainers = containers.filter((row) => text(row.container_kind) !== 'dashboard_space');
|
|
92
|
+
const sharedDashboards = dashboards.filter((row) =>
|
|
93
|
+
text(object(row.source_container_ref).container_kind) === 'shared_assets');
|
|
94
|
+
const reportMetricOccurrences = reports.reduce((sum, row) => sum + array(row.metric_occurrences).length, 0);
|
|
95
|
+
const sqlReports = reports.filter((row) => row.definition_kind === 'SQL');
|
|
96
|
+
const sqlReportSemanticsById = new Map(array(domainPlan.sql_report_semantics)
|
|
97
|
+
.map((value) => object(value))
|
|
98
|
+
.map((value) => [text(value.report_id), value])
|
|
99
|
+
.filter(([id]) => id));
|
|
100
|
+
const semanticConflicts = detectSemanticConflicts();
|
|
101
|
+
const reportConflicts = semanticConflicts.reportConflicts;
|
|
102
|
+
const metadataSemanticConflicts = semanticConflicts.metadataConflicts;
|
|
103
|
+
const metadataMissingTitles = metadata.filter((item) => !text(item.title));
|
|
104
|
+
const reportsInBusinessDomains = reports.filter((report) => domainsForReport(report).length);
|
|
105
|
+
const reportsInTechnicalAppendices = reports.filter((report) => appendicesForReport(report).length);
|
|
106
|
+
const warnings = [
|
|
107
|
+
...(text(descriptor.asset_scope ?? manifest.asset_scope) !== 'governed'
|
|
108
|
+
? [`The package scope is ${text(descriptor.asset_scope ?? manifest.asset_scope) || 'unknown'}; unauthenticated assets may be present.`]
|
|
109
|
+
: []),
|
|
110
|
+
...(unassignedDashboards.length > 0
|
|
111
|
+
? [`${unassignedDashboards.length} dashboards have no physical dashboard-space placement.`]
|
|
112
|
+
: []),
|
|
113
|
+
...(standaloneReports.length > 0
|
|
114
|
+
? [`${standaloneReports.length} reports are not referenced by a dashboard and therefore have no inferred business domain.`]
|
|
115
|
+
: []),
|
|
116
|
+
...(reportConflicts.size > 0
|
|
117
|
+
? [`${reportConflicts.size} assets have detectable semantic conflicts.`]
|
|
118
|
+
: []),
|
|
119
|
+
...(metadataMissingTitles.length > 0
|
|
120
|
+
? [`${metadataMissingTitles.length} metadata items have no source title and use their technical resource key.`]
|
|
121
|
+
: []),
|
|
122
|
+
];
|
|
123
|
+
const domainPlanHash = createHash('sha256').update(fs.readFileSync(domainPlanPath)).digest('hex');
|
|
124
|
+
const corpus = {
|
|
125
|
+
schema_version: '3.0',
|
|
126
|
+
corpus_kind: 'ae_project_asset_raw_sources',
|
|
127
|
+
project_id: projectId,
|
|
128
|
+
project_name: projectName,
|
|
129
|
+
snapshot_id: descriptor.snapshot_id ?? null,
|
|
130
|
+
snapshot_hash: snapshotHash,
|
|
131
|
+
asset_scope: assetScope,
|
|
132
|
+
truncated: Boolean(descriptor.truncated ?? manifest.truncated),
|
|
133
|
+
business_domain_source: 'agent-authored semantic plan validated against every dashboard in the asset package',
|
|
134
|
+
business_domain_plan_hash: domainPlanHash,
|
|
135
|
+
counts: {
|
|
136
|
+
business_domains: domains.length,
|
|
137
|
+
technical_appendices: appendices.length,
|
|
138
|
+
source_dashboard_spaces: physicalContainers.length,
|
|
139
|
+
technical_containers: technicalContainers.length,
|
|
140
|
+
dashboards: dashboards.length,
|
|
141
|
+
dashboards_in_business_domains: domainByDashboardId.size,
|
|
142
|
+
dashboards_in_technical_appendices: appendixByDashboardId.size,
|
|
143
|
+
dashboards_classified_once: domainByDashboardId.size + appendixByDashboardId.size,
|
|
144
|
+
shared_dashboards: sharedDashboards.length,
|
|
145
|
+
unassigned_dashboards: unassignedDashboards.length,
|
|
146
|
+
reports: reports.length,
|
|
147
|
+
reports_linked_to_dashboards: reports.length - standaloneReports.length,
|
|
148
|
+
reports_in_business_domains: reportsInBusinessDomains.length,
|
|
149
|
+
reports_in_technical_appendices: reportsInTechnicalAppendices.length,
|
|
150
|
+
standalone_reports: standaloneReports.length,
|
|
151
|
+
reusable_metrics: metrics.length,
|
|
152
|
+
analysis_metadata: metadata.length,
|
|
153
|
+
metadata_type_counts: valueCounts(metadata, 'resource_type'),
|
|
154
|
+
authenticated_assets: count(descriptor.authenticated_asset_count ?? manifest.authenticated_asset_count),
|
|
155
|
+
unauthenticated_dependencies: count(descriptor.unauthenticated_asset_count ?? manifest.unauthenticated_asset_count),
|
|
156
|
+
authenticated_reports: reports.filter((row) => row.authenticated === true).length,
|
|
157
|
+
unauthenticated_reports: reports.filter((row) => row.authenticated !== true).length,
|
|
158
|
+
authenticated_metadata: metadata.filter((row) => row.authenticated === true).length,
|
|
159
|
+
unauthenticated_metadata: metadata.filter((row) => row.authenticated !== true).length,
|
|
160
|
+
semantic_conflict_reports: reportConflicts.size,
|
|
161
|
+
semantic_conflict_metadata: metadataSemanticConflicts.size,
|
|
162
|
+
metadata_missing_source_titles: metadataMissingTitles.length,
|
|
163
|
+
report_metric_occurrences: reportMetricOccurrences,
|
|
164
|
+
sql_reports: sqlReports.length,
|
|
165
|
+
},
|
|
166
|
+
warnings,
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
materialize(outputRoot, force, (staging) => {
|
|
170
|
+
for (const directory of [
|
|
171
|
+
'_source', 'domains', 'appendices', 'source-containers', 'dashboards', 'reports', 'metrics', 'indexes',
|
|
172
|
+
'metadata', 'briefs/domains', 'briefs/dashboards', 'briefs/reports', 'briefs/metrics', 'briefs/metadata',
|
|
173
|
+
]) fs.mkdirSync(path.join(staging, directory), { recursive: true });
|
|
174
|
+
for (const type of [...new Set(metadata.map((row) => metadataDirectory(row.resource_type)))]) {
|
|
175
|
+
fs.mkdirSync(path.join(staging, 'metadata', type), { recursive: true });
|
|
176
|
+
fs.mkdirSync(path.join(staging, 'briefs', 'metadata', type), { recursive: true });
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
fs.copyFileSync(path.join(packageRoot, '.asset-package.json'), path.join(staging, '_source', 'asset-package.json'));
|
|
180
|
+
fs.copyFileSync(path.join(packageRoot, 'manifest.json'), path.join(staging, '_source', 'manifest.json'));
|
|
181
|
+
fs.copyFileSync(path.join(packageRoot, 'metadata', 'analysis-selectable.jsonl'),
|
|
182
|
+
path.join(staging, '_source', 'analysis-selectable.jsonl'));
|
|
183
|
+
fs.copyFileSync(domainPlanPath, path.join(staging, '_source', 'business-domain-plan.json'));
|
|
184
|
+
for (const name of ['asset-containers', 'dashboard-catalog', 'report-catalog', 'metric-catalog']) {
|
|
185
|
+
fs.copyFileSync(path.join(packageRoot, 'indexes', `${name}.jsonl`), path.join(staging, '_source', `${name}.jsonl`));
|
|
186
|
+
}
|
|
187
|
+
writeJson(path.join(staging, 'corpus.json'), corpus);
|
|
188
|
+
writeText(path.join(staging, 'index.md'), renderRootIndex());
|
|
189
|
+
|
|
190
|
+
for (const domain of domains) {
|
|
191
|
+
const key = text(domain.domain_id);
|
|
192
|
+
writeText(path.join(staging, 'domains', domainFiles.get(key)), renderDomainSource(domain));
|
|
193
|
+
writeText(path.join(staging, 'briefs', 'domains', domainFiles.get(key)), renderDomainBrief(domain));
|
|
194
|
+
}
|
|
195
|
+
for (const appendix of appendices) {
|
|
196
|
+
const key = text(appendix.appendix_id);
|
|
197
|
+
writeText(path.join(staging, 'appendices', appendixFiles.get(key)), renderAppendixSource(appendix));
|
|
198
|
+
}
|
|
199
|
+
for (const container of sort(containers, 'container_key')) {
|
|
200
|
+
const key = text(container.container_key);
|
|
201
|
+
writeText(path.join(staging, 'source-containers', containerFiles.get(key)), renderContainerSource(container));
|
|
202
|
+
}
|
|
203
|
+
for (const dashboard of sort(dashboards, 'resource_key')) {
|
|
204
|
+
const id = text(dashboard.resource_key);
|
|
205
|
+
writeText(path.join(staging, 'dashboards', dashboardFiles.get(id)), renderDashboardSource(dashboard));
|
|
206
|
+
writeText(path.join(staging, 'briefs', 'dashboards', dashboardFiles.get(id)), renderDashboardBrief(dashboard));
|
|
207
|
+
}
|
|
208
|
+
for (const report of sort(reports, 'resource_key')) {
|
|
209
|
+
const id = text(report.resource_key);
|
|
210
|
+
writeText(path.join(staging, 'reports', reportFiles.get(id)), renderReportSource(report));
|
|
211
|
+
writeText(path.join(staging, 'briefs', 'reports', reportFiles.get(id)), renderReportBrief(report));
|
|
212
|
+
}
|
|
213
|
+
for (const metric of sort(metrics, 'resource_key')) {
|
|
214
|
+
const id = text(metric.resource_key);
|
|
215
|
+
writeText(path.join(staging, 'metrics', metricFiles.get(id)), renderMetricSource(metric));
|
|
216
|
+
writeText(path.join(staging, 'briefs', 'metrics', metricFiles.get(id)), renderMetricBrief(metric));
|
|
217
|
+
}
|
|
218
|
+
for (const item of sort(metadata, 'evidence_id')) {
|
|
219
|
+
const id = text(item.evidence_id);
|
|
220
|
+
const directory = metadataDirectory(item.resource_type);
|
|
221
|
+
writeText(path.join(staging, 'metadata', directory, metadataFiles.get(id)), renderMetadataSource(item));
|
|
222
|
+
writeText(path.join(staging, 'briefs', 'metadata', directory, metadataFiles.get(id)), renderMetadataBrief(item));
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
writeText(path.join(staging, 'domains', 'index.md'), renderAssetIndex('Business domains', domains, domainFiles, 'domain_id'));
|
|
226
|
+
writeText(path.join(staging, 'appendices', 'index.md'), renderAssetIndex('Technical appendices', appendices, appendixFiles, 'appendix_id'));
|
|
227
|
+
writeText(path.join(staging, 'source-containers', 'index.md'), renderAssetIndex('Source containers', containers, containerFiles, 'container_key'));
|
|
228
|
+
writeText(path.join(staging, 'dashboards', 'index.md'), renderAssetIndex('Dashboards', dashboards, dashboardFiles));
|
|
229
|
+
writeText(path.join(staging, 'reports', 'index.md'), renderAssetIndex('Reports', reports, reportFiles));
|
|
230
|
+
writeText(path.join(staging, 'metrics', 'index.md'), renderAssetIndex('Reusable metrics', metrics, metricFiles));
|
|
231
|
+
writeText(path.join(staging, 'metadata', 'index.md'), renderMetadataIndex(false));
|
|
232
|
+
writeText(path.join(staging, 'briefs', 'index.md'), renderBriefIndex());
|
|
233
|
+
writeText(path.join(staging, 'briefs', 'domains', 'index.md'), renderBriefList('Business domain briefs', domains, domainFiles, 'domain_id'));
|
|
234
|
+
writeText(path.join(staging, 'briefs', 'dashboards', 'index.md'), renderBriefList('Dashboard briefs', dashboards, dashboardFiles));
|
|
235
|
+
writeText(path.join(staging, 'briefs', 'reports', 'index.md'), renderBriefList('Report briefs', reports, reportFiles));
|
|
236
|
+
writeText(path.join(staging, 'briefs', 'metrics', 'index.md'), renderBriefList('Metric briefs', metrics, metricFiles));
|
|
237
|
+
writeText(path.join(staging, 'briefs', 'metadata', 'index.md'), renderMetadataIndex(true));
|
|
238
|
+
writeText(path.join(staging, 'indexes', 'coverage.md'), renderCoverage());
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
process.stdout.write(`${JSON.stringify({ output_path: outputRoot, ...corpus }, null, 2)}\n`);
|
|
242
|
+
|
|
243
|
+
function validateAndEnrichDomainPlan(plan) {
|
|
244
|
+
if (!['1.0', '2.0'].includes(text(plan.schema_version))) {
|
|
245
|
+
fail('Business domain plan schema 1.0 or project semantic Wiki plan schema 2.0 is required.');
|
|
246
|
+
}
|
|
247
|
+
if (text(plan.source_snapshot_hash) !== snapshotHash) {
|
|
248
|
+
fail(`Business domain plan snapshot mismatch: expected ${snapshotHash}.`);
|
|
249
|
+
}
|
|
250
|
+
if (text(plan.generation_method) !== 'agent_semantic_synthesis') {
|
|
251
|
+
fail('Business domain plan generation_method must be agent_semantic_synthesis.');
|
|
252
|
+
}
|
|
253
|
+
const domainValues = array(plan.domains);
|
|
254
|
+
const appendixValues = array(plan.appendices);
|
|
255
|
+
if (!domainValues.length) fail('Business domain plan must contain at least one domain.');
|
|
256
|
+
const domainIds = new Set();
|
|
257
|
+
const domainTitles = new Set();
|
|
258
|
+
const appendixIds = new Set();
|
|
259
|
+
const appendixTitles = new Set();
|
|
260
|
+
const assigned = new Map();
|
|
261
|
+
const technicalKeys = new Set(technicalContainerIdentityValues());
|
|
262
|
+
const enrichedDomains = domainValues.map((value, domainIndex) => {
|
|
263
|
+
const domain = object(value);
|
|
264
|
+
const id = text(domain.domain_id);
|
|
265
|
+
const title = text(domain.title);
|
|
266
|
+
const summary = text(domain.summary);
|
|
267
|
+
const rationale = text(domain.merge_rationale);
|
|
268
|
+
const questions = array(domain.primary_questions).map(text).filter(Boolean);
|
|
269
|
+
const primaryMetrics = normalizeAgentAuthoredDomainFacts(domain.primary_metrics);
|
|
270
|
+
const drilldownDimensions = normalizeAgentAuthoredDomainFacts(domain.drilldown_dimensions);
|
|
271
|
+
const dashboardIds = array(domain.dashboard_ids).map(text).filter(Boolean);
|
|
272
|
+
if (!/^[a-z0-9][a-z0-9_-]{1,79}$/.test(id)) fail(`Domain ${domainIndex + 1} has an invalid domain_id: ${id || '<empty>'}.`);
|
|
273
|
+
if (!title) fail(`Domain ${id} is missing title.`);
|
|
274
|
+
if (!summary) fail(`Domain ${id} is missing summary.`);
|
|
275
|
+
if (!rationale) fail(`Domain ${id} is missing merge_rationale.`);
|
|
276
|
+
if (!questions.length) fail(`Domain ${id} is missing primary_questions.`);
|
|
277
|
+
if (!primaryMetrics.length) fail(`Domain ${id} is missing agent-authored primary_metrics.`);
|
|
278
|
+
if (!drilldownDimensions.length) fail(`Domain ${id} is missing agent-authored drilldown_dimensions.`);
|
|
279
|
+
if (!dashboardIds.length) fail(`Domain ${id} does not assign any dashboards.`);
|
|
280
|
+
const normalizedTitle = title.normalize('NFKC').toLocaleLowerCase('zh-CN');
|
|
281
|
+
if (domainIds.has(id)) fail(`Business domain plan contains duplicate domain_id: ${id}.`);
|
|
282
|
+
if (domainTitles.has(normalizedTitle)) fail(`Business domain plan contains duplicate domain title: ${title}.`);
|
|
283
|
+
if (technicalKeys.has(id) || technicalKeys.has(normalizedTitle)) {
|
|
284
|
+
fail(`Technical source container cannot be used as a business domain: ${title} (${id}).`);
|
|
285
|
+
}
|
|
286
|
+
domainIds.add(id);
|
|
287
|
+
domainTitles.add(normalizedTitle);
|
|
288
|
+
for (const dashboardId of dashboardIds) {
|
|
289
|
+
if (!dashboardById.has(dashboardId)) fail(`Domain ${id} references unknown dashboard_id: ${dashboardId}.`);
|
|
290
|
+
if (assigned.has(dashboardId)) {
|
|
291
|
+
fail(`Dashboard ${dashboardId} is assigned to both ${assigned.get(dashboardId)} and ${id}.`);
|
|
292
|
+
}
|
|
293
|
+
assigned.set(dashboardId, id);
|
|
294
|
+
}
|
|
295
|
+
assertAgentAuthoredDomain(domain, `Domain ${id}`);
|
|
296
|
+
const dashboardRows = dashboardIds.map((dashboardId) => dashboardById.get(dashboardId));
|
|
297
|
+
const sourceContainerRefs = uniqueObjects(dashboardRows.map((row) => object(row.source_container_ref)), 'container_key');
|
|
298
|
+
const reportRefs = uniqueObjects(dashboardRows.flatMap((row) => array(row.report_refs)), 'report_id');
|
|
299
|
+
return {
|
|
300
|
+
domain_id: id,
|
|
301
|
+
title,
|
|
302
|
+
summary,
|
|
303
|
+
primary_questions: questions,
|
|
304
|
+
primary_metrics: primaryMetrics,
|
|
305
|
+
drilldown_dimensions: drilldownDimensions,
|
|
306
|
+
merge_rationale: rationale,
|
|
307
|
+
dashboard_ids: dashboardIds,
|
|
308
|
+
dashboard_refs: dashboardRows.map((row) => ({
|
|
309
|
+
dashboard_id: text(row.resource_key),
|
|
310
|
+
dashboard_name: text(row.title),
|
|
311
|
+
report_count: array(row.report_refs).length,
|
|
312
|
+
})),
|
|
313
|
+
source_container_refs: sourceContainerRefs,
|
|
314
|
+
report_refs: reportRefs,
|
|
315
|
+
};
|
|
316
|
+
});
|
|
317
|
+
const enrichedAppendices = appendixValues.map((value, appendixIndex) => {
|
|
318
|
+
const appendix = object(value);
|
|
319
|
+
const id = text(appendix.appendix_id);
|
|
320
|
+
const title = text(appendix.title);
|
|
321
|
+
const summary = text(appendix.summary);
|
|
322
|
+
const rationale = text(appendix.selection_rationale);
|
|
323
|
+
const dashboardIds = array(appendix.dashboard_ids).map(text).filter(Boolean);
|
|
324
|
+
if (!/^[a-z0-9][a-z0-9_-]{1,79}$/.test(id)) fail(`Appendix ${appendixIndex + 1} has an invalid appendix_id: ${id || '<empty>'}.`);
|
|
325
|
+
if (!title) fail(`Appendix ${id} is missing title.`);
|
|
326
|
+
if (!summary) fail(`Appendix ${id} is missing summary.`);
|
|
327
|
+
if (!rationale) fail(`Appendix ${id} is missing selection_rationale.`);
|
|
328
|
+
if (!dashboardIds.length) fail(`Appendix ${id} does not assign any dashboards.`);
|
|
329
|
+
const normalizedTitle = title.normalize('NFKC').toLocaleLowerCase('zh-CN');
|
|
330
|
+
if (appendixIds.has(id) || domainIds.has(id)) fail(`Business domain plan contains duplicate classification id: ${id}.`);
|
|
331
|
+
if (appendixTitles.has(normalizedTitle) || domainTitles.has(normalizedTitle)) {
|
|
332
|
+
fail(`Business domain plan contains duplicate classification title: ${title}.`);
|
|
333
|
+
}
|
|
334
|
+
appendixIds.add(id);
|
|
335
|
+
appendixTitles.add(normalizedTitle);
|
|
336
|
+
for (const dashboardId of dashboardIds) {
|
|
337
|
+
if (!dashboardById.has(dashboardId)) fail(`Appendix ${id} references unknown dashboard_id: ${dashboardId}.`);
|
|
338
|
+
if (assigned.has(dashboardId)) {
|
|
339
|
+
fail(`Dashboard ${dashboardId} is assigned to both ${assigned.get(dashboardId)} and appendix:${id}.`);
|
|
340
|
+
}
|
|
341
|
+
assigned.set(dashboardId, `appendix:${id}`);
|
|
342
|
+
}
|
|
343
|
+
const dashboardRows = dashboardIds.map((dashboardId) => dashboardById.get(dashboardId));
|
|
344
|
+
return {
|
|
345
|
+
appendix_id: id,
|
|
346
|
+
title,
|
|
347
|
+
summary,
|
|
348
|
+
selection_rationale: rationale,
|
|
349
|
+
dashboard_ids: dashboardIds,
|
|
350
|
+
dashboard_refs: dashboardRows.map((row) => ({
|
|
351
|
+
dashboard_id: text(row.resource_key),
|
|
352
|
+
dashboard_name: text(row.title),
|
|
353
|
+
report_count: array(row.report_refs).length,
|
|
354
|
+
})),
|
|
355
|
+
source_container_refs: uniqueObjects(dashboardRows.map((row) => object(row.source_container_ref)), 'container_key'),
|
|
356
|
+
report_refs: uniqueObjects(dashboardRows.flatMap((row) => array(row.report_refs)), 'report_id'),
|
|
357
|
+
};
|
|
358
|
+
});
|
|
359
|
+
validateSqlSemanticPlan(plan);
|
|
360
|
+
const missing = dashboards.map((row) => text(row.resource_key)).filter((id) => !assigned.has(id));
|
|
361
|
+
if (missing.length) {
|
|
362
|
+
fail(`Business domain plan does not classify ${missing.length} dashboard(s): ${missing.slice(0, 20).join(', ')}${missing.length > 20 ? ', ...' : ''}.`);
|
|
363
|
+
}
|
|
364
|
+
return {
|
|
365
|
+
domains: enrichedDomains.sort((a, b) => a.domain_id.localeCompare(b.domain_id, 'en')),
|
|
366
|
+
appendices: enrichedAppendices.sort((a, b) => a.appendix_id.localeCompare(b.appendix_id, 'en')),
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
function assertAgentAuthoredDomain(domain, label) {
|
|
372
|
+
const values = [domain.title, domain.summary, domain.merge_rationale, ...array(domain.primary_questions)].map(text);
|
|
373
|
+
if (values.some(isMechanicalPlanText)) {
|
|
374
|
+
fail(`${label} looks mechanically generated. Agent must read asset definitions and author business semantics before rendering sources.`);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function validateSqlSemanticPlan(plan) {
|
|
379
|
+
const sqlReportsById = new Map(reports.filter((report) => text(report.definition_kind) === 'SQL').map((report) => [text(report.resource_key), report]));
|
|
380
|
+
for (const summaryValue of array(plan.sql_report_semantics)) {
|
|
381
|
+
const summary = object(summaryValue);
|
|
382
|
+
const reportId = text(summary.report_id);
|
|
383
|
+
if (!sqlReportsById.has(reportId)) continue;
|
|
384
|
+
if (text(summary.definition_state) !== 'valid') continue;
|
|
385
|
+
const issues = sqlSemanticQualityIssues(summary);
|
|
386
|
+
if (issues.length) {
|
|
387
|
+
fail(`SQL report ${reportId} semantic summary is not Agent-authored enough: ${issues.slice(0, 6).join('; ')}.`);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function sqlSemanticQualityIssues(summary) {
|
|
393
|
+
const issues = [];
|
|
394
|
+
const textFields = [summary.business_purpose, summary.statistical_grain, ...array(summary.applicable_questions), ...array(summary.non_applicable_questions)]
|
|
395
|
+
.map((item) => replaceSqlParameterMentions(renderPlainSemanticItem(item), summary)).join('\n');
|
|
396
|
+
if (isMechanicalPlanText(textFields)) issues.push('contains generic template language');
|
|
397
|
+
for (const [field, values] of Object.entries({
|
|
398
|
+
output_fields: summary.output_fields,
|
|
399
|
+
key_filters: summary.key_filters,
|
|
400
|
+
default_limits: summary.default_limits,
|
|
401
|
+
applicable_questions: summary.applicable_questions,
|
|
402
|
+
})) {
|
|
403
|
+
const rendered = array(values).map((item) => replaceSqlParameterMentions(renderPlainSemanticItem(item), summary)).join('\n');
|
|
404
|
+
if (/sql\s*输出字段|来源表达式|where 条件|order by|group by|\$\{|(?:^|[^a-z0-9_])(?:selector|partdate|variable)\d*(?:$|[^a-z0-9_])/i.test(rendered)) {
|
|
405
|
+
issues.push(`${field} exposes SQL/parser placeholders instead of business text`);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
if (!array(summary.output_fields).some((item) => {
|
|
409
|
+
const row = object(item);
|
|
410
|
+
return usefulSemanticText(row.name ?? row.field ?? row.key ?? row.column ?? row.title)
|
|
411
|
+
&& usefulSemanticText(row.meaning ?? row.description ?? row.remark ?? row.purpose);
|
|
412
|
+
})) {
|
|
413
|
+
issues.push('output fields need business names and meanings');
|
|
414
|
+
}
|
|
415
|
+
return issues;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function isMechanicalPlanText(value) {
|
|
419
|
+
const normalized = text(value).toLowerCase();
|
|
420
|
+
if (!normalized) return false;
|
|
421
|
+
return /根据报表标题|根据 sql 输出结构|根据 sql 查询|相关明细、汇总或排行|具体范围由 sql 参数决定|用于查看或统计|用于分析.*明细.*趋势|packaged sql report|exact business caliber remains unknown/.test(normalized);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function validatePackageReferenceIntegrity() {
|
|
425
|
+
const missingReports = [];
|
|
426
|
+
for (const dashboard of dashboards) {
|
|
427
|
+
for (const ref of array(dashboard.report_refs)) {
|
|
428
|
+
const reportId = text(object(ref).report_id);
|
|
429
|
+
if (reportId && !reportById.has(reportId)) {
|
|
430
|
+
missingReports.push(`${text(dashboard.resource_key)}->${reportId}`);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
const missingDashboards = [];
|
|
435
|
+
for (const report of reports) {
|
|
436
|
+
for (const ref of array(report.dashboard_refs)) {
|
|
437
|
+
const dashboardId = text(object(ref).dashboard_id);
|
|
438
|
+
if (dashboardId && !dashboardById.has(dashboardId)) {
|
|
439
|
+
missingDashboards.push(`${text(report.resource_key)}->${dashboardId}`);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
if (missingReports.length || missingDashboards.length) {
|
|
444
|
+
fail(`Asset package has dangling knowledge-index references: `
|
|
445
|
+
+ `${missingReports.length} dashboard-to-report and ${missingDashboards.length} report-to-dashboard. `
|
|
446
|
+
+ `Re-export from Common. Examples: ${[...missingReports, ...missingDashboards].slice(0, 12).join(', ')}.`);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function technicalContainerIdentityValues() {
|
|
451
|
+
return containers.filter((row) => text(row.container_kind) !== 'dashboard_space').flatMap((row) => [
|
|
452
|
+
text(row.container_key),
|
|
453
|
+
text(row.container_title).normalize('NFKC').toLocaleLowerCase('zh-CN'),
|
|
454
|
+
]).filter(Boolean);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function renderRootIndex() {
|
|
458
|
+
return `---\ntype: project-asset-raw-index\nproject_id: ${projectId}\nproject_name: ${yaml(projectName)}\nsnapshot_hash: ${yaml(snapshotHash)}\n---\n\n# ${heading(projectName)} 项目资产知识原始语料\n\n本目录由项目扫描资产包和同快照的 Agent 业务域规划生成。\`briefs/\` 用于先理解资产解决的问题;事实、精确口径和物理来源以对应源页面为准。\n\n## 浏览入口\n\n- [AI 辅助摘要](briefs/index.md)\n- [业务域资料](domains/index.md)\n- [技术附录](appendices/index.md)\n- [物理来源容器](source-containers/index.md)\n- [看板源资料](dashboards/index.md)\n- [报表源资料](reports/index.md)\n- [可复用指标源资料](metrics/index.md)\n- [分析元数据](metadata/index.md)\n- [覆盖与未归属资产](indexes/coverage.md)\n\n## 快照\n\n- Project ID: ${projectId}\n- 资产范围: ${text(corpus.asset_scope) || 'unknown'}\n- Snapshot hash: \`${snapshotHash}\`\n- Business domain plan hash: \`${domainPlanHash}\`\n- Truncated: ${corpus.truncated}\n\n${countTable()}\n\n${warningSection()}\n`;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function renderBriefIndex() {
|
|
462
|
+
return `---\ntype: ai-asset-brief-index\nsummary_kind: evidence_grounded_generated\nproject_id: ${projectId}\nproject_name: ${yaml(projectName)}\nsource_snapshot_hash: ${yaml(snapshotHash)}\n---\n\n# ${heading(projectName)} 资产理解摘要\n\n这一层把结构化资产定义转成“主要讲什么、解决什么问题、包含什么”的阅读入口。业务域由 Agent 跨物理空间综合归类;测试、演示和临时探索类看板可进入技术附录。每张看板必须且只能进入一个业务域或技术附录。摘要不替代原始定义。\n\n## 浏览入口\n\n- [业务域摘要](domains/index.md)\n- [看板摘要](dashboards/index.md)\n- [报表摘要](reports/index.md)\n- [指标摘要](metrics/index.md)\n- [元数据摘要](metadata/index.md)\n\n## 使用规则\n\n- 先读业务域和看板摘要判断应使用哪个资产。\n- 回答指标、过滤条件、时间范围或 SQL 口径时,必须回到页面中的“原始定义”链接核对。\n- 业务域是知识组织层;\`source-containers/\` 才是看板空间、共享资产和未归属等物理来源。\n- \`confidence\` 表示摘要与结构化证据的一致程度,不表示业务认证状态。\n- \`semantic_conflict\` 表示多个资产表达同一业务语义但口径、字段或过滤不同,Agent 不能仅凭名称选择,需要业务域、召回卡或用户确认。\n\n${countTable()}\n\n${warningSection()}\n`;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function renderDomainSource(domain) {
|
|
466
|
+
const key = text(domain.domain_id);
|
|
467
|
+
return `---\ntype: project-business-domain-source\nproject_id: ${projectId}\ndomain_id: ${yaml(key)}\ntitle: ${yaml(domain.title)}\nclassification_method: agent_semantic_synthesis\nsnapshot_hash: ${yaml(snapshotHash)}\n---\n\n# ${heading(domain.title)}\n\n${text(domain.summary)}\n\n## 主要业务问题\n\n${array(domain.primary_questions).map((question) => `- ${question}`).join('\n')}\n\n## 归类依据\n\n${text(domain.merge_rationale)}\n\n${renderDomainAgentUsageSummary(domain)}\n## 看板(${array(domain.dashboard_refs).length})\n\n${refLinks(array(domain.dashboard_refs), 'dashboard_id', 'dashboard_name', dashboardFiles, '../dashboards')}\n\n## 涉及报表(${array(domain.report_refs).length})\n\n${refLinks(array(domain.report_refs), 'report_id', 'report_name', reportFiles, '../reports')}\n\n## 物理来源容器\n\n${containerLinks(domain.source_container_refs, '../source-containers')}\n\n## 规划依据摘要\n\n- Domain ID: \`${key}\`\n- 归类方法: agent_semantic_synthesis\n- 快照: \`${snapshotHash}\`\n- 纳入看板: ${array(domain.dashboard_refs).length}\n- 涉及报表: ${array(domain.report_refs).length}\n- 物理来源容器: ${array(domain.source_container_refs).length}\n`;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function renderDomainAgentUsageSummary(domain) {
|
|
471
|
+
const reportsInDomain = array(domain.report_refs)
|
|
472
|
+
.map((ref) => reportById.get(text(object(ref).report_id)))
|
|
473
|
+
.filter(Boolean);
|
|
474
|
+
const dashboardLines = array(domain.dashboard_refs).slice(0, 5)
|
|
475
|
+
.map((ref) => `- [${link(text(ref.dashboard_name) || text(ref.dashboard_id))}](../dashboards/${dashboardFiles.get(text(ref.dashboard_id))})`);
|
|
476
|
+
const reportLines = pickRepresentativeReports(reportsInDomain)
|
|
477
|
+
.map((report) => `- [${link(text(report.title) || text(report.resource_key))}](../reports/${reportFiles.get(text(report.resource_key))})`);
|
|
478
|
+
const events = topDomainFacts(reportsInDomain.flatMap(domainReportEvents), 12);
|
|
479
|
+
const metrics = topDomainFacts(array(domain.primary_metrics).map(renderAgentAuthoredDomainFact), 12);
|
|
480
|
+
const dimensions = topDomainFacts(array(domain.drilldown_dimensions).map(renderAgentAuthoredDomainFact), 16);
|
|
481
|
+
const sqlCount = reportsInDomain.filter((report) => text(report.definition_kind) === 'SQL').length;
|
|
482
|
+
const unknownSqlCount = reportsInDomain.filter((report) => text(report.definition_kind) === 'SQL'
|
|
483
|
+
&& text(sqlReportSemanticsById.get(text(report.resource_key))?.definition_state) === 'unknown').length;
|
|
484
|
+
const conflictCount = reportsInDomain.filter((report) => reportConflicts.has(text(report.resource_key))).length;
|
|
485
|
+
const boundaryLines = [
|
|
486
|
+
'- 当前数量、客户名单、趋势结果需要实时执行对应看板或报表;知识库只提供路由、口径和使用边界。',
|
|
487
|
+
sqlCount ? `- 本业务域包含 ${sqlCount} 张 SQL 报表;SQL 报表优先使用报表页的 \`Agent 使用摘要\`,不要重新从长 SQL 临场推导。` : '',
|
|
488
|
+
unknownSqlCount ? `- ${unknownSqlCount} 张 SQL 报表语义状态为 unknown,只能作为待核验线索,不能直接回答确定口径。` : '',
|
|
489
|
+
conflictCount ? `- ${conflictCount} 张报表存在语义冲突,回答前必须回到报表页核对冲突说明。` : '',
|
|
490
|
+
].filter(Boolean);
|
|
491
|
+
return `## Agent 使用摘要\n\n### 主要分析目标\n\n${array(domain.primary_questions).map((question) => `- ${question}`).join('\n')}\n\n### 首选入口\n\n看板:\n${dashboardLines.join('\n') || '- 无'}\n\n主要报表:\n${reportLines.join('\n') || '- 无'}\n\n### 主要事件\n\n${events.map((item) => `- ${item}`).join('\n') || '- 无'}\n\n### 主要指标\n\n${metrics.map((item) => `- ${item}`).join('\n') || '- 无'}\n\n### 常用下钻维度\n\n${dimensions.map((item) => `- ${item}`).join('\n') || '- 无'}\n\n### 使用边界\n\n${boundaryLines.join('\n')}\n\n`;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function normalizeAgentAuthoredDomainFacts(values) {
|
|
495
|
+
return array(values).map((value) => {
|
|
496
|
+
if (typeof value === 'string') return cleanSemanticFactForDisplay(value);
|
|
497
|
+
const item = object(value);
|
|
498
|
+
const name = text(item.name ?? item.title ?? item.metric ?? item.dimension ?? item.field);
|
|
499
|
+
const key = text(item.key ?? item.resource_key ?? item.field_key ?? item.event_key);
|
|
500
|
+
const meaning = text(item.meaning ?? item.description ?? item.purpose ?? item.usage);
|
|
501
|
+
if (!name && !key && !meaning) return '';
|
|
502
|
+
return {
|
|
503
|
+
name,
|
|
504
|
+
key,
|
|
505
|
+
meaning,
|
|
506
|
+
source_report_ids: array(item.source_report_ids ?? item.report_ids).map(text).filter(Boolean),
|
|
507
|
+
source_dashboard_ids: array(item.source_dashboard_ids ?? item.dashboard_ids).map(text).filter(Boolean),
|
|
508
|
+
};
|
|
509
|
+
}).filter((value) => {
|
|
510
|
+
if (typeof value === 'string') return Boolean(value);
|
|
511
|
+
return Boolean(value.name || value.key || value.meaning);
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function renderAgentAuthoredDomainFact(value) {
|
|
516
|
+
if (typeof value === 'string') return value;
|
|
517
|
+
const item = object(value);
|
|
518
|
+
const label = text(item.name || item.key || item.meaning);
|
|
519
|
+
const key = text(item.key);
|
|
520
|
+
const meaning = text(item.meaning);
|
|
521
|
+
const evidence = [
|
|
522
|
+
...array(item.source_report_ids).map((id) => reportById.has(text(id)) ? `报表 ${text(id)}` : ''),
|
|
523
|
+
...array(item.source_dashboard_ids).map((id) => dashboardById.has(text(id)) ? `看板 ${text(id)}` : ''),
|
|
524
|
+
].filter(Boolean).slice(0, 3);
|
|
525
|
+
return [
|
|
526
|
+
key && label !== key ? `${label}(\`${key}\`)` : label,
|
|
527
|
+
meaning,
|
|
528
|
+
evidence.length ? `证据:${evidence.join('、')}` : '',
|
|
529
|
+
].filter(Boolean).join(':');
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
function pickRepresentativeReports(reportsInDomain) {
|
|
533
|
+
return sort([...reportsInDomain], 'resource_key')
|
|
534
|
+
.sort((left, right) => reportRepresentativeScore(right) - reportRepresentativeScore(left))
|
|
535
|
+
.slice(0, 8);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
function reportRepresentativeScore(report) {
|
|
539
|
+
const sqlSummary = sqlReportSemanticsById.get(text(report.resource_key));
|
|
540
|
+
return (report.authenticated === true ? 100 : 0)
|
|
541
|
+
+ (text(report.definition_kind) === 'SQL' ? 20 : 0)
|
|
542
|
+
+ (array(report.metric_occurrences).length ? 10 : 0)
|
|
543
|
+
+ (text(sqlSummary?.definition_state) === 'valid' ? 6 : 0)
|
|
544
|
+
- (text(sqlSummary?.definition_state) === 'unknown' ? 12 : 0)
|
|
545
|
+
- (reportConflicts.has(text(report.resource_key)) ? 20 : 0);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
function domainReportEvents(report) {
|
|
549
|
+
const refs = metadataRefsForReport(report)
|
|
550
|
+
.filter((item) => text(item.resource_type) === 'event')
|
|
551
|
+
.map((item) => namedCodeFact(item.title, item.resource_key))
|
|
552
|
+
.filter(isUsefulDomainEventFact);
|
|
553
|
+
const occurrenceEvents = array(report.metric_occurrences)
|
|
554
|
+
.map((item) => namedCodeFact(item.event_title, item.event_name))
|
|
555
|
+
.filter(isUsefulDomainEventFact);
|
|
556
|
+
return [...refs, ...occurrenceEvents];
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
function domainReportMetrics(report) {
|
|
560
|
+
const occurrenceMetrics = array(report.metric_occurrences).map((item) => {
|
|
561
|
+
const label = occurrenceLabel(item);
|
|
562
|
+
const event = metricEventLabel(item);
|
|
563
|
+
const aggregation = aggregationLabel(item);
|
|
564
|
+
const measure = readableMetricText(item.measure_title || item.measure_key);
|
|
565
|
+
const detail = [event && `基于“${event}”`, aggregation && `按“${aggregation}”`, measure && `度量“${measure}”`]
|
|
566
|
+
.filter(Boolean).join(',');
|
|
567
|
+
return label ? `${label}${detail ? `:${detail}` : ''}` : '';
|
|
568
|
+
});
|
|
569
|
+
return occurrenceMetrics.filter(isUsefulDomainMetricFact);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
function domainReportDimensions(report) {
|
|
573
|
+
const groups = array(object(report.calculation_context).group_by)
|
|
574
|
+
.map(groupLabel)
|
|
575
|
+
.filter(isUsefulDomainDimensionFact);
|
|
576
|
+
const metadataDimensions = metadataRefsForReport(report)
|
|
577
|
+
.filter((item) => ['event_prop', 'user_prop', 'tag', 'user_tag', 'cluster', 'user_cluster'].includes(text(item.resource_type)))
|
|
578
|
+
.map((item) => namedCodeFact(item.title, item.resource_key))
|
|
579
|
+
.filter(isUsefulDomainDimensionFact);
|
|
580
|
+
return [...groups, ...metadataDimensions];
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
function sqlSummaryForReport(report) {
|
|
584
|
+
const summary = completeSqlAgentSummary(report, sqlReportSemanticsById.get(text(report.resource_key)),
|
|
585
|
+
calculationContextForKb(report).sql_semantic_facts);
|
|
586
|
+
return summary ? [summary] : [];
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
function renderDomainSemanticOutputField(value) {
|
|
590
|
+
const item = object(value);
|
|
591
|
+
const name = text(item.name ?? item.field ?? item.key ?? item.column ?? item.title);
|
|
592
|
+
const meaning = text(item.meaning ?? item.description ?? item.remark ?? item.purpose);
|
|
593
|
+
if (!name && !meaning) return '';
|
|
594
|
+
return name && meaning ? `${name}:${meaning}` : name || meaning;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
function renderDomainDimensionName(value) {
|
|
598
|
+
const item = object(value);
|
|
599
|
+
const name = text(item.name ?? item.field ?? item.key ?? item.column ?? item.title);
|
|
600
|
+
const meaning = shortenDomainDimensionMeaning(text(item.meaning ?? item.description ?? item.remark ?? item.purpose));
|
|
601
|
+
return name && meaning ? `${name}:${meaning}` : name || meaning;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
function shortenDomainDimensionMeaning(value) {
|
|
605
|
+
return text(value)
|
|
606
|
+
.replace(/。.*$/s, '')
|
|
607
|
+
.replace(/;.*$/s, '')
|
|
608
|
+
.replace(/,?选项[::].*$/s, '')
|
|
609
|
+
.replace(/保存包未提供实际选中值或默认值.*$/s, '')
|
|
610
|
+
.trim();
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
function isLikelyMetricText(value) {
|
|
614
|
+
const normalized = text(value).toLowerCase();
|
|
615
|
+
return /(指标|数量|人数|次数|用户数|客户数|公司数|金额|收入|数据量|增量|累计|上限|已用|日均|预估|天数|耗时|时长|率|占比|令牌|调用数|调用次数|长度|_ct\b|_count\b)/i.test(normalized)
|
|
616
|
+
|| /(?:^|[^a-z0-9_])(count|cnt|num|amount|sum|avg|rate|ratio|percent|duration|tokens?)(?:$|[^a-z0-9_])/i.test(normalized)
|
|
617
|
+
|| /(?:^|_)(count|cnt|num|amount|sum|avg|rate|ratio|percent|duration|tokens?)(?:$|_|\d)/i.test(normalized);
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
function isUsefulDomainMetricFact(value) {
|
|
621
|
+
const normalized = text(value).toLowerCase();
|
|
622
|
+
if (!normalized || isExcludedDrilldownDimensionText(normalized)) return false;
|
|
623
|
+
if (/chat_content|content_len|input_tokens|output_tokens|cache_.*tokens|tool_call_count|skill_call_count|mcp_call_count|process_steps|agent_reply_duration/.test(normalized)) return false;
|
|
624
|
+
return isLikelyMetricText(normalized);
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
function isLikelyDrilldownDimensionText(value) {
|
|
628
|
+
const name = cleanSemanticFactKey(text(value)).split(/[::]/)[0].trim().toLowerCase();
|
|
629
|
+
return /^(date|day|week|month|quarter|year|timewindow|company|company_name|customer|customer_name|project|project_id|project_name|user_name|login_name|role_type|ta_serial_no|csm|agent_id|session_id|final_state|is_dev|send_channel)$/.test(name);
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
function isUsefulDomainEventFact(value) {
|
|
633
|
+
const normalized = text(value).toLowerCase();
|
|
634
|
+
if (!normalized || isLikelyMetricText(normalized)) return false;
|
|
635
|
+
if (/自定义指标|渗透率|留存|排行|趋势|报表$|看板$/.test(normalized)) return false;
|
|
636
|
+
return /(`[^`]+`)|`[^`]+`|event|_create|_search|_send|_receive|_like|_dislike|_action|_click|_query|_save|_edit|pageview|访问|查询|创建|发送|接收|点赞|点踩|操作|更新|浏览/.test(normalized);
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
function isUsefulDomainDimensionFact(value) {
|
|
640
|
+
const normalized = text(value).toLowerCase();
|
|
641
|
+
if (!normalized || isLikelyMetricText(normalized) || isExcludedDrilldownDimensionText(normalized)) return false;
|
|
642
|
+
return isLikelyDrilldownDimensionText(normalized)
|
|
643
|
+
|| /(公司|客户|企业|成员|用户|账号|角色|会话|项目|集群|版本|阶段|状态|类型|渠道|来源|事件时间|时间|日期|国家|地区|城市|标签|分群|负责人|空间|页面|环境|测试|正式|company|customer|user|login|role|session|agent|project|cluster|version|status|state|type|channel|source|date|time|country|city|region|tag|segment|owner|space|folder|page|url|env)/i.test(normalized);
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
function isExcludedDrilldownDimensionText(value) {
|
|
647
|
+
const normalized = text(value).toLowerCase();
|
|
648
|
+
const name = normalized.split(/[::]/)[0].trim();
|
|
649
|
+
return /^number\d*$/.test(name)
|
|
650
|
+
|| /^x$/.test(name)
|
|
651
|
+
|| /\$\{|selector|partdate|variable\d*|sql\s*输出字段|来源表达式|source expression|where 条件|order by|group by|limit|排序方法|筛选数据量|返回数量|row count|chat_content|content_len|input_tokens|output_tokens|cache_.*tokens|mcp_list|skill_list|process_steps|response_duration|agent_reply_duration|call_count|tool_call_count|skill_call_count|mcp_call_count/.test(normalized);
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
function namedCodeFact(titleValue, keyValue) {
|
|
655
|
+
const key = text(keyValue);
|
|
656
|
+
const title = cleanDimensionTitle(titleValue, key);
|
|
657
|
+
const fallbackTitle = dimensionKeyLabel(key);
|
|
658
|
+
if (title && key && title !== key) return `${title}(\`${key}\`)`;
|
|
659
|
+
if (key && fallbackTitle && (!title || title === key)) return `${fallbackTitle}(\`${key}\`)`;
|
|
660
|
+
return title || (key ? `\`${key}\`` : '');
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
function cleanDimensionTitle(titleValue, keyValue = '') {
|
|
664
|
+
const key = text(keyValue);
|
|
665
|
+
const fallbackTitle = dimensionKeyLabel(key);
|
|
666
|
+
const cleaned = text(titleValue)
|
|
667
|
+
.replace(/([^)]*(?:处理\d*个saas|区分saas集群用)[^)]*)/gi, '')
|
|
668
|
+
.replace(/\([^)]*(?:处理\d*个saas|区分saas集群用)[^)]*\)/gi, '')
|
|
669
|
+
.trim();
|
|
670
|
+
if (!cleaned || cleaned === key) return fallbackTitle || cleaned;
|
|
671
|
+
if (/^level_for_csm_serial_company$/i.test(cleaned)) return '公司级别';
|
|
672
|
+
if (/^initial_date_serial_company$/i.test(cleaned)) return '首次接入日期';
|
|
673
|
+
if (/^es_serial_no_event_date$/i.test(cleaned)) return 'ES客户集群序列号';
|
|
674
|
+
return cleaned;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
function dimensionKeyLabel(value) {
|
|
678
|
+
const key = text(value);
|
|
679
|
+
const normalized = key.replace(/^#vp@serial_company@/i, '');
|
|
680
|
+
return ({
|
|
681
|
+
send_channel: '发送渠道',
|
|
682
|
+
final_state: '最终状态',
|
|
683
|
+
type: '消息方向',
|
|
684
|
+
login_name: '登录账号名',
|
|
685
|
+
user_name: '成员显示名',
|
|
686
|
+
session_id: '会话 ID',
|
|
687
|
+
agent_id: 'Agent ID',
|
|
688
|
+
company_name: '公司名称',
|
|
689
|
+
project_id: '项目 ID',
|
|
690
|
+
project_name: '项目名称',
|
|
691
|
+
date: '日期',
|
|
692
|
+
day: '日期',
|
|
693
|
+
level_for_csm_serial_company: '公司级别',
|
|
694
|
+
level_for_csm: '公司级别',
|
|
695
|
+
initial_date_serial_company: '首次接入日期',
|
|
696
|
+
initial_date: '首次接入日期',
|
|
697
|
+
serial_no_saas: 'ES客户集群序列号',
|
|
698
|
+
ta_version: 'TA版本号',
|
|
699
|
+
is_dev: '是否为测试服',
|
|
700
|
+
})[normalized] || '';
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
function topDomainFacts(values, limit) {
|
|
704
|
+
const entries = [];
|
|
705
|
+
const byCode = new Map();
|
|
706
|
+
const byLabel = new Map();
|
|
707
|
+
for (const value of values.map(text).filter(Boolean)) {
|
|
708
|
+
const cleaned = cleanSemanticFactForDisplay(value);
|
|
709
|
+
if (!cleaned) continue;
|
|
710
|
+
const identity = semanticFactIdentity(cleaned);
|
|
711
|
+
if (!identity.code && !identity.label) continue;
|
|
712
|
+
|
|
713
|
+
const candidates = [
|
|
714
|
+
identity.code ? byCode.get(identity.code) : null,
|
|
715
|
+
identity.label ? byLabel.get(identity.label) : null,
|
|
716
|
+
].filter((item) => item != null);
|
|
717
|
+
const existingIndex = candidates.length ? candidates[0] : -1;
|
|
718
|
+
|
|
719
|
+
if (existingIndex < 0) {
|
|
720
|
+
const nextIndex = entries.length;
|
|
721
|
+
entries.push({ value: cleaned, identity });
|
|
722
|
+
if (identity.code) byCode.set(identity.code, nextIndex);
|
|
723
|
+
if (identity.label) byLabel.set(identity.label, nextIndex);
|
|
724
|
+
continue;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
const existing = entries[existingIndex];
|
|
728
|
+
const replacement = factSpecificity(cleaned) > factSpecificity(existing.value)
|
|
729
|
+
? { value: cleaned, identity }
|
|
730
|
+
: existing;
|
|
731
|
+
entries[existingIndex] = replacement;
|
|
732
|
+
if (identity.code) byCode.set(identity.code, existingIndex);
|
|
733
|
+
if (identity.label) byLabel.set(identity.label, existingIndex);
|
|
734
|
+
if (replacement.identity.code) byCode.set(replacement.identity.code, existingIndex);
|
|
735
|
+
if (replacement.identity.label) byLabel.set(replacement.identity.label, existingIndex);
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
const result = [];
|
|
739
|
+
const seenCodes = new Set();
|
|
740
|
+
const seenLabels = new Set();
|
|
741
|
+
for (const entry of entries) {
|
|
742
|
+
if (!entry) continue;
|
|
743
|
+
const { code, label } = entry.identity;
|
|
744
|
+
if ((code && seenCodes.has(code)) || (label && seenLabels.has(label))) continue;
|
|
745
|
+
result.push(entry.value);
|
|
746
|
+
if (code) seenCodes.add(code);
|
|
747
|
+
if (label) seenLabels.add(label);
|
|
748
|
+
if (result.length >= limit) break;
|
|
749
|
+
}
|
|
750
|
+
return result;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
function semanticFactIdentity(value) {
|
|
754
|
+
const normalized = cleanSemanticFactForDisplay(value).normalize('NFKC').toLocaleLowerCase('zh-CN');
|
|
755
|
+
const code = normalized.match(/`([^`]+)`/)?.[1]?.trim() || '';
|
|
756
|
+
const label = normalized
|
|
757
|
+
.replace(/(`[^`]+`)/g, '')
|
|
758
|
+
.replace(/\s*\(`[^`]+`\)/g, '')
|
|
759
|
+
.split(/[::]/)[0]
|
|
760
|
+
.replace(/[\s`]+/g, '');
|
|
761
|
+
return {
|
|
762
|
+
code,
|
|
763
|
+
label: normalizeSemanticFactLabel(label),
|
|
764
|
+
};
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
function normalizeSemanticFactLabel(value) {
|
|
768
|
+
const normalized = text(value)
|
|
769
|
+
.replace(/([^)]*(?:处理\d*个saas|区分saas集群用)[^)]*)/gi, '')
|
|
770
|
+
.replace(/\([^)]*(?:处理\d*个saas|区分saas集群用)[^)]*\)/gi, '')
|
|
771
|
+
.trim();
|
|
772
|
+
if (/^level_for_csm_serial_company$/i.test(normalized)) return '公司级别';
|
|
773
|
+
if (/^initial_date_serial_company$/i.test(normalized)) return '首次接入日期';
|
|
774
|
+
if (/^es_serial_no_event_date$/i.test(normalized)) return 'ES客户集群序列号';
|
|
775
|
+
return normalized;
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
function factSpecificity(value) {
|
|
779
|
+
const identity = semanticFactIdentity(value);
|
|
780
|
+
const rawCodeLabel = identity.code && (identity.label === identity.code || !identity.label);
|
|
781
|
+
return (/`[^`]+`/.test(value) ? 2 : 0)
|
|
782
|
+
+ (/[::]/.test(value) ? 1 : 0)
|
|
783
|
+
+ (identity.code && !rawCodeLabel ? 2 : 0);
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
function cleanSemanticFactForDisplay(value) {
|
|
787
|
+
return text(value)
|
|
788
|
+
.replace(/\$\{(?:(?:Text|Selector|PartDate|Number|Date|DateTime|MultiSelector)\s*:)?\s*([A-Za-z_][\w.-]*)\s*\}/g, '$1')
|
|
789
|
+
.replace(/([^)]*(?:处理\d*个saas|区分saas集群用)[^)]*)/gi, '')
|
|
790
|
+
.replace(/\([^)]*(?:处理\d*个saas|区分saas集群用)[^)]*\)/gi, '')
|
|
791
|
+
.replace(/\s+/g, ' ')
|
|
792
|
+
.trim();
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
function cleanSemanticFactKey(value) {
|
|
796
|
+
return cleanSemanticFactForDisplay(value).replace(/`/g, '');
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
function renderAppendixSource(appendix) {
|
|
800
|
+
const key = text(appendix.appendix_id);
|
|
801
|
+
return `---\ntype: project-technical-appendix-source\nproject_id: ${projectId}\nappendix_id: ${yaml(key)}\ntitle: ${yaml(appendix.title)}\nclassification_method: agent_semantic_synthesis\nsnapshot_hash: ${yaml(snapshotHash)}\n---\n\n# ${heading(appendix.title)}\n\n${text(appendix.summary)}\n\n本附录中的资产保留用于追溯,但不进入主要业务域检索入口。\n\n## 收录依据\n\n${text(appendix.selection_rationale)}\n\n## 看板(${array(appendix.dashboard_refs).length})\n\n${refLinks(array(appendix.dashboard_refs), 'dashboard_id', 'dashboard_name', dashboardFiles, '../dashboards')}\n\n## 涉及报表(${array(appendix.report_refs).length})\n\n${refLinks(array(appendix.report_refs), 'report_id', 'report_name', reportFiles, '../reports')}\n\n## 物理来源容器\n\n${containerLinks(appendix.source_container_refs, '../source-containers')}\n`;
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
function renderContainerSource(container) {
|
|
805
|
+
const key = text(container.container_key);
|
|
806
|
+
return `---\ntype: project-asset-source-container\nproject_id: ${projectId}\ncontainer_key: ${yaml(key)}\ncontainer_kind: ${yaml(container.container_kind)}\nspace_id: ${container.space_id == null ? 'null' : yaml(container.space_id)}\nsnapshot_hash: ${yaml(snapshotHash)}\n---\n\n# ${heading(container.container_title || key)}\n\n这是资产包记录的物理/技术来源容器,不是业务域。相同名称可以对应不同空间,混合空间也可以被拆分到多个业务域。\n\n## Agent 使用方式\n\n- 用它追溯资产的物理归属、看板空间或共享资产来源。\n- 不要把来源容器名称当作业务域或业务口径。\n- 回答业务问题时,应从业务域、场景召回卡、看板或报表进入。\n\n## 看板(${array(container.dashboard_refs).length})\n\n${refLinks(array(container.dashboard_refs), 'dashboard_id', 'dashboard_name', dashboardFiles, '../dashboards')}\n\n## 独立报表(${array(container.standalone_report_refs).length})\n\n${refLinks(array(container.standalone_report_refs), 'report_id', 'report_name', reportFiles, '../reports')}\n\n## 证据定位\n\n- container_key: \`${key || 'unknown'}\`\n- container_kind: ${text(container.container_kind) || 'unknown'}\n- space_id: ${container.space_id == null ? 'unknown' : `\`${text(container.space_id)}\``}\n`;
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
function renderDashboardSource(dashboard) {
|
|
810
|
+
const id = text(dashboard.resource_key);
|
|
811
|
+
const domain = domainByDashboardId.get(id);
|
|
812
|
+
const appendix = appendixByDashboardId.get(id);
|
|
813
|
+
const container = object(dashboard.source_container_ref);
|
|
814
|
+
const classification = domain
|
|
815
|
+
? `- 业务域: [${link(domain.title)}](../domains/${domainFiles.get(domain.domain_id)})`
|
|
816
|
+
: `- 技术附录: [${link(appendix.title)}](../appendices/${appendixFiles.get(appendix.appendix_id)})`;
|
|
817
|
+
const notes = array(dashboard.notes);
|
|
818
|
+
const reportRows = array(dashboard.report_refs).map((ref) => reportById.get(text(ref.report_id))).filter(Boolean);
|
|
819
|
+
return `${assetFrontmatter('project-dashboard', dashboard)}\n# ${heading(dashboard.title || `Dashboard ${id}`)}\n\n## Agent 使用摘要\n\n${renderDashboardAgentUsageSummary(dashboard, reportRows, notes, domain, appendix)}\n\n## 身份与归属\n\n- 看板 ID: \`${id}\`\n${classification}\n- 物理来源: [${link(container.container_title || container.container_key)}](../source-containers/${containerFiles.get(text(container.container_key))})\n- 已认证: ${Boolean(dashboard.authenticated)}\n- 更新时间: ${text(dashboard.updated_at) || 'unknown'}\n- 源详情: \`${text(dashboard.source_detail_path) || 'unavailable'}\`\n\n## 描述\n\n${text(dashboard.description || dashboard.summary) || '_资产包中没有描述。_'}\n\n## 看板便签(${notes.length})\n\n${renderDashboardNotes(notes)}\n\n## 报表(${array(dashboard.report_refs).length})\n\n${refLinks(array(dashboard.report_refs), 'report_id', 'report_name', reportFiles, '../reports')}\n\n## 证据定位\n\n- 资产包源定位: \`${text(dashboard.source_detail_path) || 'unavailable'}\`\n`;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
function renderReportSource(report) {
|
|
823
|
+
const id = text(report.resource_key);
|
|
824
|
+
const reportDomains = domainsForReport(report);
|
|
825
|
+
const reportAppendices = appendicesForReport(report);
|
|
826
|
+
const metadataRefs = metadataRefsForReport(report);
|
|
827
|
+
const calculationContext = calculationContextForKb(report);
|
|
828
|
+
const sqlSummary = completeSqlAgentSummary(report, sqlReportSemanticsById.get(id), calculationContext.sql_semantic_facts);
|
|
829
|
+
return `${assetFrontmatter('project-report', report)}\n# ${heading(report.title || `Report ${id}`)}\n\n## Agent 使用摘要\n\n${renderReportAgentUsageSummary(report, metadataRefs, calculationContext, sqlSummary)}\n\n## 身份与归属\n\n- 报表 ID: \`${id}\`\n- 报表模型: ${text(report.report_model) || 'unknown'}\n- 定义类型: ${text(report.definition_kind) || 'UNKNOWN'}\n- 独立报表: ${Boolean(report.standalone)}\n- 已认证: ${Boolean(report.authenticated)}\n- 源详情: \`${text(report.source_detail_path) || 'unavailable'}\`\n\n业务域(由承载看板派生):\n${domainLinks(reportDomains, '../domains')}\n\n技术附录(由承载看板派生):\n${appendixLinks(reportAppendices, '../appendices')}\n\n物理来源容器:\n${containerLinks(report.source_container_refs, '../source-containers')}\n\n看板:\n${refLinks(array(report.dashboard_refs), 'dashboard_id', 'dashboard_name', dashboardFiles, '../dashboards')}\n\n## 描述\n\n${text(report.description || report.summary) || '_资产包中没有描述。_'}\n\n## 指标与计算口径(${array(report.metric_occurrences).length})\n\n${renderReportMetricOccurrencesSection(report)}\n\n## 引用元数据(${metadataRefs.length})\n\n${metadataLinks(metadataRefs, '../metadata')}\n\n## 证据定位\n\n- 资产包源定位: \`${text(report.source_detail_path) || 'unavailable'}\`\n${text(object(calculationContext.sql_semantic_facts).evidence_hash) ? `- SQL 证据 SHA-256: \`${text(object(calculationContext.sql_semantic_facts).evidence_hash)}\`\n` : ''}`;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
|
|
833
|
+
|
|
834
|
+
function renderDashboardAgentUsageSummary(dashboard, reportRows, notes, domain, appendix) {
|
|
835
|
+
const classification = domain
|
|
836
|
+
? `属于“${text(domain.title)}”业务域。`
|
|
837
|
+
: `收录在“${text(appendix?.title)}”技术附录,不作为主要业务域入口。`;
|
|
838
|
+
const purposes = reportRows.slice(0, 8).map((row) => reportPurpose(row));
|
|
839
|
+
const metrics = topDomainFacts(reportRows.flatMap((row) => reportMetricSummaryItems(row)), 10);
|
|
840
|
+
const dimensions = topDomainFacts(reportRows.flatMap((row) => reportDimensionSummaryItems(row)), 10);
|
|
841
|
+
return `- 用途:${classification}${reportRows.length ? `这张看板承载 ${reportRows.length} 张报表,用于从看板层进入相关分析。` : '当前没有关联报表。'}\n- 适合回答:\n${purposes.map((item) => ` - ${item}`).join('\n') || ' - 当前看板没有足够报表证据概括适用问题。'}\n- 主要指标:${metrics.join('、') || '未从关联报表识别到稳定指标。'}\n- 常用下钻维度:${dimensions.join('、') || '未从关联报表识别到稳定下钻维度。'}\n- 便签规则:${notes.length ? '看板便签可用于理解业务目的、注意事项和共享边界;精确口径仍以下属报表为准。' : '没有看板便签。'}\n- 使用边界:当前数值、客户名单和趋势结果需要实时执行看板或下属报表;知识库只提供路由、口径和边界。`;
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
function renderReportAgentUsageSummary(report, metadataRefs, calculationContext, sqlSummary) {
|
|
845
|
+
const isSql = text(report.definition_kind) === 'SQL';
|
|
846
|
+
if (isSql) return renderSqlReportAgentUsageSummary(report, sqlSummary);
|
|
847
|
+
const purpose = usefulSemanticText(sqlSummary?.business_purpose) || reportPurpose(report);
|
|
848
|
+
const questions = usefulSemanticList(sqlSummary?.applicable_questions)
|
|
849
|
+
? array(sqlSummary.applicable_questions).map(renderPlainSemanticItem)
|
|
850
|
+
: [reportPurpose(report)];
|
|
851
|
+
const metrics = array(report.metric_occurrences).map(occurrenceSentence);
|
|
852
|
+
const parameters = reportParameterSummaryItems(calculationContext);
|
|
853
|
+
const dimensions = reportDimensionSummaryItems(report, sqlSummary, calculationContext, metadataRefs);
|
|
854
|
+
const filters = reportFilterSummaryItems(report, calculationContext);
|
|
855
|
+
const limits = [];
|
|
856
|
+
const boundaries = usefulSemanticList(sqlSummary?.non_applicable_questions)
|
|
857
|
+
? array(sqlSummary.non_applicable_questions).map(renderPlainSemanticItem)
|
|
858
|
+
: ['不能替代实时执行结果;当前知识库只说明报表语义和使用边界。'];
|
|
859
|
+
return `- 用途:${purpose}\n- 适合回答:\n${questions.map((item) => ` - ${item}`).join('\n') || ' - 暂无明确适用问题。'}\n- 主要指标/输出:\n${metrics.map((item) => ` - ${item}`).join('\n') || ' - 未识别到稳定指标或输出字段。'}\n- 输入参数:\n${parameters.map((item) => ` - ${item}`).join('\n') || ' - 未发现需要 Agent 解释的输入参数。'}\n- 常用下钻维度:${dimensions.join('、') || '未识别到稳定下钻维度。'}\n- 关键过滤与限制:\n${[...filters, ...limits].map((item) => ` - ${item}`).join('\n') || ' - 未发现明确过滤、排序或行数限制。'}\n- 使用边界:\n${boundaries.map((item) => ` - ${item}`).join('\n')}`;
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
function renderReportMetricOccurrencesSection(report) {
|
|
863
|
+
const occurrences = array(report.metric_occurrences);
|
|
864
|
+
if (occurrences.length) return occurrences.map(renderOccurrence).join('\n\n');
|
|
865
|
+
if (text(report.definition_kind) === 'SQL') return '_SQL 报表口径以本页的 Agent 使用摘要为准。_';
|
|
866
|
+
return '_未识别到结构化指标项。_';
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
function renderSqlReportAgentUsageSummary(report, summary) {
|
|
870
|
+
if (!summary) {
|
|
871
|
+
return '_当前 SQL 报表没有可用的 Agent 语义摘要。该报表只能作为待核验线索,不能直接回答确定口径。_';
|
|
872
|
+
}
|
|
873
|
+
const purpose = usefulSemanticText(summary.business_purpose) || reportPurpose(report);
|
|
874
|
+
return `该摘要由 CLI Agent 基于同一资产快照生成,作为 SQL 报表的源文件级业务语义输入;KB 可以直接消费这段摘要并结合项目其他资料组织 Wiki,不需要重新从长 SQL 中完整推导业务含义。\n\n- 摘要来源: ${text(summary.summary_source) || 'semantic_plan'}\n- 定义状态: ${text(summary.definition_state) || 'unknown'}\n- 用途:${purpose}\n- 统计粒度: ${text(summary.statistical_grain) || 'unknown'}\n- 证据定位: \`${text(summary.evidence_locator) || 'unknown'}\`\n\n### 输入参数\n\n${renderSemanticList(summary.input_parameters, renderSemanticItem)}\n\n### 输出字段\n\n${renderSemanticList(summary.output_fields, (item) => renderSqlSemanticItem(item, summary))}\n\n### 关键过滤与排除\n\n${renderSemanticList(summary.key_filters, (item) => renderSqlSemanticItem(item, summary))}\n\n### 默认限制与排序\n\n${renderSemanticList(summary.default_limits, (item) => renderSqlSemanticItem(item, summary))}\n\n### 可回答的问题\n\n${renderSemanticList(summary.applicable_questions, (item) => renderSqlSemanticItem(item, summary))}\n\n### 不可安全回答的问题\n\n${renderSemanticList(summary.non_applicable_questions, (item) => renderSqlSemanticItem(item, summary))}`;
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
function renderSqlSemanticItem(value, summary) {
|
|
878
|
+
return replaceSqlParameterMentions(renderSemanticItem(value), summary);
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
function replaceSqlParameterMentions(value, summary) {
|
|
882
|
+
let result = sanitizeVisibleSqlSemanticText(value);
|
|
883
|
+
for (const [name, meaning] of sqlParameterMeaningEntries(summary)) {
|
|
884
|
+
const pattern = new RegExp(`(?<![A-Za-z0-9_#$])${escapeRegExp(name)}(?![A-Za-z0-9_#$]|\\s*\\()`, 'giu');
|
|
885
|
+
result = result.replace(pattern, meaning);
|
|
886
|
+
}
|
|
887
|
+
return sanitizeVisibleSqlSemanticText(result)
|
|
888
|
+
.replace(/\bLIMIT\s+([^,;。]+?)限制/g, '按$1限制')
|
|
889
|
+
.replace(/\bLIMIT\s+/g, '返回行数上限:')
|
|
890
|
+
.replace(/\bLIMIT\b/gi, '返回行数上限');
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
function sqlParameterMeaningEntries(summary) {
|
|
894
|
+
return array(summary?.input_parameters)
|
|
895
|
+
.map((value) => {
|
|
896
|
+
const item = object(value);
|
|
897
|
+
const name = text(item.name ?? item.field ?? item.key ?? item.column ?? item.title);
|
|
898
|
+
const meaning = shortSqlParameterMeaning(item);
|
|
899
|
+
return name && meaning ? [name, meaning] : null;
|
|
900
|
+
})
|
|
901
|
+
.filter(Boolean)
|
|
902
|
+
.sort((left, right) => right[0].length - left[0].length);
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
function shortSqlParameterMeaning(item) {
|
|
906
|
+
const name = text(item.name ?? item.field ?? item.key ?? item.column ?? item.title);
|
|
907
|
+
const raw = text(item.meaning ?? item.description ?? item.remark ?? item.purpose);
|
|
908
|
+
const shortened = raw
|
|
909
|
+
.replace(/;选项[::].*$/s, '')
|
|
910
|
+
.replace(/,?选项[::].*$/s, '')
|
|
911
|
+
.replace(/。保存包未提供实际选中值或默认值。?$/s, '')
|
|
912
|
+
.replace(/保存包未提供实际选中值或默认值。?$/s, '')
|
|
913
|
+
.replace(/[;;].*$/s, '')
|
|
914
|
+
.trim();
|
|
915
|
+
return shortened || genericSqlParameterMeaning(name);
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
function genericSqlParameterMeaning(name) {
|
|
919
|
+
const normalized = text(name);
|
|
920
|
+
if (/^date\d*$/i.test(normalized)) return '日期范围';
|
|
921
|
+
if (/^selector\d*$/i.test(normalized)) return '选项参数';
|
|
922
|
+
if (/^variable\d*$/i.test(normalized)) return '输入值';
|
|
923
|
+
return normalized;
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
function escapeRegExp(value) {
|
|
927
|
+
return text(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
function usefulSemanticText(value) {
|
|
931
|
+
const normalized = text(value);
|
|
932
|
+
if (!normalized || ['none', 'unknown', 'null', 'undefined'].includes(normalized.toLowerCase())) return '';
|
|
933
|
+
return normalized;
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
function renderPlainSemanticItem(value) {
|
|
937
|
+
if (value == null) return '';
|
|
938
|
+
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') return text(value);
|
|
939
|
+
const item = object(value);
|
|
940
|
+
const name = text(item.name ?? item.field ?? item.key ?? item.column ?? item.title);
|
|
941
|
+
const meaning = text(item.meaning ?? item.description ?? item.remark ?? item.purpose);
|
|
942
|
+
const extras = [];
|
|
943
|
+
if (item.required != null) extras.push(`required=${Boolean(item.required)}`);
|
|
944
|
+
if (item.default_value != null && text(item.default_value)) extras.push(`default=${text(item.default_value)}`);
|
|
945
|
+
const expressionKind = text(item.expression_kind);
|
|
946
|
+
if (expressionKind) extras.push(expressionKind);
|
|
947
|
+
if (isInputParameterSemanticItem(item) && meaning && name) {
|
|
948
|
+
return [`${meaning}(\`${name}\`)`, extras.join(',')].filter(Boolean).join(':');
|
|
949
|
+
}
|
|
950
|
+
return [name ? `\`${name}\`` : '', meaning, extras.join(',')].filter(Boolean).join(':') || jsonInline(item);
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
function reportMetricSummaryItems(report) {
|
|
954
|
+
if (text(report.definition_kind) === 'SQL') {
|
|
955
|
+
const context = calculationContextForKb(report);
|
|
956
|
+
const summary = completeSqlAgentSummary(report, sqlReportSemanticsById.get(text(report.resource_key)), context.sql_semantic_facts);
|
|
957
|
+
return sqlOutputMetricSummaryItems(summary);
|
|
958
|
+
}
|
|
959
|
+
return array(report.metric_occurrences).map(occurrenceSentence);
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
function sqlOutputMetricSummaryItems(summary) {
|
|
963
|
+
return array(summary?.output_fields)
|
|
964
|
+
.map((field) => renderPlainSemanticItem(field))
|
|
965
|
+
.filter((item) => item && isLikelyMetricText(item))
|
|
966
|
+
.slice(0, 12);
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
function reportDimensionSummaryItems(report, summary = null, calculationContext = null, metadataRefs = null) {
|
|
970
|
+
const context = calculationContext || calculationContextForKb(report);
|
|
971
|
+
const facts = object(context.sql_semantic_facts);
|
|
972
|
+
const refs = metadataRefs || metadataRefsForReport(report);
|
|
973
|
+
const candidates = [
|
|
974
|
+
...array(summary?.output_fields)
|
|
975
|
+
.filter((item) => text(object(item).semantic_role) === 'dimension')
|
|
976
|
+
.map(renderPlainSemanticItem),
|
|
977
|
+
...array(facts.group_by).map((item) => groupByLabel(item, array(summary?.output_fields || []))),
|
|
978
|
+
...array(object(facts.visual_view).dimensions).map((item) => text(object(item).title || object(item).name)),
|
|
979
|
+
...array(object(context.visual_view).dimensions).map((item) => text(object(item).title || object(item).name)),
|
|
980
|
+
...array(object(context.event_view).group_by).map(text),
|
|
981
|
+
...array(object(context.event_view).groups).map(text),
|
|
982
|
+
...array(refs).map((ref) => namedCodeFact(ref.title || ref.display_name, ref.resource_key)),
|
|
983
|
+
];
|
|
984
|
+
return topDomainFacts(candidates.filter((item) => isLikelyDrilldownDimensionText(item) && !isExcludedDrilldownDimensionText(item)), 12);
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
function reportParameterSummaryItems(calculationContext) {
|
|
988
|
+
const context = object(calculationContext);
|
|
989
|
+
return [
|
|
990
|
+
...array(context.sql_params).map((item) => renderPlainSemanticItem({ name: object(item).name || object(item).paramName, meaning: object(item).displayName || object(item).placeholder })),
|
|
991
|
+
].filter(Boolean).slice(0, 12);
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
function reportFilterSummaryItems(report, calculationContext) {
|
|
995
|
+
const items = [
|
|
996
|
+
...array(report.metric_occurrences).flatMap((item) => filterSummaryLines(item.filters ?? item.custom_filters)),
|
|
997
|
+
...array(object(calculationContext).filters).flatMap((item) => filterSummaryLines(item)),
|
|
998
|
+
...array(object(calculationContext).event_view?.filts).flatMap((item) => filterSummaryLines(item)),
|
|
999
|
+
...array(object(calculationContext).visual_view?.filters).flatMap((item) => filterSummaryLines(item)),
|
|
1000
|
+
...array(object(calculationContext).key_predicate_fragments).map(compactSqlFragment),
|
|
1001
|
+
];
|
|
1002
|
+
return topDomainFacts(items, 10);
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
function completeSqlAgentSummary(report, planSummary, facts) {
|
|
1006
|
+
const base = object(planSummary);
|
|
1007
|
+
if (Object.keys(base).length) return { summary_source: 'semantic_plan', ...base };
|
|
1008
|
+
return null;
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
function usefulSemanticList(values) {
|
|
1012
|
+
return array(values).some((value) => {
|
|
1013
|
+
if (value == null) return false;
|
|
1014
|
+
if (typeof value === 'string') {
|
|
1015
|
+
const normalized = value.trim().toLowerCase();
|
|
1016
|
+
return Boolean(normalized) && !['none', 'unknown'].includes(normalized) && !/none observed|未发现/.test(normalized);
|
|
1017
|
+
}
|
|
1018
|
+
return Object.values(object(value)).some((item) => text(item) && !['none', 'unknown'].includes(text(item).toLowerCase()));
|
|
1019
|
+
});
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
function sqlParameterKindMeaning(value) {
|
|
1023
|
+
const kind = text(value).toLowerCase();
|
|
1024
|
+
if (kind.includes('partdate') || kind.includes('date')) return '日期或时间范围参数';
|
|
1025
|
+
if (kind.includes('selector')) return '枚举选择参数';
|
|
1026
|
+
if (kind.includes('text')) return '文本输入参数';
|
|
1027
|
+
if (kind.includes('number')) return '数值输入参数';
|
|
1028
|
+
if (kind.includes('variable')) return '变量参数';
|
|
1029
|
+
return kind ? `${value} 参数` : '';
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
function expressionDisplayName(value) {
|
|
1033
|
+
const expression = text(value);
|
|
1034
|
+
const quoted = expression.match(/^"([^"]+)"$/)?.[1] ?? expression.match(/^`([^`]+)`$/)?.[1];
|
|
1035
|
+
if (quoted) return quoted;
|
|
1036
|
+
const dotted = expression.match(/(?:^|\.)([A-Za-z_][\w$#@]*)$/)?.[1];
|
|
1037
|
+
return dotted || '';
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
function sqlFieldMeaning(name, kind) {
|
|
1041
|
+
const label = text(name);
|
|
1042
|
+
const lower = label.toLowerCase();
|
|
1043
|
+
if (kind === 'all_columns') return '返回源表全部列,资产包未展开字段清单';
|
|
1044
|
+
if (kind === 'aggregate') return `${label} 聚合指标`;
|
|
1045
|
+
if (kind === 'window') return `${label} 窗口计算字段`;
|
|
1046
|
+
if (kind === 'case_expression') return `${label} 分段或条件派生字段`;
|
|
1047
|
+
if (/(date|day|日期|时间)/i.test(label)) return `${label} 时间维度`;
|
|
1048
|
+
if (/(count|cnt|num|数量|人数|次数|用户数|公司数)/i.test(lower)) return `${label} 数量指标`;
|
|
1049
|
+
if (/(rate|ratio|percent|率|占比)/i.test(lower)) return `${label} 比率指标`;
|
|
1050
|
+
return `${label} 输出字段`;
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
function groupByLabel(value, outputFields, parameters = new Map()) {
|
|
1054
|
+
const raw = text(value);
|
|
1055
|
+
if (!raw) return '';
|
|
1056
|
+
const leading = raw.match(/^\s*(\d+)\b/);
|
|
1057
|
+
if (leading) {
|
|
1058
|
+
const field = outputFields[Number(leading[1]) - 1];
|
|
1059
|
+
if (field?.name) return field.name;
|
|
1060
|
+
}
|
|
1061
|
+
return raw.replace(/\$\{(?:(?:Text|Selector|PartDate|Number|Date|DateTime|MultiSelector)\s*:)?\s*([A-Za-z_][\w.-]*)\s*\}/g, (_, name) => {
|
|
1062
|
+
const parameter = parameters.get(text(name));
|
|
1063
|
+
return text(parameter?.display_name) || text(name);
|
|
1064
|
+
});
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
function firstSourceTableName(facts) {
|
|
1068
|
+
return text(object(array(object(facts).source_tables)[0]).table);
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
function normalizeSqlParametersFromFacts(facts) {
|
|
1072
|
+
return new Map(array(object(facts).parameters).map((item) => {
|
|
1073
|
+
const row = object(item);
|
|
1074
|
+
return [text(row.name), row];
|
|
1075
|
+
}).filter(([name]) => name));
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
function parameterizedFieldName(expressionValue, parameters) {
|
|
1079
|
+
const expression = text(expressionValue);
|
|
1080
|
+
const names = [...expression.matchAll(/\$\{(?:(?:Text|Selector|PartDate|Number|Date|DateTime|MultiSelector)\s*:)?\s*([A-Za-z_][\w.-]*)\s*\}/g)]
|
|
1081
|
+
.map((match) => text(match[1]))
|
|
1082
|
+
.filter(Boolean);
|
|
1083
|
+
if (!names.length) return '';
|
|
1084
|
+
const labels = names.map((name) => text(parameters.get(name)?.display_name) || name);
|
|
1085
|
+
return `参数化字段(${labels.join('、')})`;
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
function sanitizeKeyFilters(values) {
|
|
1089
|
+
return array(values).map((value) => {
|
|
1090
|
+
if (typeof value === 'string') return compactSqlFragment(value);
|
|
1091
|
+
const item = object(value);
|
|
1092
|
+
return compactSqlFragment(text(item.meaning ?? item.description ?? item.sql ?? item.expression ?? JSON.stringify(item)));
|
|
1093
|
+
}).filter(Boolean).slice(0, 8);
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
|
|
1097
|
+
function sanitizeVisibleSqlSemanticText(value) {
|
|
1098
|
+
let normalized = text(value)
|
|
1099
|
+
.replace(/\$\{(?:(?:Text|Selector|PartDate|Number|Date|DateTime|MultiSelector)\s*:)?\s*([A-Za-z_][\w.-]*)\s*\}/g, '$1')
|
|
1100
|
+
.replace(/SQL\s*输出字段[::]?/gi, '')
|
|
1101
|
+
.replace(/来源表达式[::]?/g, '')
|
|
1102
|
+
.replace(/where 条件[::]?/gi, '')
|
|
1103
|
+
.replace(/[;;]?\s*计算上下文[::]\s*/g, ';')
|
|
1104
|
+
.replace(/\bORDER\s+BY\b/gi, '排序')
|
|
1105
|
+
.replace(/\bGROUP\s+BY\b/gi, '分组')
|
|
1106
|
+
.replace(/\bSelector\b/gi, '选择参数')
|
|
1107
|
+
.replace(/\bPartDate\b/gi, '日期参数')
|
|
1108
|
+
.replace(/\bVariable\b/gi, '变量参数')
|
|
1109
|
+
.replace(/\bNumber\b/gi, '数值参数')
|
|
1110
|
+
.replace(/无\s*LIMIT/gi, '无行数上限')
|
|
1111
|
+
.replace(/\s+/g, ' ')
|
|
1112
|
+
.replace(/;{2,}/g, ';')
|
|
1113
|
+
.replace(/^[;;,,\s]+|[;;,,\s]+$/g, '')
|
|
1114
|
+
.trim();
|
|
1115
|
+
normalized = dedupeSemicolonSegments(normalized);
|
|
1116
|
+
return normalized;
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
function dedupeSemicolonSegments(value) {
|
|
1120
|
+
const parts = text(value).split(';').map((part) => part.trim()).filter(Boolean);
|
|
1121
|
+
const result = [];
|
|
1122
|
+
for (const part of parts) {
|
|
1123
|
+
const previous = result.at(-1) || '';
|
|
1124
|
+
if (previous === part || previous.endsWith(part)) continue;
|
|
1125
|
+
result.push(part);
|
|
1126
|
+
}
|
|
1127
|
+
return result.join(';');
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
function renderSemanticList(value, renderer) {
|
|
1131
|
+
const values = array(value);
|
|
1132
|
+
if (!values.length) return '- 无';
|
|
1133
|
+
return values.map((item) => `- ${renderer(item)}`).join('\n');
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
function renderSemanticItem(value) {
|
|
1137
|
+
if (value == null) return '无';
|
|
1138
|
+
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') return text(value);
|
|
1139
|
+
const item = object(value);
|
|
1140
|
+
const name = text(item.name ?? item.field ?? item.key ?? item.column ?? item.title);
|
|
1141
|
+
const meaning = text(item.meaning ?? item.description ?? item.remark ?? item.purpose);
|
|
1142
|
+
const details = Object.entries(item)
|
|
1143
|
+
.filter(([key]) => !['name', 'field', 'key', 'column', 'title', 'meaning', 'description', 'remark', 'purpose'].includes(key))
|
|
1144
|
+
.map(([key, itemValue]) => `${key}=${inlineSemanticValue(itemValue)}`)
|
|
1145
|
+
.join(', ');
|
|
1146
|
+
if (isInputParameterSemanticItem(item) && meaning && name) {
|
|
1147
|
+
return [`${meaning}(\`${name}\`)`, details].filter(Boolean).join(':') || jsonInline(item);
|
|
1148
|
+
}
|
|
1149
|
+
return [name ? `\`${name}\`` : '', meaning, details].filter(Boolean).join(':') || jsonInline(item);
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
function isInputParameterSemanticItem(item) {
|
|
1153
|
+
const row = object(item);
|
|
1154
|
+
const name = text(row.name ?? row.field ?? row.key ?? row.column ?? row.title);
|
|
1155
|
+
const meaning = text(row.meaning ?? row.description ?? row.remark ?? row.purpose);
|
|
1156
|
+
return row.required != null
|
|
1157
|
+
|| row.default_value != null
|
|
1158
|
+
|| Array.isArray(row.options)
|
|
1159
|
+
|| (Boolean(meaning) && /^(?:selector|variable|date|partdate|text|number)\d*$/i.test(name));
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
function inlineSemanticValue(value) {
|
|
1163
|
+
if (value == null) return 'null';
|
|
1164
|
+
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') return JSON.stringify(value);
|
|
1165
|
+
if (Array.isArray(value)) {
|
|
1166
|
+
const labels = value.map((item) => {
|
|
1167
|
+
const row = object(item);
|
|
1168
|
+
return text(row.label ?? row.name ?? row.title ?? row.value ?? item);
|
|
1169
|
+
}).filter(Boolean);
|
|
1170
|
+
return labels.length ? JSON.stringify(labels.slice(0, 20).join('、')) : JSON.stringify(value);
|
|
1171
|
+
}
|
|
1172
|
+
return JSON.stringify(value);
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
|
|
1176
|
+
function calculationContextForKb(report) {
|
|
1177
|
+
const context = { ...reportDetailCalculationContext(report), ...object(report.calculation_context) };
|
|
1178
|
+
const sql = sqlTextFromCalculationContext(context);
|
|
1179
|
+
if (text(report.definition_kind) !== 'SQL' && !sql) return context;
|
|
1180
|
+
return {
|
|
1181
|
+
...context,
|
|
1182
|
+
sql_semantic_facts: extractSqlSemanticFacts(sql, context),
|
|
1183
|
+
};
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
function reportDetailCalculationContext(report) {
|
|
1187
|
+
const normalizedDetail = readPackageJson(text(report.source_detail_path));
|
|
1188
|
+
const rawDetail = readPackageJson(text(object(normalizedDetail.raw_locator).path));
|
|
1189
|
+
const rawDefinition = object(rawDetail.raw_definition);
|
|
1190
|
+
const result = {};
|
|
1191
|
+
const visualView = jsonObject(rawDefinition.visual_view ?? rawDefinition.visualView);
|
|
1192
|
+
if (Object.keys(visualView).length) result.visual_view = visualView;
|
|
1193
|
+
const eventView = jsonObject(rawDefinition.event_view ?? rawDefinition.eventView);
|
|
1194
|
+
if (Object.keys(eventView).length) result.event_view = eventView;
|
|
1195
|
+
const events = jsonObject(rawDefinition.events);
|
|
1196
|
+
if (array(events.sqlVoParams).length) result.sql_params = events.sqlVoParams;
|
|
1197
|
+
return result;
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
function sqlTextFromCalculationContext(context) {
|
|
1201
|
+
const direct = text(context.sql);
|
|
1202
|
+
if (direct) return direct;
|
|
1203
|
+
return firstStringByKey(context, new Set(['sql', 'sql_text', 'sqlText', 'query']));
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
function extractSqlSemanticFacts(sql, context) {
|
|
1207
|
+
const normalizedSql = normalizeSql(sql);
|
|
1208
|
+
const placeholders = extractSqlPlaceholders(normalizedSql);
|
|
1209
|
+
const finalSelect = extractFinalSelect(normalizedSql);
|
|
1210
|
+
const sourceTables = extractSourceTables(normalizedSql);
|
|
1211
|
+
const warnings = [];
|
|
1212
|
+
if (!normalizedSql) warnings.push('sql_text_missing');
|
|
1213
|
+
if (normalizedSql && !finalSelect.fields.length) warnings.push('final_select_not_confidently_parsed');
|
|
1214
|
+
if (normalizedSql && !sourceTables.length) warnings.push('source_tables_not_confidently_parsed');
|
|
1215
|
+
if (finalSelect.truncated) warnings.push('final_select_field_list_truncated');
|
|
1216
|
+
if (sourceTables.length >= 80) warnings.push('source_table_list_truncated');
|
|
1217
|
+
return {
|
|
1218
|
+
schema_version: '1.0',
|
|
1219
|
+
extraction_method: 'cli_deterministic_sql_facts',
|
|
1220
|
+
extraction_boundary: 'mechanical_sql_structure_only_not_business_summary',
|
|
1221
|
+
raw_sql_sha256: normalizedSql ? createHash('sha256').update(normalizedSql).digest('hex') : null,
|
|
1222
|
+
sql_char_count: normalizedSql.length,
|
|
1223
|
+
statement_count: countSqlStatements(normalizedSql),
|
|
1224
|
+
placeholders,
|
|
1225
|
+
parameters: normalizeSqlParameters(context, placeholders),
|
|
1226
|
+
visual_view: extractVisualViewFacts(context),
|
|
1227
|
+
final_select: finalSelect,
|
|
1228
|
+
source_tables: sourceTables.slice(0, 80),
|
|
1229
|
+
group_by: extractClauseList(normalizedSql, 'group by', ['having', 'order by', 'limit', 'union']),
|
|
1230
|
+
order_by: extractClauseList(normalizedSql, 'order by', ['limit', 'union']),
|
|
1231
|
+
limit: extractLimit(normalizedSql),
|
|
1232
|
+
key_predicate_fragments: [
|
|
1233
|
+
...extractClauseFragments(normalizedSql, 'where', ['group by', 'having', 'order by', 'limit', 'union']),
|
|
1234
|
+
...extractClauseFragments(normalizedSql, 'having', ['order by', 'limit', 'union']),
|
|
1235
|
+
].slice(0, 40),
|
|
1236
|
+
warnings,
|
|
1237
|
+
};
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
function normalizeSql(value) {
|
|
1241
|
+
return text(value).replace(/\r\n?/g, '\n').replace(/[ \t]+$/gm, '').trim();
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
function extractSqlPlaceholders(sql) {
|
|
1245
|
+
const found = [];
|
|
1246
|
+
const seen = new Set();
|
|
1247
|
+
const patterns = [
|
|
1248
|
+
/\$\{(?:(Text|Selector|PartDate|Number|Date|DateTime|MultiSelector)\s*:)?\s*([A-Za-z_][\w.-]*)\s*\}/g,
|
|
1249
|
+
/:([A-Za-z_][\w.-]*)/g,
|
|
1250
|
+
];
|
|
1251
|
+
for (const pattern of patterns) {
|
|
1252
|
+
for (const match of sql.matchAll(pattern)) {
|
|
1253
|
+
const raw = match[0];
|
|
1254
|
+
const name = match[2] ?? match[1];
|
|
1255
|
+
if (raw.startsWith(':') && isInsideDollarPlaceholder(sql, match.index)) continue;
|
|
1256
|
+
if (!name || seen.has(raw)) continue;
|
|
1257
|
+
seen.add(raw);
|
|
1258
|
+
found.push({ raw, name, kind: match[2] ? text(match[1]) || 'raw' : 'colon' });
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
return found.sort((a, b) => a.raw.localeCompare(b.raw, 'en'));
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
|
|
1265
|
+
function extractVisualViewFacts(context) {
|
|
1266
|
+
const view = jsonObject(context.visual_view ?? context.visualView);
|
|
1267
|
+
const dimensions = array(view.groupBys ?? view.group_by ?? view.dimensions).map(normalizeVisualField).filter((item) => item.name || item.title);
|
|
1268
|
+
const measures = array(view.aggregates ?? view.measures ?? view.displayQuotas).map(normalizeVisualField).filter((item) => item.name || item.title);
|
|
1269
|
+
return {
|
|
1270
|
+
dimensions,
|
|
1271
|
+
measures,
|
|
1272
|
+
};
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
function normalizeVisualField(value) {
|
|
1276
|
+
const row = object(value);
|
|
1277
|
+
return {
|
|
1278
|
+
name: text(row.columnName ?? row.name ?? row.key ?? row.quota ?? row.eventNameDisplay),
|
|
1279
|
+
title: text(row.columnDesc ?? row.title ?? row.display_name ?? row.displayName ?? row.name ?? row.columnName ?? row.eventNameDisplay),
|
|
1280
|
+
column_type: text(row.columnType ?? row.type ?? row.selectType) || null,
|
|
1281
|
+
analysis: text(row.analysis ?? row.analysisDesc) || null,
|
|
1282
|
+
};
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
function semanticFieldsFromVisualFacts(value) {
|
|
1286
|
+
const facts = object(value);
|
|
1287
|
+
return [
|
|
1288
|
+
...array(facts.dimensions).map((field) => visualSemanticField(field, 'dimension')),
|
|
1289
|
+
...array(facts.measures).map((field) => visualSemanticField(field, 'measure')),
|
|
1290
|
+
].filter((field) => field.name);
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
function visualSemanticField(value, role) {
|
|
1294
|
+
const field = object(value);
|
|
1295
|
+
const name = text(field.title || field.name);
|
|
1296
|
+
return {
|
|
1297
|
+
name,
|
|
1298
|
+
meaning: role === 'dimension' ? `${name} 分组维度` : `${name} 指标`,
|
|
1299
|
+
expression_kind: 'visual_view',
|
|
1300
|
+
semantic_role: role,
|
|
1301
|
+
};
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
function normalizeSqlParameters(context, placeholders) {
|
|
1305
|
+
const candidate = [context.parameters, context.params, context.sql_params, context.sqlParams, object(context.taSqlVo).sqlVoParams]
|
|
1306
|
+
.find(Array.isArray);
|
|
1307
|
+
const byName = new Map(array(candidate).map((item) => {
|
|
1308
|
+
const row = object(item);
|
|
1309
|
+
return [text(row.name ?? row.key ?? row.param_name ?? row.paramName), row];
|
|
1310
|
+
}).filter(([name]) => name));
|
|
1311
|
+
return placeholders.map((placeholder) => {
|
|
1312
|
+
const row = byName.get(placeholder.name) ?? {};
|
|
1313
|
+
const options = array(row.options ?? row.selectorItems ?? row.values).map((item) => {
|
|
1314
|
+
const option = object(item);
|
|
1315
|
+
return {
|
|
1316
|
+
value: option.value ?? option.selectorValue ?? option.key ?? option.id ?? null,
|
|
1317
|
+
label: text(option.label ?? option.selectorName ?? option.name ?? option.title ?? option.text) || null,
|
|
1318
|
+
};
|
|
1319
|
+
}).slice(0, 60);
|
|
1320
|
+
return {
|
|
1321
|
+
name: placeholder.name,
|
|
1322
|
+
placeholder: placeholder.raw,
|
|
1323
|
+
placeholder_kind: placeholder.kind,
|
|
1324
|
+
param_type: text(row.paramType ?? row.param_type ?? row.type) || null,
|
|
1325
|
+
display_name: text(row.paramDisplay ?? row.display_name ?? row.displayName ?? row.title) || null,
|
|
1326
|
+
remark: text(row.paramRemark ?? row.remark ?? row.description) || null,
|
|
1327
|
+
required: row.required == null ? null : Boolean(row.required),
|
|
1328
|
+
default_value: row.defaultValue ?? row.default_value ?? row.value ?? null,
|
|
1329
|
+
use_timezone: row.use_timezone ?? row.useTimezone ?? null,
|
|
1330
|
+
options,
|
|
1331
|
+
};
|
|
1332
|
+
});
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
function extractFinalSelect(sql) {
|
|
1336
|
+
const select = findTopLevelKeyword(sql, 'select', { last: true });
|
|
1337
|
+
if (select < 0) return { fields: [], truncated: false };
|
|
1338
|
+
const from = findNextTopLevelKeyword(sql, 'from', select + 'select'.length);
|
|
1339
|
+
if (from < 0) return { fields: [], truncated: false };
|
|
1340
|
+
const projection = sql.slice(select + 'select'.length, from).trim();
|
|
1341
|
+
const rawFields = splitTopLevel(projection, ',');
|
|
1342
|
+
return {
|
|
1343
|
+
fields: rawFields.slice(0, 80).map((expression, index) => summarizeSelectField(expression, index + 1)),
|
|
1344
|
+
truncated: rawFields.length > 80,
|
|
1345
|
+
};
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
function summarizeSelectField(expression, ordinal) {
|
|
1349
|
+
const expr = expression.trim();
|
|
1350
|
+
const aliasMatch = expr.match(/\s+as\s+((?:"[^"]+")|(?:`[^`]+`)|(?:\[[^\]]+\])|(?:[A-Za-z_][\w$#@.-]*))\s*$/i)
|
|
1351
|
+
?? expr.match(/\s+((?:"[^"]+")|(?:`[^`]+`)|(?:\[[^\]]+\])|(?:[A-Za-z_][\w$#@.-]*))\s*$/);
|
|
1352
|
+
const alias = aliasMatch && aliasMatch.index > 0 ? unquoteIdentifier(aliasMatch[1]) : null;
|
|
1353
|
+
return {
|
|
1354
|
+
ordinal,
|
|
1355
|
+
name: alias,
|
|
1356
|
+
expression: shortenSql(expr, 500),
|
|
1357
|
+
expression_kind: classifySelectExpression(expr),
|
|
1358
|
+
};
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
function classifySelectExpression(expression) {
|
|
1362
|
+
const value = expression.toLowerCase();
|
|
1363
|
+
if (/\b(count|sum|avg|min|max|approx_distinct|count_if|percentile)\s*\(/.test(value)) return 'aggregate';
|
|
1364
|
+
if (/\bover\s*\(/.test(value)) return 'window';
|
|
1365
|
+
if (/\bcase\b/.test(value)) return 'case_expression';
|
|
1366
|
+
if (expression.trim() === '*') return 'all_columns';
|
|
1367
|
+
if (/\$\{/.test(expression)) return 'parameterized_expression';
|
|
1368
|
+
return 'field_or_expression';
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
function extractSourceTables(sql) {
|
|
1372
|
+
const tables = [];
|
|
1373
|
+
const seen = new Set();
|
|
1374
|
+
const pattern = /\b(from|join)\s+((?:"[^"]+"|`[^`]+`|\[[^\]]+\]|[A-Za-z_][\w$#@.-]*)(?:\s*\.\s*(?:"[^"]+"|`[^`]+`|\[[^\]]+\]|[A-Za-z_][\w$#@.-]*))*)/gi;
|
|
1375
|
+
for (const match of stripSqlComments(sql).matchAll(pattern)) {
|
|
1376
|
+
const normalized = unquoteIdentifier(match[2].replace(/\s*\.\s*/g, '.'));
|
|
1377
|
+
if (!normalized || seen.has(normalized.toLowerCase())) continue;
|
|
1378
|
+
seen.add(normalized.toLowerCase());
|
|
1379
|
+
tables.push({ clause: match[1].toLowerCase(), table: normalized });
|
|
1380
|
+
}
|
|
1381
|
+
return tables;
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
function extractClauseFragments(sql, keyword, endKeywords) {
|
|
1385
|
+
const clause = extractTopLevelClause(sql, keyword, endKeywords);
|
|
1386
|
+
if (!clause) return [];
|
|
1387
|
+
return splitTopLevel(clause, /\band\b/i).map((item) => shortenSql(item, 500)).filter(Boolean);
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
function extractClauseList(sql, keyword, endKeywords) {
|
|
1391
|
+
const clause = extractTopLevelClause(sql, keyword, endKeywords);
|
|
1392
|
+
if (!clause) return [];
|
|
1393
|
+
return splitTopLevel(clause, ',').map((item) => shortenSql(item, 300)).filter(Boolean).slice(0, 80);
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
function extractTopLevelClause(sql, keyword, endKeywords) {
|
|
1397
|
+
const start = findTopLevelKeyword(sql, keyword, { last: true });
|
|
1398
|
+
if (start < 0) return '';
|
|
1399
|
+
const bodyStart = start + keyword.length;
|
|
1400
|
+
const endPositions = endKeywords.map((value) => findNextTopLevelKeyword(sql, value, bodyStart)).filter((value) => value >= 0);
|
|
1401
|
+
const end = endPositions.length ? Math.min(...endPositions) : sql.length;
|
|
1402
|
+
return sql.slice(bodyStart, end).trim();
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
function extractLimit(sql) {
|
|
1406
|
+
const match = stripSqlComments(sql).match(/\blimit\s+(.+?)\s*;?\s*$/i);
|
|
1407
|
+
if (!match) return null;
|
|
1408
|
+
const rawValue = match[1].trim();
|
|
1409
|
+
const numeric = rawValue.match(/^(\d+)(?:\s*,\s*(\d+))?(?:\s+offset\s+(\d+))?$/i);
|
|
1410
|
+
if (!numeric) return { value: null, offset: null, expression: rawValue, raw: match[0].trim() };
|
|
1411
|
+
return {
|
|
1412
|
+
value: Number(numeric[2] ?? numeric[1]),
|
|
1413
|
+
offset: numeric[3] == null && numeric[2] != null ? Number(numeric[1]) : numeric[3] == null ? null : Number(numeric[3]),
|
|
1414
|
+
raw: match[0].trim(),
|
|
1415
|
+
};
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
function countSqlStatements(sql) {
|
|
1419
|
+
return splitTopLevel(stripSqlComments(sql), ';').map((item) => item.trim()).filter(Boolean).length;
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
function findTopLevelKeyword(sql, keyword, options = {}) {
|
|
1423
|
+
const positions = [];
|
|
1424
|
+
const lower = sql.toLowerCase();
|
|
1425
|
+
const target = keyword.toLowerCase();
|
|
1426
|
+
let quote = null;
|
|
1427
|
+
let lineComment = false;
|
|
1428
|
+
let blockComment = false;
|
|
1429
|
+
let depth = 0;
|
|
1430
|
+
for (let i = 0; i < sql.length; i += 1) {
|
|
1431
|
+
const ch = sql[i];
|
|
1432
|
+
const next = sql[i + 1];
|
|
1433
|
+
if (lineComment) {
|
|
1434
|
+
if (ch === '\n') lineComment = false;
|
|
1435
|
+
continue;
|
|
1436
|
+
}
|
|
1437
|
+
if (blockComment) {
|
|
1438
|
+
if (ch === '*' && next === '/') { blockComment = false; i += 1; }
|
|
1439
|
+
continue;
|
|
1440
|
+
}
|
|
1441
|
+
if (quote) {
|
|
1442
|
+
if (ch === quote && sql[i - 1] !== '\\') quote = null;
|
|
1443
|
+
continue;
|
|
1444
|
+
}
|
|
1445
|
+
if (ch === '-' && next === '-') { lineComment = true; i += 1; continue; }
|
|
1446
|
+
if (ch === '/' && next === '*') { blockComment = true; i += 1; continue; }
|
|
1447
|
+
if (ch === "'" || ch === '"' || ch === '`') { quote = ch; continue; }
|
|
1448
|
+
if (ch === '(') { depth += 1; continue; }
|
|
1449
|
+
if (ch === ')') { depth = Math.max(0, depth - 1); continue; }
|
|
1450
|
+
if (depth === 0 && lower.startsWith(target, i) && isKeywordBoundary(sql[i - 1]) && isKeywordBoundary(sql[i + target.length])) {
|
|
1451
|
+
positions.push(i);
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
return options.last ? positions.at(-1) ?? -1 : positions[0] ?? -1;
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
function findNextTopLevelKeyword(sql, keyword, offset) {
|
|
1458
|
+
const position = findTopLevelKeyword(sql.slice(offset), keyword);
|
|
1459
|
+
return position < 0 ? -1 : offset + position;
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
function splitTopLevel(value, separator) {
|
|
1463
|
+
const parts = [];
|
|
1464
|
+
let quote = null;
|
|
1465
|
+
let lineComment = false;
|
|
1466
|
+
let blockComment = false;
|
|
1467
|
+
let depth = 0;
|
|
1468
|
+
let start = 0;
|
|
1469
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
1470
|
+
const ch = value[i];
|
|
1471
|
+
const next = value[i + 1];
|
|
1472
|
+
if (lineComment) {
|
|
1473
|
+
if (ch === '\n') lineComment = false;
|
|
1474
|
+
continue;
|
|
1475
|
+
}
|
|
1476
|
+
if (blockComment) {
|
|
1477
|
+
if (ch === '*' && next === '/') { blockComment = false; i += 1; }
|
|
1478
|
+
continue;
|
|
1479
|
+
}
|
|
1480
|
+
if (quote) {
|
|
1481
|
+
if (ch === quote && value[i - 1] !== '\\') quote = null;
|
|
1482
|
+
continue;
|
|
1483
|
+
}
|
|
1484
|
+
if (ch === '-' && next === '-') { lineComment = true; i += 1; continue; }
|
|
1485
|
+
if (ch === '/' && next === '*') { blockComment = true; i += 1; continue; }
|
|
1486
|
+
if (ch === "'" || ch === '"' || ch === '`') { quote = ch; continue; }
|
|
1487
|
+
if (ch === '(') { depth += 1; continue; }
|
|
1488
|
+
if (ch === ')') { depth = Math.max(0, depth - 1); continue; }
|
|
1489
|
+
if (depth !== 0) continue;
|
|
1490
|
+
if (typeof separator === 'string' && ch === separator) {
|
|
1491
|
+
parts.push(value.slice(start, i).trim());
|
|
1492
|
+
start = i + 1;
|
|
1493
|
+
} else if (separator instanceof RegExp) {
|
|
1494
|
+
const match = value.slice(i).match(separator);
|
|
1495
|
+
if (match?.index === 0) {
|
|
1496
|
+
parts.push(value.slice(start, i).trim());
|
|
1497
|
+
start = i + match[0].length;
|
|
1498
|
+
i = start - 1;
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
parts.push(value.slice(start).trim());
|
|
1503
|
+
return parts.filter(Boolean);
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
function stripSqlComments(sql) {
|
|
1507
|
+
return sql.replace(/--[^\n\r]*/g, '').replace(/\/\*[\s\S]*?\*\//g, '');
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
function shortenSql(value, max) {
|
|
1511
|
+
const collapsed = text(value).replace(/\s+/g, ' ').trim();
|
|
1512
|
+
return collapsed.length > max ? `${collapsed.slice(0, max - 1)}…` : collapsed;
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
function firstStringByKey(value, keys) {
|
|
1516
|
+
if (Array.isArray(value)) {
|
|
1517
|
+
for (const item of value) {
|
|
1518
|
+
const found = firstStringByKey(item, keys);
|
|
1519
|
+
if (found) return found;
|
|
1520
|
+
}
|
|
1521
|
+
return '';
|
|
1522
|
+
}
|
|
1523
|
+
if (!value || typeof value !== 'object') return '';
|
|
1524
|
+
for (const [key, item] of Object.entries(value)) {
|
|
1525
|
+
if (keys.has(key) && typeof item === 'string' && item.trim()) return item;
|
|
1526
|
+
const found = firstStringByKey(item, keys);
|
|
1527
|
+
if (found) return found;
|
|
1528
|
+
}
|
|
1529
|
+
return '';
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
function isInsideDollarPlaceholder(sql, index) {
|
|
1533
|
+
const open = sql.lastIndexOf('${', index);
|
|
1534
|
+
if (open < 0) return false;
|
|
1535
|
+
const close = sql.lastIndexOf('}', index);
|
|
1536
|
+
return close < open;
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
function unquoteIdentifier(value) {
|
|
1540
|
+
return text(value).split('.').map((part) => part.trim().replace(/^["`\[]|["`\]]$/g, '')).join('.');
|
|
1541
|
+
}
|
|
1542
|
+
|
|
1543
|
+
function isKeywordBoundary(ch) {
|
|
1544
|
+
return !ch || !/[A-Za-z0-9_$#@.-]/.test(ch);
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
function renderMetricSource(metric) {
|
|
1548
|
+
const id = text(metric.resource_key);
|
|
1549
|
+
const metadataRefs = metadataRefsForMetric(metric);
|
|
1550
|
+
const occurrences = array(metric.metric_occurrences);
|
|
1551
|
+
const dependencies = [
|
|
1552
|
+
...array(metric.dependent_events).map((item) => namedCodeFact(object(item).title || object(item).display_name, object(item).resource_key || item)),
|
|
1553
|
+
...array(metric.dependent_properties).map((item) => namedCodeFact(object(item).title || object(item).display_name, object(item).resource_key || item)),
|
|
1554
|
+
].filter(Boolean);
|
|
1555
|
+
const metadataDependencies = metadataRefs
|
|
1556
|
+
.filter((item) => ['event', 'event_prop', 'user_prop', 'tag', 'user_tag', 'cluster', 'user_cluster'].includes(text(item.resource_type)))
|
|
1557
|
+
.map((item) => namedCodeFact(item.title, item.resource_key))
|
|
1558
|
+
.filter(Boolean);
|
|
1559
|
+
const dependencySummary = [...new Set([...dependencies, ...metadataDependencies])];
|
|
1560
|
+
return `${assetFrontmatter('project-reusable-metric', metric)}\n# ${heading(metric.title || `Metric ${id}`)}\n\n## Agent 使用摘要\n\n- 用途:统一表达“${text(metric.title || id)}”这一可复用指标,供报表和业务域复用。\n- 计算口径:\n${occurrences.map((item) => ` - ${occurrenceSentence(item)}`).join('\n') || ' - 未识别到结构化指标项,需查看原始定义。'}\n- 依赖事件/属性:${dependencySummary.slice(0, 12).join('、') || '未识别到稳定依赖。'}\n- 使用边界:当前数值需要实时执行对应报表或查询;知识库只说明口径和依赖。\n\n## 身份\n\n- 指标 key: \`${id}\`\n- 已认证: ${Boolean(metric.authenticated)}\n- 源详情: \`${text(metric.source_detail_path) || 'unavailable'}\`\n\n## 描述\n\n${text(metric.description || metric.summary) || '_资产包中没有描述。_'}\n\n## 计算定义\n\n${occurrences.map(renderOccurrence).join('\n\n') || '_未识别到结构化指标项。_'}\n\n## 引用元数据(${metadataRefs.length})\n\n${metadataLinks(metadataRefs, '../metadata')}\n\n## 证据定位\n\n- 资产包源定位: \`${text(metric.source_detail_path) || 'unavailable'}\`\n`;
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
function renderMetadataSource(item) {
|
|
1564
|
+
const id = text(item.evidence_id);
|
|
1565
|
+
const usage = metadataUsageByEvidenceId.get(id) ?? [];
|
|
1566
|
+
const metricUsage = metadataMetricUsageByEvidenceId.get(id) ?? [];
|
|
1567
|
+
const title = metadataDisplayTitle(item);
|
|
1568
|
+
return `---\ntype: project-analysis-metadata\nproject_id: ${projectId}\nevidence_id: ${yaml(id)}\nresource_type: ${yaml(item.resource_type)}\nresource_key: ${yaml(item.resource_key)}\ntitle: ${yaml(title)}\nauthenticated: ${Boolean(item.authenticated)}\nsnapshot_hash: ${yaml(snapshotHash)}\n---\n\n# ${heading(title)}\n\n## Agent 使用摘要\n\n${metadataAgentSummaryLines(item, usage, metricUsage).map((line) => `- ${line}`).join('\n')}\n\n## 身份\n\n- 元数据类型: ${metadataTypeLabel(item.resource_type)}\n- Resource key: \`${text(item.resource_key)}\`\n- 已认证: ${Boolean(item.authenticated)}\n- Evidence ID: \`${id}\`\n\n## 可读定义\n\n${text(item.description || item.summary) || '_资产包中没有描述。_'}\n\n${metadataDefinitionLines(item).map((line) => `- ${line}`).join('\n') || '- 资产包未提供可读结构化定义。'}\n\n## 被报表引用(${usage.length})\n\n${refLinks(usage, 'report_id', 'report_name', reportFiles, '../../reports')}\n\n## 被可复用指标引用(${metricUsage.length})\n\n${refLinks(metricUsage, 'metric_id', 'metric_name', metricFiles, '../../metrics')}\n\n## 证据定位\n\n${metadataEvidenceLines(item).map((line) => `- ${line}`).join('\n')}\n`;
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
|
|
1572
|
+
function metadataAgentSummaryLines(item, usage, metricUsage) {
|
|
1573
|
+
const type = text(item.resource_type);
|
|
1574
|
+
const label = metadataTypeLabel(type);
|
|
1575
|
+
const title = metadataDisplayTitle(item);
|
|
1576
|
+
const result = [
|
|
1577
|
+
`用途:这是${label}“${title}”,用于理解报表、看板或可复用指标中引用的底层字段/事件,不作为独立业务结论。`,
|
|
1578
|
+
`使用方式:${metadataUsageHint(type)}。`,
|
|
1579
|
+
`引用范围:当前快照中被 ${usage.length} 张报表直接引用,被 ${metricUsage.length} 个可复用指标依赖。`,
|
|
1580
|
+
`认证边界:${item.authenticated === true ? '已认证,可作为被引用资产的元数据依据。' : '未认证,只能作为被认证报表/指标的依赖线索,不能单独作为权威口径。'}`,
|
|
1581
|
+
'实时边界:知识库只记录定义、引用和适用范围;具体数值、用户清单或趋势仍需实时执行报表/查询。',
|
|
1582
|
+
];
|
|
1583
|
+
if (!text(item.title)) result.push('命名边界:资产包没有业务标题,当前仅保留 resource key,回答时需要回到承载报表确认业务含义。');
|
|
1584
|
+
return result;
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1587
|
+
function metadataUsageHint(type) {
|
|
1588
|
+
return ({
|
|
1589
|
+
event: '作为行为事件入口,优先用于判断某个报表或指标观察的是哪类用户行为',
|
|
1590
|
+
event_prop: '作为事件属性,常用于筛选、分组、下钻或度量字段,需结合引用它的事件理解',
|
|
1591
|
+
user_prop: '作为用户属性,常用于用户分层、筛选或分组,需确认统计对象是否为用户/公司/账号',
|
|
1592
|
+
tag: '作为标签或标签派生字段,常用于分群、过滤或维度拆分,需注意标签版本和生成规则',
|
|
1593
|
+
user_tag: '作为用户标签,常用于用户/公司分层、过滤或维度拆分,需注意标签版本和生成规则',
|
|
1594
|
+
cluster: '作为分群条件,常用于限制分析对象范围,需注意分群快照和计算时间',
|
|
1595
|
+
user_cluster: '作为用户分群条件,常用于限制分析对象范围,需注意分群快照和计算时间',
|
|
1596
|
+
metric: '作为指标元数据,常用于复用口径;精确计算还应查看可复用指标页或承载报表',
|
|
1597
|
+
})[text(type)] ?? '作为依赖元数据使用,需要回到引用它的报表、指标或业务域确认语义';
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1600
|
+
function metadataDefinitionLines(item) {
|
|
1601
|
+
const definition = object(item.definition);
|
|
1602
|
+
const lines = [];
|
|
1603
|
+
const add = (label, value) => {
|
|
1604
|
+
const normalized = text(value);
|
|
1605
|
+
if (normalized) lines.push(`${label}: ${normalized}`);
|
|
1606
|
+
};
|
|
1607
|
+
add('资产类型', definition.asset_kind);
|
|
1608
|
+
add('来源类型', definition.source_type);
|
|
1609
|
+
add('分析类型', definition.select_type);
|
|
1610
|
+
add('存储类型', definition.real_data_type);
|
|
1611
|
+
add('单位', definition.unit);
|
|
1612
|
+
const tableScope = metadataTableScope(definition.table_type);
|
|
1613
|
+
if (tableScope) lines.push(`作用对象: ${tableScope}`);
|
|
1614
|
+
const tagKind = text(definition.tag_sub_type || definition.tag_type);
|
|
1615
|
+
if (tagKind) lines.push(`标签类型: ${tagKind}${tagKind === 'sql' ? '(SQL 标签,知识页不展开原始 SQL)' : ''}`);
|
|
1616
|
+
if (definition.has_connected != null) lines.push(`已连接/可用状态: ${definition.has_connected ? '是' : '否'}`);
|
|
1617
|
+
if (text(definition.entity_mapping_scope)) lines.push(`实体映射范围: ${text(definition.entity_mapping_scope)}`);
|
|
1618
|
+
const sourceVersion = text(object(item.raw).sourceVersion ?? object(item.raw).source_version);
|
|
1619
|
+
if (sourceVersion) lines.push(`来源版本: ${sourceVersion}`);
|
|
1620
|
+
return lines;
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
function metadataTableScope(value) {
|
|
1624
|
+
const normalized = text(value);
|
|
1625
|
+
if (normalized === '0') return '事件';
|
|
1626
|
+
if (normalized === '1') return '用户';
|
|
1627
|
+
if (normalized === '2') return '标签/分群';
|
|
1628
|
+
return '';
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
function metadataEvidenceLines(item) {
|
|
1632
|
+
const raw = object(item.raw);
|
|
1633
|
+
return [
|
|
1634
|
+
`Evidence ID: \`${text(item.evidence_id)}\``,
|
|
1635
|
+
`Source ref: \`${text(raw.sourceRef ?? raw.source_ref) || 'unknown'}\``,
|
|
1636
|
+
`Source version: \`${text(raw.sourceVersion ?? raw.source_version) || 'unknown'}\``,
|
|
1637
|
+
];
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
function renderMetadataBrief(item) {
|
|
1641
|
+
const id = text(item.evidence_id);
|
|
1642
|
+
const usage = metadataUsageByEvidenceId.get(id) ?? [];
|
|
1643
|
+
const metricUsage = metadataMetricUsageByEvidenceId.get(id) ?? [];
|
|
1644
|
+
const definition = object(item.definition);
|
|
1645
|
+
const details = [
|
|
1646
|
+
definition.asset_kind ? `资产类型 ${text(definition.asset_kind)}` : '',
|
|
1647
|
+
definition.select_type ? `分析类型 ${text(definition.select_type)}` : '',
|
|
1648
|
+
definition.real_data_type ? `存储类型 ${text(definition.real_data_type)}` : '',
|
|
1649
|
+
definition.unit ? `单位 ${text(definition.unit)}` : '',
|
|
1650
|
+
].filter(Boolean).join(',');
|
|
1651
|
+
const titleStatus = text(item.title) ? {} : { title_status: 'source_title_missing' };
|
|
1652
|
+
const title = metadataDisplayTitle(item);
|
|
1653
|
+
return `${briefFrontmatter('ai-metadata-brief', {
|
|
1654
|
+
evidence_id: id, resource_key: item.resource_key, authenticated: Boolean(item.authenticated), ...titleStatus,
|
|
1655
|
+
}, text(item.title) ? 'high' : 'medium')}\n# ${heading(title)}\n\n> 这是一个${metadataTypeLabel(item.resource_type)}${details ? `,${details}` : ''};在当前快照中被 ${usage.length} 张报表直接引用,并被 ${metricUsage.length} 个可复用指标依赖。\n\n## 用途边界\n\n- 认证状态:${Boolean(item.authenticated)}。\n- ${text(item.title) ? '名称来自资产包,已清理为 agent 可读名称。' : '资产包没有业务标题,当前仅展示技术 resource key,不补写业务名称。'}\n- 该页只概括资产包中的技术定义,不补写业务口径。\n- 精确规则、数据类型和认证状态以原始元数据页为准。\n\n## 来源\n\n- [原始元数据定义](../../../metadata/${metadataDirectory(item.resource_type)}/${metadataFiles.get(id)})\n`;
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
function renderDomainBrief(domain) {
|
|
1659
|
+
const key = text(domain.domain_id);
|
|
1660
|
+
const dashboardRefs = array(domain.dashboard_refs);
|
|
1661
|
+
const reportRefs = array(domain.report_refs);
|
|
1662
|
+
return `${briefFrontmatter('ai-business-domain-brief', { domain_id: key }, 'high')}\n# ${heading(domain.title)}\n\n> ${text(domain.summary)}\n\n## 主要解决的问题\n\n${array(domain.primary_questions).map((question) => `- ${question}`).join('\n')}\n\n## 资产组成\n\n- 看板:${dashboardRefs.length} 张\n- 涉及报表:${reportRefs.length} 张\n- 物理来源容器:${array(domain.source_container_refs).length} 个\n\n${dashboardRefs.length ? `代表看板:\n${refLinks(dashboardRefs.slice(0, 12), 'dashboard_id', 'dashboard_name', dashboardFiles, '../dashboards')}` : ''}\n\n## 边界\n\n- 该业务域是 Agent 基于同快照资产证据生成的知识组织层,不等同于看板空间,也不表示业务认证。\n- 精确指标、过滤条件和计算口径必须继续下钻到报表或指标源资料。\n\n## 来源\n\n- [业务域规划与完整资产清单](../../domains/${domainFiles.get(key)})\n`;
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1665
|
+
function renderDashboardBrief(dashboard) {
|
|
1666
|
+
const id = text(dashboard.resource_key);
|
|
1667
|
+
const domain = domainByDashboardId.get(id);
|
|
1668
|
+
const appendix = appendixByDashboardId.get(id);
|
|
1669
|
+
const refs = array(dashboard.report_refs);
|
|
1670
|
+
const reportRows = refs.map((ref) => reportById.get(text(ref.report_id))).filter(Boolean);
|
|
1671
|
+
const notes = array(dashboard.notes);
|
|
1672
|
+
const themes = topNames(reportRows.map((row) => row.title), 6);
|
|
1673
|
+
const classification = domain
|
|
1674
|
+
? { fields: { dashboard_id: id, domain_id: domain.domain_id, authenticated: Boolean(dashboard.authenticated) }, confidence: 'medium', sentence: `属于“${text(domain.title)}”业务域` }
|
|
1675
|
+
: { fields: { dashboard_id: id, appendix_id: appendix.appendix_id, authenticated: Boolean(dashboard.authenticated) }, confidence: 'low', sentence: `收录在“${text(appendix.title)}”技术附录,不作为主要业务域资产` };
|
|
1676
|
+
return `${briefFrontmatter('ai-dashboard-brief', classification.fields, classification.confidence)}\n# ${heading(dashboard.title || `Dashboard ${id}`)}\n\n> 这张看板${classification.sentence},共承载 ${refs.length} 张报表${themes ? `,主要覆盖${themes}` : ''}${notes.length ? `,并包含 ${notes.length} 条看板便签说明` : ''}。\n\n## 主要回答的问题\n\n${reportRows.slice(0, 12).map((row) => `- ${reportPurpose(row)}`).join('\n') || '- 当前看板没有关联报表,无法从资产定义归纳具体分析问题。'}\n\n## 看板便签说明\n\n${notes.map((value, index) => dashboardNoteSummary(value, index)).join('\n') || '- 当前看板没有便签。'}\n\n## 报表结构\n\n| 报表 | 主要作用 |\n| --- | --- |\n${reportRows.map((row) => `| [${table(row.title || row.resource_key)}](../reports/${reportFiles.get(text(row.resource_key))}) | ${table(reportPurpose(row))} |`).join('\n')}\n\n## 使用边界\n\n- 看板便签用于解释业务目的、使用顺序和注意事项,但不能覆盖报表中的精确指标、筛选和时间口径。\n- 看板主题由名称、便签和下属报表结构概括,精确计算仍以报表源页为准。\n- [原始看板定义](../../dashboards/${dashboardFiles.get(id)})\n`;
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
function renderDashboardNotes(values) {
|
|
1680
|
+
if (!values.length) return '_该看板没有便签。_';
|
|
1681
|
+
return values.map((value, index) => {
|
|
1682
|
+
const note = object(value);
|
|
1683
|
+
const title = dashboardNoteText(note.note_title) || `便签 ${index + 1}`;
|
|
1684
|
+
const description = dashboardNoteText(note.description);
|
|
1685
|
+
const body = description
|
|
1686
|
+
? description.split(/\r?\n/).map((line) => `> ${line || ' '}`).join('\n')
|
|
1687
|
+
: '> _便签没有正文。_';
|
|
1688
|
+
return `### ${index + 1}. ${heading(title)}\n\n- Note ID: \`${text(note.note_id) || 'unknown'}\`\n- 更新时间: ${text(note.updated_at) || 'unknown'}\n\n${body}`;
|
|
1689
|
+
}).join('\n\n');
|
|
1690
|
+
}
|
|
1691
|
+
|
|
1692
|
+
function dashboardNoteSummary(value, index) {
|
|
1693
|
+
const note = object(value);
|
|
1694
|
+
const title = dashboardNoteText(note.note_title) || `便签 ${index + 1}`;
|
|
1695
|
+
const description = dashboardNoteText(note.description).replace(/\s+/g, ' ').trim();
|
|
1696
|
+
const summary = description.length > 240 ? `${description.slice(0, 240)}…` : description;
|
|
1697
|
+
return `- ${title}${summary ? `:${summary}` : ':没有正文。'}`;
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
function dashboardNoteText(value) {
|
|
1701
|
+
return text(value)
|
|
1702
|
+
.replace(/<br\s*\/?>/gi, '\n')
|
|
1703
|
+
.replace(/<li(?:\s[^>]*)?>/gi, '\n- ')
|
|
1704
|
+
.replace(/<\/(?:p|div|li|ul|ol|h[1-6]|custom-h[1-6])>/gi, '\n')
|
|
1705
|
+
.replace(/<[^>]+>/g, '')
|
|
1706
|
+
.replace(/ | /gi, ' ')
|
|
1707
|
+
.replace(/&/gi, '&')
|
|
1708
|
+
.replace(/</gi, '<')
|
|
1709
|
+
.replace(/>/gi, '>')
|
|
1710
|
+
.replace(/"/gi, '"')
|
|
1711
|
+
.replace(/'|'/gi, "'")
|
|
1712
|
+
.replace(/&#(\d+);/g, (_, code) => String.fromCodePoint(Number(code)))
|
|
1713
|
+
.replace(/&#x([0-9a-f]+);/gi, (_, code) => String.fromCodePoint(Number.parseInt(code, 16)))
|
|
1714
|
+
.split(/\r?\n/)
|
|
1715
|
+
.map((line) => cleanQuestionMarkPlaceholders(line).trim())
|
|
1716
|
+
.filter(Boolean)
|
|
1717
|
+
.join('\n');
|
|
1718
|
+
}
|
|
1719
|
+
|
|
1720
|
+
function renderReportBrief(report) {
|
|
1721
|
+
const id = text(report.resource_key);
|
|
1722
|
+
const conflicts = reportConflicts.get(id) ?? [];
|
|
1723
|
+
const confidence = conflicts.length ? 'low'
|
|
1724
|
+
: array(report.metric_occurrences).length || report.definition_kind === 'SQL' ? 'medium' : 'low';
|
|
1725
|
+
const extra = conflicts.length ? { review_status: 'semantic_conflict' } : {};
|
|
1726
|
+
const occurrences = array(report.metric_occurrences);
|
|
1727
|
+
const groups = array(object(report.calculation_context).group_by);
|
|
1728
|
+
const metadataRefs = metadataRefsForReport(report);
|
|
1729
|
+
const reportAppendices = appendicesForReport(report);
|
|
1730
|
+
return `${briefFrontmatter('ai-report-brief', {
|
|
1731
|
+
report_id: id, authenticated: Boolean(report.authenticated), ...extra,
|
|
1732
|
+
}, confidence)}\n# ${heading(report.title || `Report ${id}`)}\n\n> ${reportPurpose(report)}\n\n## 主要解决的问题\n\n- 认证状态:${Boolean(report.authenticated)}${report.authenticated === true ? '。' : ';该报表作为认证看板的依赖被保留,并未单独认证。'}\n- ${reportAppendices.length ? `该报表仅归入技术附录“${reportAppendices.map((item) => item.title).join('、')}”,不作为主要业务域资产。` : '该报表由主要业务域看板承载。'}\n- ${report.definition_kind === 'SQL' ? '通过 SQL 明细或汇总查询定位具体对象、记录或排行。' : `观察${occurrences.map(occurrenceLabel).filter(Boolean).join('、') || text(report.title)}的规模、趋势或分布。`}\n- ${groups.length ? `支持按 ${groups.map(groupLabel).filter(Boolean).join('、')} 分组比较。` : '资产定义未暴露明确分组维度,消费时需按源定义确认。'}\n\n## 指标与口径\n\n${occurrences.map((item) => `- ${occurrenceSentence(item)}`).join('\n') || (report.definition_kind === 'SQL' ? '- 口径由 SQL 和参数决定,详见原始报表定义。' : '- 未识别到结构化指标项。')}\n\n## 引用元数据\n\n${metadataLinks(metadataRefs, '../../metadata')}\n\n${conflicts.length ? `## 语义冲突\n\n${conflicts.map((item) => `- ${item}`).join('\n')}\n\n在资产作者或业务域召回卡确认前,不应仅凭名称把该资产作为确定查询入口。\n\n` : ''}## 来源\n\n- [原始报表定义](../../reports/${reportFiles.get(id)})\n`;
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1735
|
+
function renderMetricBrief(metric) {
|
|
1736
|
+
const id = text(metric.resource_key);
|
|
1737
|
+
const occurrences = array(metric.metric_occurrences);
|
|
1738
|
+
const metadataRefs = metadataRefsForMetric(metric);
|
|
1739
|
+
return `${briefFrontmatter('ai-metric-brief', {
|
|
1740
|
+
metric_key: id, authenticated: Boolean(metric.authenticated),
|
|
1741
|
+
}, occurrences.length ? 'medium' : 'low')}\n# ${heading(metric.title || `Metric ${id}`)}\n\n> 该可复用指标用于统一表达“${text(metric.title || id)}”${occurrences.length ? `,其核心计算基于${occurrences.map(occurrenceLabel).filter(Boolean).join('、')}` : ''}。\n\n## 主要解决的问题\n\n- 认证状态:${Boolean(metric.authenticated)}。\n- 在多个分析资产之间复用同一指标定义,减少临时口径差异。\n- 明确依赖事件、属性和参数,便于知识查询后继续核对计算证据。\n\n## 计算口径\n\n${occurrences.map((item) => `- ${occurrenceSentence(item)}`).join('\n') || '- 未识别到结构化指标项,需查看原始定义。'}\n\n## 引用元数据\n\n${metadataLinks(metadataRefs, '../../metadata')}\n\n## 来源\n\n- [原始指标定义](../../metrics/${metricFiles.get(id)})\n`;
|
|
1742
|
+
}
|
|
1743
|
+
|
|
1744
|
+
function reportPurpose(report) {
|
|
1745
|
+
const title = text(report.title || report.resource_key);
|
|
1746
|
+
const occurrences = array(report.metric_occurrences);
|
|
1747
|
+
if (report.definition_kind === 'SQL') return `通过 SQL 查询“${title}”相关明细、汇总或排行,具体范围由 SQL 参数决定。`;
|
|
1748
|
+
if (occurrences.length) return eventAnalysisReportPurpose(report, title, occurrences);
|
|
1749
|
+
return `查看“${title}”相关结果;资产包未识别到可直接概括的指标项,Agent 需要回到报表源定义核验用途。`;
|
|
1750
|
+
}
|
|
1751
|
+
|
|
1752
|
+
function eventAnalysisReportPurpose(report, title, occurrences) {
|
|
1753
|
+
const eventNames = topDomainFacts(occurrences.map(metricEventLabel).filter(Boolean), 3);
|
|
1754
|
+
const metricNames = topDomainFacts(occurrences.map(occurrenceLabel).filter(Boolean), 4);
|
|
1755
|
+
const aggregationNames = topDomainFacts(occurrences.map(aggregationLabel).filter(Boolean), 3);
|
|
1756
|
+
const measureNames = topDomainFacts(occurrences.map((item) => readableMetricText(item.measure_title || item.measure_key)).filter(Boolean), 3);
|
|
1757
|
+
const groups = topDomainFacts(array(object(report.calculation_context).group_by)
|
|
1758
|
+
.map(groupLabel)
|
|
1759
|
+
.filter(isUsefulDomainDimensionFact), 4);
|
|
1760
|
+
const filters = reportFilterSummaryItems(report, object(report.calculation_context)).slice(0, 3);
|
|
1761
|
+
const subject = metricNames.length ? metricNames.join('、') : title;
|
|
1762
|
+
const basis = eventNames.length ? `基于${eventNames.map((name) => eventBasisLabel(name)).join('、')}` : '基于事件分析模型';
|
|
1763
|
+
const calculation = [
|
|
1764
|
+
aggregationNames.length ? `按“${aggregationNames.join('、')}”统计` : '统计',
|
|
1765
|
+
measureNames.length ? `度量字段为“${measureNames.join('、')}”` : '',
|
|
1766
|
+
].filter(Boolean).join(',');
|
|
1767
|
+
const suffix = [
|
|
1768
|
+
groups.length ? `可按 ${groups.join('、')} 下钻` : '',
|
|
1769
|
+
filters.length ? `过滤 ${filters.join(';')}` : '',
|
|
1770
|
+
].filter(Boolean).join(';');
|
|
1771
|
+
return `${basis}${calculation ? `${calculation}` : ''}“${subject}”${suffix ? `;${suffix}` : ''}。`;
|
|
1772
|
+
}
|
|
1773
|
+
|
|
1774
|
+
|
|
1775
|
+
function eventBasisLabel(value) {
|
|
1776
|
+
const label = text(value);
|
|
1777
|
+
if (!label) return '';
|
|
1778
|
+
return /事件$/.test(label) ? `“${label}”` : `“${label}”事件`;
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1781
|
+
function detectSemanticConflicts() {
|
|
1782
|
+
const reportConflicts = new Map();
|
|
1783
|
+
const metadataConflicts = new Map();
|
|
1784
|
+
for (const [key, values] of groupBySemanticKey(reports, reportSemanticKey)) {
|
|
1785
|
+
if (values.length < 2) continue;
|
|
1786
|
+
const signatures = new Set(values.map(reportSemanticSignature));
|
|
1787
|
+
if (signatures.size <= 1) continue;
|
|
1788
|
+
const summary = semanticConflictSummary('报表', key, values, (row) => reportSemanticSignatureLabel(row));
|
|
1789
|
+
for (const report of values) appendConflict(reportConflicts, text(report.resource_key), summary);
|
|
1790
|
+
}
|
|
1791
|
+
const comparableMetadata = metadata.filter((item) => ['event_prop', 'user_prop', 'tag', 'user_tag', 'cluster', 'user_cluster', 'metric'].includes(text(item.resource_type)));
|
|
1792
|
+
for (const [key, values] of groupBySemanticKey(comparableMetadata, metadataSemanticKey)) {
|
|
1793
|
+
if (values.length < 2) continue;
|
|
1794
|
+
const signatures = new Set(values.map(metadataSemanticSignature));
|
|
1795
|
+
if (signatures.size <= 1) continue;
|
|
1796
|
+
const summary = semanticConflictSummary('元数据', key, values, (row) => metadataSemanticSignatureLabel(row));
|
|
1797
|
+
for (const item of values) appendConflict(metadataConflicts, text(item.evidence_id), summary);
|
|
1798
|
+
}
|
|
1799
|
+
return { reportConflicts, metadataConflicts };
|
|
1800
|
+
}
|
|
1801
|
+
|
|
1802
|
+
function groupBySemanticKey(values, keyFn) {
|
|
1803
|
+
const result = new Map();
|
|
1804
|
+
for (const value of values) {
|
|
1805
|
+
const key = keyFn(value);
|
|
1806
|
+
if (!key) continue;
|
|
1807
|
+
if (!result.has(key)) result.set(key, []);
|
|
1808
|
+
result.get(key).push(value);
|
|
1809
|
+
}
|
|
1810
|
+
return result;
|
|
1811
|
+
}
|
|
1812
|
+
|
|
1813
|
+
function appendConflict(target, id, message) {
|
|
1814
|
+
if (!id) return;
|
|
1815
|
+
if (!target.has(id)) target.set(id, []);
|
|
1816
|
+
target.get(id).push(message);
|
|
1817
|
+
}
|
|
1818
|
+
|
|
1819
|
+
function semanticConflictSummary(kind, key, values, signatureLabelFn) {
|
|
1820
|
+
const rows = values.map((row) => `${assetDisplayName(row)}(${assetIdentity(row)},${signatureLabelFn(row)})`);
|
|
1821
|
+
return `语义“${key}”存在 ${values.length} 个${kind}候选,但口径不同:${rows.join(';')}。Agent 不应仅凭名称选择,需要优先使用业务域/召回卡或让用户确认。`;
|
|
1822
|
+
}
|
|
1823
|
+
|
|
1824
|
+
function reportSemanticKey(report) {
|
|
1825
|
+
return normalizeSemanticIntent(text(report.title)) || normalizeSemanticIntent(array(report.metric_occurrences).map(occurrenceLabel).join(' '));
|
|
1826
|
+
}
|
|
1827
|
+
|
|
1828
|
+
function metadataSemanticKey(item) {
|
|
1829
|
+
return normalizeMetadataSemanticIntent(text(item.title || item.summary || item.description));
|
|
1830
|
+
}
|
|
1831
|
+
|
|
1832
|
+
function normalizeMetadataSemanticIntent(value) {
|
|
1833
|
+
let normalized = text(value).toLowerCase().normalize('NFKC');
|
|
1834
|
+
normalized = normalized
|
|
1835
|
+
.replace(/正式客户|真实客户|有效客户/g, '真实正式客户')
|
|
1836
|
+
.replace(/\busers?\b/g, '用户')
|
|
1837
|
+
.replace(/公司|企业/g, '客户')
|
|
1838
|
+
.replace(/[\s_\-—–、,,。\.::;;()()【】\[\]{}<>《》“”"'`/\\]+/g, '')
|
|
1839
|
+
.replace(/\//g, '');
|
|
1840
|
+
return normalized.length >= 2 ? normalized : '';
|
|
1841
|
+
}
|
|
1842
|
+
|
|
1843
|
+
function normalizeSemanticIntent(value) {
|
|
1844
|
+
let normalized = text(value).toLowerCase().normalize('NFKC');
|
|
1845
|
+
normalized = normalized
|
|
1846
|
+
.replace(/\bdau\b|daily\s*active\s*users?|日活跃用户|日活用户|活跃用户数|活跃用户/g, '用户日活')
|
|
1847
|
+
.replace(/正式客户|真实客户|有效客户/g, '真实正式客户')
|
|
1848
|
+
.replace(/\busers?\b/g, '用户')
|
|
1849
|
+
.replace(/公司|企业/g, '客户')
|
|
1850
|
+
.replace(/报表|看板|图表|趋势|分布|情况|统计|查询|分析/g, '')
|
|
1851
|
+
.replace(/[\s_\-—–、,,。\.::;;()()【】\[\]{}<>《》“”"'`/\\]+/g, '')
|
|
1852
|
+
.replace(/\//g, '')
|
|
1853
|
+
.replace(/用户用户日活/g, '用户日活');
|
|
1854
|
+
return normalized.length >= 2 ? normalized : '';
|
|
1855
|
+
}
|
|
1856
|
+
|
|
1857
|
+
function reportSemanticSignature(report) {
|
|
1858
|
+
if (text(report.definition_kind) === 'SQL') {
|
|
1859
|
+
return JSON.stringify({ kind: 'SQL', sql: normalizeSql(text(object(report.calculation_context).sql)) });
|
|
1860
|
+
}
|
|
1861
|
+
return JSON.stringify({
|
|
1862
|
+
kind: text(report.definition_kind) || text(report.report_model),
|
|
1863
|
+
metrics: array(report.metric_occurrences).map(metricSemanticSignature).sort(),
|
|
1864
|
+
filters: filterSummaryLines(object(report.calculation_context).filters).sort(),
|
|
1865
|
+
groups: array(object(report.calculation_context).group_by).map(groupLabel).sort(),
|
|
1866
|
+
});
|
|
1867
|
+
}
|
|
1868
|
+
|
|
1869
|
+
function metricSemanticSignature(item) {
|
|
1870
|
+
return JSON.stringify({
|
|
1871
|
+
event_name: text(item.event_name),
|
|
1872
|
+
aggregation_code: text(item.aggregation_code),
|
|
1873
|
+
measure_key: text(item.measure_key),
|
|
1874
|
+
formula_expression: text(item.formula_expression),
|
|
1875
|
+
filters: filterSummaryLines(item.filters ?? item.custom_filters).sort(),
|
|
1876
|
+
});
|
|
1877
|
+
}
|
|
1878
|
+
|
|
1879
|
+
function reportSemanticSignatureLabel(report) {
|
|
1880
|
+
if (text(report.definition_kind) === 'SQL') return 'SQL 口径';
|
|
1881
|
+
const metrics = array(report.metric_occurrences).map((item) => {
|
|
1882
|
+
const event = text(item.event_title || item.event_name) || '未知事件';
|
|
1883
|
+
const aggregation = aggregationLabel(item) || '未知聚合';
|
|
1884
|
+
const measure = text(item.measure_title || item.measure_key);
|
|
1885
|
+
return `${event}/${aggregation}${measure ? `/${measure}` : ''}`;
|
|
1886
|
+
});
|
|
1887
|
+
return metrics.slice(0, 4).join('、') || '未识别指标口径';
|
|
1888
|
+
}
|
|
1889
|
+
|
|
1890
|
+
function metadataSemanticSignature(item) {
|
|
1891
|
+
const definition = object(item.definition);
|
|
1892
|
+
return JSON.stringify({
|
|
1893
|
+
resource_type: text(item.resource_type),
|
|
1894
|
+
resource_key: text(item.resource_key),
|
|
1895
|
+
table_type: text(definition.table_type),
|
|
1896
|
+
select_type: text(definition.select_type),
|
|
1897
|
+
source_type: text(definition.source_type),
|
|
1898
|
+
tag_type: text(definition.tag_type || definition.tag_sub_type),
|
|
1899
|
+
});
|
|
1900
|
+
}
|
|
1901
|
+
|
|
1902
|
+
function metadataSemanticSignatureLabel(item) {
|
|
1903
|
+
const definition = object(item.definition);
|
|
1904
|
+
return `${metadataTypeLabel(item.resource_type)} ${text(item.resource_key)}${text(definition.table_type) ? ` table_type=${text(definition.table_type)}` : ''}`;
|
|
1905
|
+
}
|
|
1906
|
+
|
|
1907
|
+
function assetDisplayName(row) {
|
|
1908
|
+
return `“${text(row.title || row.resource_key || row.evidence_id)}”`;
|
|
1909
|
+
}
|
|
1910
|
+
|
|
1911
|
+
function assetIdentity(row) {
|
|
1912
|
+
if (text(row.resource_key)) return `id=${text(row.resource_key)}`;
|
|
1913
|
+
return `evidence=${text(row.evidence_id)}`;
|
|
1914
|
+
}
|
|
1915
|
+
|
|
1916
|
+
function collectFilterValues(value) {
|
|
1917
|
+
if (Array.isArray(value)) return value.flatMap(collectFilterValues);
|
|
1918
|
+
if (!value || typeof value !== 'object') return [];
|
|
1919
|
+
return [...array(value.ftv).map(text).filter(Boolean),
|
|
1920
|
+
...Object.entries(value).filter(([key]) => key !== 'ftv').flatMap(([, item]) => collectFilterValues(item))];
|
|
1921
|
+
}
|
|
1922
|
+
|
|
1923
|
+
function renderOccurrence(item, index) {
|
|
1924
|
+
const expression = formulaExpression(item);
|
|
1925
|
+
const formulaSummary = formulaDefinitionSummary(item);
|
|
1926
|
+
const filterSummary = filterSummaryLines(item.filters || item.custom_filters);
|
|
1927
|
+
const event = metricEventLabel(item) || 'unknown';
|
|
1928
|
+
return `### ${index + 1}. ${heading(occurrenceLabel(item) || `Metric ${index + 1}`)}
|
|
1929
|
+
|
|
1930
|
+
- 事件: ${event}
|
|
1931
|
+
- 复用指标 key: ${text(item.metric_key) ? `\`${text(item.metric_key)}\`` : '无'}
|
|
1932
|
+
- 聚合方式: ${aggregationLabel(item) || '未说明'}
|
|
1933
|
+
- 度量字段: ${readableMetricText(item.measure_title || item.measure_key) || '无'}
|
|
1934
|
+
- 公式表达式: ${expression ? `\`${expression}\`` : '无'}
|
|
1935
|
+
- 公式依赖: ${formulaSummary || '无'}
|
|
1936
|
+
- 过滤条件: ${filterSummary.length ? filterSummary.join(';') : '无'}
|
|
1937
|
+
`;
|
|
1938
|
+
}
|
|
1939
|
+
|
|
1940
|
+
function occurrenceSentence(item) {
|
|
1941
|
+
const label = occurrenceLabel(item) || '未命名指标';
|
|
1942
|
+
const expression = formulaExpression(item);
|
|
1943
|
+
const dependencies = formulaDependencies(item);
|
|
1944
|
+
if (expression || dependencies.length) {
|
|
1945
|
+
return `${label}:公式指标${expression ? `,表达式为 \`${expression}\`` : ''}${dependencies.length ? `,依赖${dependencies.join('、')}` : ''}。`;
|
|
1946
|
+
}
|
|
1947
|
+
const event = metricEventLabel(item) || '未知事件';
|
|
1948
|
+
const aggregation = aggregationLabel(item) || '未说明聚合';
|
|
1949
|
+
const measure = readableMetricText(item.measure_title || item.measure_key);
|
|
1950
|
+
return `${label}:基于“${event}”按“${aggregation}”计算${measure ? `,度量字段为“${measure}”` : ''}${text(item.metric_key) ? `,复用指标 key 为 \`${text(item.metric_key)}\`` : ''}。`;
|
|
1951
|
+
}
|
|
1952
|
+
|
|
1953
|
+
function formulaExpression(item) {
|
|
1954
|
+
const expression = text(item.formula_expression) || (typeof item.formula === 'string' ? text(item.formula) : '');
|
|
1955
|
+
return readableFormulaExpression(expression);
|
|
1956
|
+
}
|
|
1957
|
+
|
|
1958
|
+
function readableFormulaExpression(value) {
|
|
1959
|
+
return text(value).replace(/\.?(A\d{3}(?:_\d+)?)/g, (_, code) => {
|
|
1960
|
+
const label = aggregationCodeLabel(code);
|
|
1961
|
+
return label ? `.${label}` : '.未知聚合';
|
|
1962
|
+
});
|
|
1963
|
+
}
|
|
1964
|
+
|
|
1965
|
+
function formulaDefinition(item) {
|
|
1966
|
+
const definition = object(item.formula_definition);
|
|
1967
|
+
return Object.keys(definition).length ? definition : object(item.formula);
|
|
1968
|
+
}
|
|
1969
|
+
|
|
1970
|
+
function formulaDependencies(item) {
|
|
1971
|
+
return array(formulaDefinition(item).formulationDeps).map((dependency) => {
|
|
1972
|
+
const event = object(object(dependency).event);
|
|
1973
|
+
const quota = object(object(dependency).quota);
|
|
1974
|
+
const eventName = text(event.eventDesc || event.eventName) || '未知事件';
|
|
1975
|
+
const aggregation = text(quota.quotaDesc || quota.quotaName) || '未说明聚合';
|
|
1976
|
+
const filters = array(object(event.filter).filts)
|
|
1977
|
+
.map((filter) => text(object(filter).columnDesc || object(filter).quotDesc || object(filter).prop))
|
|
1978
|
+
.filter(Boolean);
|
|
1979
|
+
return `“${eventName}”的“${aggregation}”${filters.length ? `(过滤:${filters.join('、')})` : ''}`;
|
|
1980
|
+
});
|
|
1981
|
+
}
|
|
1982
|
+
|
|
1983
|
+
|
|
1984
|
+
function formulaDefinitionSummary(item) {
|
|
1985
|
+
const dependencies = formulaDependencies(item);
|
|
1986
|
+
if (dependencies.length) return dependencies.join('、');
|
|
1987
|
+
const definition = formulaDefinition(item);
|
|
1988
|
+
const event = object(definition.event);
|
|
1989
|
+
const quota = object(definition.quota);
|
|
1990
|
+
const eventName = text(event.eventDesc || event.eventName || definition.eventDesc || definition.eventName);
|
|
1991
|
+
const quotaName = text(quota.quotaDesc || quota.quotaName || definition.quotaDesc || definition.quotaName);
|
|
1992
|
+
const metricName = text(definition.metricName ?? definition.metric_name);
|
|
1993
|
+
return [
|
|
1994
|
+
eventName ? `事件“${eventName}”` : '',
|
|
1995
|
+
quotaName ? `指标“${quotaName}”` : '',
|
|
1996
|
+
metricName && metricName !== quotaName ? `名称“${metricName}”` : '',
|
|
1997
|
+
].filter(Boolean).join(',');
|
|
1998
|
+
}
|
|
1999
|
+
|
|
2000
|
+
function aggregationLabel(item) {
|
|
2001
|
+
const name = text(item.aggregation_name);
|
|
2002
|
+
const code = text(item.aggregation_code);
|
|
2003
|
+
const codeLabel = aggregationCodeLabel(code);
|
|
2004
|
+
if (name && !isAggregationCode(name)) return normalizeAggregationName(name);
|
|
2005
|
+
if (codeLabel) return codeLabel;
|
|
2006
|
+
return name || (code ? '未知聚合' : '');
|
|
2007
|
+
}
|
|
2008
|
+
|
|
2009
|
+
function normalizeAggregationName(value) {
|
|
2010
|
+
const name = text(value);
|
|
2011
|
+
return ({
|
|
2012
|
+
'Total count': '总次数',
|
|
2013
|
+
'Total Count': '总次数',
|
|
2014
|
+
'Unique users': '触发用户数',
|
|
2015
|
+
'Unique Users': '触发用户数',
|
|
2016
|
+
Sum: '总和',
|
|
2017
|
+
Average: '均值',
|
|
2018
|
+
Avg: '均值',
|
|
2019
|
+
Max: '最大值',
|
|
2020
|
+
Min: '最小值',
|
|
2021
|
+
Distinct: '去重数',
|
|
2022
|
+
})[name] ?? name;
|
|
2023
|
+
}
|
|
2024
|
+
|
|
2025
|
+
function aggregationCodeLabel(value) {
|
|
2026
|
+
const code = text(value);
|
|
2027
|
+
return ({
|
|
2028
|
+
A100: '总次数',
|
|
2029
|
+
A100_1: '总次数的周期累计总和',
|
|
2030
|
+
A100_2: '总次数的周期累计人均值',
|
|
2031
|
+
A101: '触发用户数',
|
|
2032
|
+
A101_1: '触发用户数的周期累计总和',
|
|
2033
|
+
A101_2: '触发用户数的周期累计人均值',
|
|
2034
|
+
A102: '人均次数',
|
|
2035
|
+
A103: '总和',
|
|
2036
|
+
A104: '均值',
|
|
2037
|
+
A105: '人均值',
|
|
2038
|
+
A106: '最大值',
|
|
2039
|
+
A107: '最小值',
|
|
2040
|
+
A108: '去重数',
|
|
2041
|
+
A109: '为真数',
|
|
2042
|
+
A110: '为假数',
|
|
2043
|
+
A111: '不为空数',
|
|
2044
|
+
A112: '为空数',
|
|
2045
|
+
A113: '周期累计总和',
|
|
2046
|
+
A114: '周期累计人均值',
|
|
2047
|
+
A115: '列表整体去重数',
|
|
2048
|
+
A116: '列表元素去重数',
|
|
2049
|
+
A117: '中位数',
|
|
2050
|
+
A118: '元素集合去重数',
|
|
2051
|
+
A119: '分位数',
|
|
2052
|
+
A120: '方差',
|
|
2053
|
+
A121: '标准差',
|
|
2054
|
+
A200: '次数',
|
|
2055
|
+
A201: '天数',
|
|
2056
|
+
A202: '小时数',
|
|
2057
|
+
})[code] ?? '';
|
|
2058
|
+
}
|
|
2059
|
+
|
|
2060
|
+
function isAggregationCode(value) {
|
|
2061
|
+
return /^A\d{3}(?:_\d+)?$/.test(text(value));
|
|
2062
|
+
}
|
|
2063
|
+
|
|
2064
|
+
function filterSummaryLines(value) {
|
|
2065
|
+
const conditions = collectFilterConditions(value);
|
|
2066
|
+
return [...new Set(conditions.map(filterConditionSentence).filter(Boolean))];
|
|
2067
|
+
}
|
|
2068
|
+
|
|
2069
|
+
function collectFilterConditions(value) {
|
|
2070
|
+
if (Array.isArray(value)) return value.flatMap(collectFilterConditions);
|
|
2071
|
+
if (!value || typeof value !== 'object') return [];
|
|
2072
|
+
const source = object(value);
|
|
2073
|
+
const nested = Object.entries(source)
|
|
2074
|
+
.filter(([key]) => !['ftv', 'columnDesc', 'columnName', 'quotDesc', 'prop', 'calcuSymbol', 'calcu_symbol', 'relation', 'junctionKind', 'junction_kind'].includes(key))
|
|
2075
|
+
.flatMap(([, item]) => collectFilterConditions(item));
|
|
2076
|
+
const hasConditionIdentity = ['columnDesc', 'columnName', 'quotDesc', 'prop', 'propertyName', 'tagName'].some((key) => text(source[key]))
|
|
2077
|
+
|| array(source.ftv).some((item) => text(item));
|
|
2078
|
+
return hasConditionIdentity ? [source, ...nested] : nested;
|
|
2079
|
+
}
|
|
2080
|
+
|
|
2081
|
+
function filterConditionSentence(condition) {
|
|
2082
|
+
const field = text(condition.columnDesc || condition.quotDesc || condition.columnName || condition.prop || condition.propertyName || condition.tagName) || '未命名字段';
|
|
2083
|
+
const values = array(condition.ftv).map(text).filter(Boolean);
|
|
2084
|
+
const code = text(condition.calcuSymbol ?? condition.calcu_symbol);
|
|
2085
|
+
const operator = filterOperatorLabel(code);
|
|
2086
|
+
const relation = text(condition.relation || condition.junctionKind || condition.junction_kind);
|
|
2087
|
+
const needsValue = filterOperatorNeedsValue(code);
|
|
2088
|
+
const valueText = values.length ? values.map((value) => `“${value}”`).join('、') : needsValue ? '未给出固定值' : '';
|
|
2089
|
+
const valuePart = valueText ? ` ${valueText}` : '';
|
|
2090
|
+
const relationText = relation ? `(关系:${relation})` : '';
|
|
2091
|
+
return `${field} ${operator}${valuePart}${relationText}`;
|
|
2092
|
+
}
|
|
2093
|
+
|
|
2094
|
+
function filterOperatorLabel(value) {
|
|
2095
|
+
const code = text(value);
|
|
2096
|
+
return ({
|
|
2097
|
+
C00: '等于',
|
|
2098
|
+
C01: '不等于',
|
|
2099
|
+
C02: '小于',
|
|
2100
|
+
C020: '小于等于',
|
|
2101
|
+
C03: '大于',
|
|
2102
|
+
C030: '大于等于',
|
|
2103
|
+
C04: '有值',
|
|
2104
|
+
C05: '无值',
|
|
2105
|
+
C06: '区间',
|
|
2106
|
+
C07: '包括',
|
|
2107
|
+
C08: '不包括',
|
|
2108
|
+
C09: '为真',
|
|
2109
|
+
C10: '为假',
|
|
2110
|
+
C11: '正则匹配',
|
|
2111
|
+
C12: '正则不匹配',
|
|
2112
|
+
C13: '相对当前时间',
|
|
2113
|
+
C14: '相对事件时间',
|
|
2114
|
+
C15: '存在元素',
|
|
2115
|
+
C16: '不存在元素',
|
|
2116
|
+
C17: '元素位置',
|
|
2117
|
+
C18: '无值',
|
|
2118
|
+
C19: '有值',
|
|
2119
|
+
C20: '属于分群',
|
|
2120
|
+
C21: '不属于分群',
|
|
2121
|
+
C22: '存在对象满足',
|
|
2122
|
+
C23: '没有对象满足',
|
|
2123
|
+
C24: '全部对象满足',
|
|
2124
|
+
})[code] ?? (code ? '未知操作符' : '满足');
|
|
2125
|
+
}
|
|
2126
|
+
|
|
2127
|
+
function filterOperatorNeedsValue(value) {
|
|
2128
|
+
const code = text(value);
|
|
2129
|
+
return !new Set(['', 'C04', 'C05', 'C09', 'C10', 'C18', 'C19']).has(code);
|
|
2130
|
+
}
|
|
2131
|
+
|
|
2132
|
+
function occurrenceLabel(item) { return readableMetricText(item.display_name || item.event_title || item.event_name); }
|
|
2133
|
+
|
|
2134
|
+
function metricEventLabel(item) {
|
|
2135
|
+
const title = readableMetricText(item.event_title);
|
|
2136
|
+
const name = readableMetricText(item.event_name);
|
|
2137
|
+
const display = readableMetricText(item.display_name);
|
|
2138
|
+
if (title && !isInternalMetricExpression(title)) return title;
|
|
2139
|
+
if (name && !isInternalMetricExpression(name)) return name;
|
|
2140
|
+
if (display === '自定义指标') return '自定义指标';
|
|
2141
|
+
return display || title || name;
|
|
2142
|
+
}
|
|
2143
|
+
|
|
2144
|
+
function isInternalMetricExpression(value) {
|
|
2145
|
+
return /(?:^|[、.])dashboard_search(?:[、.]|$)|(?:^|[、.])event_view(?:[、.]|$)|(?:^|[、.])undefined(?:[、.]|$)/i.test(text(value));
|
|
2146
|
+
}
|
|
2147
|
+
|
|
2148
|
+
function readableMetricText(value) {
|
|
2149
|
+
return text(value)
|
|
2150
|
+
.replace(/undefined[._-]*/gi, '')
|
|
2151
|
+
.replace(/\.?(A\d{3}(?:_\d+)?)/g, (_, code) => {
|
|
2152
|
+
const label = aggregationCodeLabel(code);
|
|
2153
|
+
return label ? `.${label}` : '.未知聚合';
|
|
2154
|
+
})
|
|
2155
|
+
.replace(/([\u4e00-\u9fa5])\.([A-Za-z_])/g, '$1、$2')
|
|
2156
|
+
.replace(/([A-Za-z_])\.([\u4e00-\u9fa5])/g, '$1、$2')
|
|
2157
|
+
.replace(/([\u4e00-\u9fa5])\.([\u4e00-\u9fa5])/g, '$1、$2')
|
|
2158
|
+
.replace(/,+/g, '、')
|
|
2159
|
+
.replace(/、+/g, '、')
|
|
2160
|
+
.replace(/^、|、$/g, '')
|
|
2161
|
+
.trim();
|
|
2162
|
+
}
|
|
2163
|
+
function groupLabel(group) { return text(object(group).columnDesc || object(group).columnName || group); }
|
|
2164
|
+
|
|
2165
|
+
function renderSemanticConflictList() {
|
|
2166
|
+
const reportLines = [...reportConflicts.entries()].map(([id, conflicts]) =>
|
|
2167
|
+
`- [${link(reportById.get(id)?.title || id)}](../briefs/reports/${reportFiles.get(id)}):${conflicts.join(';')}`);
|
|
2168
|
+
const metadataLines = [...metadataSemanticConflicts.entries()].map(([id, conflicts]) => {
|
|
2169
|
+
const item = metadata.find((row) => text(row.evidence_id) === id);
|
|
2170
|
+
if (!item) return `- \`${id}\`:${conflicts.join(';')}`;
|
|
2171
|
+
return `- [${link(metadataDisplayTitle(item))}](../briefs/metadata/${metadataDirectory(item.resource_type)}/${metadataFiles.get(id)}):${conflicts.join(';')}`;
|
|
2172
|
+
});
|
|
2173
|
+
return [...reportLines, ...metadataLines].join('\n') || '- 无';
|
|
2174
|
+
}
|
|
2175
|
+
|
|
2176
|
+
function renderCoverage() {
|
|
2177
|
+
return `# 覆盖、物理来源与未归属资产
|
|
2178
|
+
|
|
2179
|
+
## Agent 使用方式
|
|
2180
|
+
|
|
2181
|
+
- 先用本页判断知识库覆盖范围、未归属资产和风险清单,再回到业务域、召回卡或具体资产页回答问题。
|
|
2182
|
+
- 语义冲突表示多个资产表达同一业务含义但口径不同,Agent 不能只按名称选择资产。
|
|
2183
|
+
- 缺少源标题的元数据通常只作为被引用字段或事件解释,不单独作为业务入口。
|
|
2184
|
+
- 独立报表没有看板承载,不根据名称强行归入业务域;使用前需先确认它是否适配当前问题。
|
|
2185
|
+
|
|
2186
|
+
| 检查项 | 数量 |
|
|
2187
|
+
| --- | ---: |
|
|
2188
|
+
| 业务域 | ${domains.length} |
|
|
2189
|
+
| 技术附录 | ${appendices.length} |
|
|
2190
|
+
| 归属业务域的看板 | ${domainByDashboardId.size} |
|
|
2191
|
+
| 归属技术附录的看板 | ${appendixByDashboardId.size} |
|
|
2192
|
+
| 恰好完成一次分类的看板 | ${domainByDashboardId.size + appendixByDashboardId.size} |
|
|
2193
|
+
| 物理看板空间 | ${physicalContainers.length} |
|
|
2194
|
+
| 共享资产看板 | ${sharedDashboards.length} |
|
|
2195
|
+
| 无物理空间归属的看板 | ${unassignedDashboards.length} |
|
|
2196
|
+
| 业务域报表 | ${reportsInBusinessDomains.length} |
|
|
2197
|
+
| 技术附录报表 | ${reportsInTechnicalAppendices.length} |
|
|
2198
|
+
| 独立报表(不推断业务域) | ${standaloneReports.length} |
|
|
2199
|
+
| 语义冲突报表 | ${reportConflicts.size} |
|
|
2200
|
+
| 语义冲突元数据 | ${metadataSemanticConflicts.size} |
|
|
2201
|
+
| 缺少源标题的元数据 | ${metadataMissingTitles.length} |
|
|
2202
|
+
|
|
2203
|
+
## 技术附录
|
|
2204
|
+
|
|
2205
|
+
${appendices.map((row) => `- [${link(row.title)}](../appendices/${appendixFiles.get(text(row.appendix_id))})`).join('\n') || '- 无'}
|
|
2206
|
+
|
|
2207
|
+
## 技术来源容器
|
|
2208
|
+
|
|
2209
|
+
${containerAssetLinks(technicalContainers)}
|
|
2210
|
+
|
|
2211
|
+
## 无物理空间看板
|
|
2212
|
+
|
|
2213
|
+
${assetLinks(unassignedDashboards, dashboardFiles, '../dashboards')}
|
|
2214
|
+
|
|
2215
|
+
## 独立报表
|
|
2216
|
+
|
|
2217
|
+
这些报表没有看板承载,保留为可检索资产,但不根据名称强行推断业务域。
|
|
2218
|
+
|
|
2219
|
+
${assetLinks(standaloneReports, reportFiles, '../reports')}
|
|
2220
|
+
|
|
2221
|
+
## 语义冲突
|
|
2222
|
+
|
|
2223
|
+
${renderSemanticConflictList()}
|
|
2224
|
+
|
|
2225
|
+
## 缺少源标题的元数据
|
|
2226
|
+
|
|
2227
|
+
${metadataMissingTitles.map((item) => `- [\`${link(item.resource_key)}\`](../metadata/${metadataDirectory(item.resource_type)}/${metadataFiles.get(text(item.evidence_id))})`).join('\n') || '- 无'}
|
|
2228
|
+
`;
|
|
2229
|
+
}
|
|
2230
|
+
|
|
2231
|
+
function renderMetadataIndex(brief) {
|
|
2232
|
+
const grouped = groupBy(metadata, 'resource_type');
|
|
2233
|
+
const sections = [...grouped.entries()]
|
|
2234
|
+
.sort(([left], [right]) => left.localeCompare(right, 'en'))
|
|
2235
|
+
.map(([type, values]) => {
|
|
2236
|
+
const directory = metadataDirectory(type);
|
|
2237
|
+
const rows = sort(values, 'evidence_id').map((item) => {
|
|
2238
|
+
const id = text(item.evidence_id);
|
|
2239
|
+
return `| [${table(metadataDisplayTitle(item))}](${directory}/${metadataFiles.get(id)}) | \`${table(item.resource_key)}\` | ${Boolean(item.authenticated)} | ${(metadataUsageByEvidenceId.get(id) ?? []).length} |`;
|
|
2240
|
+
}).join('\n');
|
|
2241
|
+
return `## ${metadataTypeLabel(type)}(${values.length})\n\n| 名称 | Resource key | 已认证 | 引用报表 |\n| --- | --- | --- | ---: |\n${rows}`;
|
|
2242
|
+
}).join('\n\n');
|
|
2243
|
+
return `# ${brief ? '元数据摘要' : '分析元数据'}\n\n元数据只收录当前看板报表闭包实际引用的事件、属性、标签、分群和指标定义,并保留各自认证状态。\n\n${sections}\n`;
|
|
2244
|
+
}
|
|
2245
|
+
|
|
2246
|
+
function renderAssetIndex(title, values, files, key = 'resource_key') {
|
|
2247
|
+
return `# ${title}\n\n| 名称 | Key | 已认证 |\n| --- | --- | --- |\n${sort(values, key).map((row) => `| [${table(row.title || row.domain_title || row.container_title || row[key])}](${files.get(text(row[key]))}) | \`${table(row[key])}\` | ${row.authenticated == null ? '-' : Boolean(row.authenticated)} |`).join('\n')}\n`;
|
|
2248
|
+
}
|
|
2249
|
+
|
|
2250
|
+
function renderBriefList(title, values, files, key = 'resource_key') {
|
|
2251
|
+
return `# ${title}\n\n${sort(values, key).map((row) => `- [${link(row.title || row.domain_title || row.container_title || row[key])}](${files.get(text(row[key]))})`).join('\n') || '- 无'}\n`;
|
|
2252
|
+
}
|
|
2253
|
+
|
|
2254
|
+
function countTable() {
|
|
2255
|
+
return `## 数量
|
|
2256
|
+
|
|
2257
|
+
| 资产 | 数量 |
|
|
2258
|
+
| --- | ---: |
|
|
2259
|
+
| Agent 业务域 | ${corpus.counts.business_domains} |
|
|
2260
|
+
| 技术附录 | ${corpus.counts.technical_appendices} |
|
|
2261
|
+
| 业务域看板 | ${corpus.counts.dashboards_in_business_domains} |
|
|
2262
|
+
| 技术附录看板 | ${corpus.counts.dashboards_in_technical_appendices} |
|
|
2263
|
+
| 物理看板空间 | ${corpus.counts.source_dashboard_spaces} |
|
|
2264
|
+
| 技术来源容器 | ${corpus.counts.technical_containers} |
|
|
2265
|
+
| 看板 | ${corpus.counts.dashboards} |
|
|
2266
|
+
| 报表 | ${corpus.counts.reports} |
|
|
2267
|
+
| 业务域报表 | ${corpus.counts.reports_in_business_domains} |
|
|
2268
|
+
| 技术附录报表 | ${corpus.counts.reports_in_technical_appendices} |
|
|
2269
|
+
| 独立报表 | ${corpus.counts.standalone_reports} |
|
|
2270
|
+
| 可复用指标 | ${corpus.counts.reusable_metrics} |
|
|
2271
|
+
| 分析元数据 | ${corpus.counts.analysis_metadata} |
|
|
2272
|
+
| 已认证资产 | ${corpus.counts.authenticated_assets} |
|
|
2273
|
+
| 未单独认证的依赖资产 | ${corpus.counts.unauthenticated_dependencies} |
|
|
2274
|
+
| 已认证 / 未单独认证报表 | ${corpus.counts.authenticated_reports} / ${corpus.counts.unauthenticated_reports} |
|
|
2275
|
+
| 已认证 / 未单独认证元数据 | ${corpus.counts.authenticated_metadata} / ${corpus.counts.unauthenticated_metadata} |
|
|
2276
|
+
| 语义冲突报表 | ${corpus.counts.semantic_conflict_reports} |
|
|
2277
|
+
| 语义冲突元数据 | ${corpus.counts.semantic_conflict_metadata} |
|
|
2278
|
+
| 缺少源标题的元数据 | ${corpus.counts.metadata_missing_source_titles} |
|
|
2279
|
+
| 报表指标出现项 | ${corpus.counts.report_metric_occurrences} |
|
|
2280
|
+
| SQL 报表 | ${corpus.counts.sql_reports} |`;
|
|
2281
|
+
}
|
|
2282
|
+
|
|
2283
|
+
function warningSection() {
|
|
2284
|
+
return `## 已知质量提示\n\n${warnings.map((warning) => `- ${warning}`).join('\n') || '- 无'}`;
|
|
2285
|
+
}
|
|
2286
|
+
|
|
2287
|
+
function assetFrontmatter(type, asset) {
|
|
2288
|
+
return `---\ntype: ${type}\nproject_id: ${projectId}\nresource_key: ${yaml(asset.resource_key)}\ntitle: ${yaml(asset.title)}\nauthenticated: ${Boolean(asset.authenticated)}\nsnapshot_hash: ${yaml(snapshotHash)}\nsource_detail_path: ${yaml(asset.source_detail_path)}\n---\n`;
|
|
2289
|
+
}
|
|
2290
|
+
|
|
2291
|
+
function briefFrontmatter(type, extra, confidence) {
|
|
2292
|
+
const fields = Object.entries(extra).map(([key, value]) => `${key}: ${yaml(value)}`).join('\n');
|
|
2293
|
+
return `---\ntype: ${type}\nsummary_kind: evidence_grounded_generated\nconfidence: ${confidence}\nproject_id: ${projectId}\n${fields}\nsource_snapshot_hash: ${snapshotHash}\n---\n`;
|
|
2294
|
+
}
|
|
2295
|
+
|
|
2296
|
+
function domainsForReport(report) {
|
|
2297
|
+
const result = [];
|
|
2298
|
+
const seen = new Set();
|
|
2299
|
+
for (const ref of array(report.dashboard_refs)) {
|
|
2300
|
+
const domain = domainByDashboardId.get(text(ref.dashboard_id));
|
|
2301
|
+
if (domain && !seen.has(domain.domain_id)) {
|
|
2302
|
+
seen.add(domain.domain_id);
|
|
2303
|
+
result.push(domain);
|
|
2304
|
+
}
|
|
2305
|
+
}
|
|
2306
|
+
return result;
|
|
2307
|
+
}
|
|
2308
|
+
|
|
2309
|
+
function appendicesForReport(report) {
|
|
2310
|
+
const result = [];
|
|
2311
|
+
const seen = new Set();
|
|
2312
|
+
for (const ref of array(report.dashboard_refs)) {
|
|
2313
|
+
const appendix = appendixByDashboardId.get(text(ref.dashboard_id));
|
|
2314
|
+
if (appendix && !seen.has(appendix.appendix_id)) {
|
|
2315
|
+
seen.add(appendix.appendix_id);
|
|
2316
|
+
result.push(appendix);
|
|
2317
|
+
}
|
|
2318
|
+
}
|
|
2319
|
+
return result;
|
|
2320
|
+
}
|
|
2321
|
+
|
|
2322
|
+
function domainLinks(values, prefix) {
|
|
2323
|
+
return array(values).map((domain) => `- [${link(domain.title || domain.domain_id)}](${prefix}/${domainFiles.get(text(domain.domain_id))})`).join('\n') || '- 无';
|
|
2324
|
+
}
|
|
2325
|
+
|
|
2326
|
+
function appendixLinks(values, prefix) {
|
|
2327
|
+
return array(values).map((appendix) => `- [${link(appendix.title || appendix.appendix_id)}](${prefix}/${appendixFiles.get(text(appendix.appendix_id))})`).join('\n') || '- 无';
|
|
2328
|
+
}
|
|
2329
|
+
|
|
2330
|
+
function containerLinks(values, prefix) {
|
|
2331
|
+
return array(values).map((ref) => `- [${link(ref.container_title || ref.container_key)}](${prefix}/${containerFiles.get(text(ref.container_key))})`).join('\n') || '- 无';
|
|
2332
|
+
}
|
|
2333
|
+
|
|
2334
|
+
function metadataLinks(values, prefix) {
|
|
2335
|
+
return array(values).map((item) => {
|
|
2336
|
+
const id = text(item.evidence_id);
|
|
2337
|
+
return `- [${link(metadataDisplayTitle(item))}](${prefix}/${metadataDirectory(item.resource_type)}/${metadataFiles.get(id)})(${metadataTypeLabel(item.resource_type)},\`${text(item.resource_key)}\`)`;
|
|
2338
|
+
}).join('\n') || '- 无';
|
|
2339
|
+
}
|
|
2340
|
+
|
|
2341
|
+
function refLinks(values, idKey, nameKey, files, prefix) {
|
|
2342
|
+
return values.map((ref) => `- [${link(ref[nameKey] || ref[idKey])}](${prefix}/${files.get(text(ref[idKey]))}) (\`${text(ref[idKey])}\`)`).join('\n') || '- 无';
|
|
2343
|
+
}
|
|
2344
|
+
|
|
2345
|
+
function assetLinks(values, files, prefix) {
|
|
2346
|
+
return values.map((row) => `- [${link(row.title || row.resource_key)}](${prefix}/${files.get(text(row.resource_key))})`).join('\n') || '- 无';
|
|
2347
|
+
}
|
|
2348
|
+
|
|
2349
|
+
function containerAssetLinks(values) {
|
|
2350
|
+
return values.map((row) => `- [${link(row.container_title || row.container_key)}](../source-containers/${containerFiles.get(text(row.container_key))})(${text(row.container_kind)})`).join('\n') || '- 无';
|
|
2351
|
+
}
|
|
2352
|
+
|
|
2353
|
+
function topNames(values, limit = 5) {
|
|
2354
|
+
const names = [...new Set(values.map(text).filter(Boolean))].slice(0, limit);
|
|
2355
|
+
return names.length ? `“${names.join('、')}”` : '';
|
|
2356
|
+
}
|
|
2357
|
+
|
|
2358
|
+
function rows(name) {
|
|
2359
|
+
return readJsonl(path.join(packageRoot, 'indexes', `${name}.jsonl`)).filter((row) => row.record_type !== 'header');
|
|
2360
|
+
}
|
|
2361
|
+
|
|
2362
|
+
function normalizeMetadata(row) {
|
|
2363
|
+
const raw = object(row);
|
|
2364
|
+
const evidenceId = text(raw.evidenceId ?? raw.evidence_id);
|
|
2365
|
+
const resourceType = text(raw.resourceType ?? raw.resource_type);
|
|
2366
|
+
const resourceKey = text(raw.resourceKey ?? raw.resource_key);
|
|
2367
|
+
if (!evidenceId || !resourceType || !resourceKey) {
|
|
2368
|
+
fail(`Analysis metadata row is missing identity: ${JSON.stringify(raw).slice(0, 300)}.`);
|
|
2369
|
+
}
|
|
2370
|
+
return {
|
|
2371
|
+
evidence_id: evidenceId,
|
|
2372
|
+
resource_type: resourceType,
|
|
2373
|
+
resource_key: resourceKey,
|
|
2374
|
+
title: text(raw.title),
|
|
2375
|
+
summary: text(raw.summary),
|
|
2376
|
+
description: text(raw.description),
|
|
2377
|
+
authenticated: raw.authenticated === true,
|
|
2378
|
+
definition: parseEmbeddedJson(raw.definitionJson ?? raw.definition_json),
|
|
2379
|
+
raw,
|
|
2380
|
+
};
|
|
2381
|
+
}
|
|
2382
|
+
|
|
2383
|
+
function metadataDirectory(type) {
|
|
2384
|
+
return ({
|
|
2385
|
+
event: 'events',
|
|
2386
|
+
event_prop: 'event-properties',
|
|
2387
|
+
user_prop: 'user-properties',
|
|
2388
|
+
tag: 'tags',
|
|
2389
|
+
user_tag: 'tags',
|
|
2390
|
+
cluster: 'clusters',
|
|
2391
|
+
user_cluster: 'clusters',
|
|
2392
|
+
metric: 'metrics',
|
|
2393
|
+
})[text(type)] ?? 'other';
|
|
2394
|
+
}
|
|
2395
|
+
|
|
2396
|
+
function metadataTypeLabel(type) {
|
|
2397
|
+
return ({
|
|
2398
|
+
event: '事件',
|
|
2399
|
+
event_prop: '事件属性',
|
|
2400
|
+
user_prop: '用户属性',
|
|
2401
|
+
tag: '标签',
|
|
2402
|
+
user_tag: '标签',
|
|
2403
|
+
cluster: '分群',
|
|
2404
|
+
user_cluster: '分群',
|
|
2405
|
+
metric: '指标元数据',
|
|
2406
|
+
})[text(type)] ?? `其他元数据(${text(type) || 'unknown'})`;
|
|
2407
|
+
}
|
|
2408
|
+
|
|
2409
|
+
function metadataDisplayTitle(item) {
|
|
2410
|
+
const row = object(item);
|
|
2411
|
+
return cleanDimensionTitle(row.title || row.resource_key, row.resource_key) || text(row.resource_key) || '未命名元数据';
|
|
2412
|
+
}
|
|
2413
|
+
|
|
2414
|
+
function metadataRefsForReport(report) {
|
|
2415
|
+
const found = new Map();
|
|
2416
|
+
collectMetadataReferences(report.metric_occurrences, found);
|
|
2417
|
+
collectMetadataReferences(report.calculation_context, found);
|
|
2418
|
+
return [...found.values()].sort((left, right) =>
|
|
2419
|
+
text(left.evidence_id).localeCompare(text(right.evidence_id), 'en'));
|
|
2420
|
+
}
|
|
2421
|
+
|
|
2422
|
+
function metadataRefsForMetric(metric) {
|
|
2423
|
+
const found = new Map();
|
|
2424
|
+
collectMetadataReferences(metric.metric_occurrences, found);
|
|
2425
|
+
collectMetadataReferences(metric.dependent_events, found);
|
|
2426
|
+
collectMetadataReferences(metric.dependent_properties, found);
|
|
2427
|
+
return [...found.values()].filter((item) => text(item.resource_type) !== 'metric').sort((left, right) =>
|
|
2428
|
+
text(left.evidence_id).localeCompare(text(right.evidence_id), 'en'));
|
|
2429
|
+
}
|
|
2430
|
+
|
|
2431
|
+
function collectMetadataReferences(value, found, inheritedKind = null) {
|
|
2432
|
+
if (Array.isArray(value)) {
|
|
2433
|
+
value.forEach((item) => collectMetadataReferences(item, found, inheritedKind));
|
|
2434
|
+
return;
|
|
2435
|
+
}
|
|
2436
|
+
if (value && typeof value === 'object') {
|
|
2437
|
+
const source = object(value);
|
|
2438
|
+
const metadataKind = metadataReferenceKind(source, inheritedKind);
|
|
2439
|
+
Object.entries(value).forEach(([key, item]) => {
|
|
2440
|
+
const expectedTypes = expectedMetadataTypes(key, metadataKind);
|
|
2441
|
+
if (expectedTypes && (typeof item === 'string' || typeof item === 'number')) {
|
|
2442
|
+
addMetadataCandidate(text(item), found, expectedTypes);
|
|
2443
|
+
} else if (key === 'sql' && typeof item === 'string') {
|
|
2444
|
+
collectSqlMetadataReferences(item, found);
|
|
2445
|
+
} else if (['group_by', 'groupBy'].includes(key) && Array.isArray(item)) {
|
|
2446
|
+
for (const group of item) {
|
|
2447
|
+
if (typeof group === 'string') addMetadataCandidate(group, found, new Set(['event_prop']));
|
|
2448
|
+
else collectMetadataReferences(group, found, metadataKind);
|
|
2449
|
+
}
|
|
2450
|
+
} else {
|
|
2451
|
+
collectMetadataReferences(item, found, metadataKind);
|
|
2452
|
+
}
|
|
2453
|
+
});
|
|
2454
|
+
return;
|
|
2455
|
+
}
|
|
2456
|
+
}
|
|
2457
|
+
|
|
2458
|
+
function collectSqlMetadataReferences(value, found) {
|
|
2459
|
+
for (const match of value.matchAll(/["`]([^"`]+)["`]/g)) {
|
|
2460
|
+
let candidate = text(match[1]);
|
|
2461
|
+
if (!candidate || candidate.startsWith('$part_') || isSqlAlias(value, match.index ?? 0)) continue;
|
|
2462
|
+
if (candidate.startsWith('@vpc_cluster_tag_')) {
|
|
2463
|
+
candidate = candidate.slice('@vpc_cluster_'.length);
|
|
2464
|
+
addMetadataCandidate(candidate, found, new Set(['tag', 'user_tag']));
|
|
2465
|
+
} else if (candidate.startsWith('@vpc_cluster_')) {
|
|
2466
|
+
candidate = candidate.slice('@vpc_cluster_'.length);
|
|
2467
|
+
addMetadataCandidate(candidate, found, new Set(['cluster', 'user_cluster']));
|
|
2468
|
+
} else {
|
|
2469
|
+
candidate = candidate.replace(/^@vpc_tz_/, '');
|
|
2470
|
+
addMetadataCandidate(candidate, found, new Set(['event_prop', 'user_prop']));
|
|
2471
|
+
}
|
|
2472
|
+
}
|
|
2473
|
+
for (const match of value.matchAll(/["`]?\$part_event["`]?\s*=\s*'([^']+)'/gi)) {
|
|
2474
|
+
addMetadataCandidate(text(match[1]), found, new Set(['event']));
|
|
2475
|
+
}
|
|
2476
|
+
for (const match of value.matchAll(/["`]?\$part_event["`]?\s+in\s*\(([^)]*)\)/gi)) {
|
|
2477
|
+
for (const literal of match[1].matchAll(/'([^']+)'/g)) {
|
|
2478
|
+
addMetadataCandidate(text(literal[1]), found, new Set(['event']));
|
|
2479
|
+
}
|
|
2480
|
+
}
|
|
2481
|
+
}
|
|
2482
|
+
|
|
2483
|
+
function isSqlAlias(sql, identifierStart) {
|
|
2484
|
+
return /\bas\s*$/is.test(sql.slice(Math.max(0, identifierStart - 12), identifierStart));
|
|
2485
|
+
}
|
|
2486
|
+
|
|
2487
|
+
function metadataReferenceKind(source, inheritedKind) {
|
|
2488
|
+
const tableType = text(source.tableType ?? source.table_type);
|
|
2489
|
+
if (!tableType) return inheritedKind;
|
|
2490
|
+
if (tableType === '0') return 'event_property';
|
|
2491
|
+
if (tableType === '1') return 'user_property';
|
|
2492
|
+
if (tableType !== '2') return inheritedKind;
|
|
2493
|
+
const subTableType = text(source.subTableType ?? source.sub_table_type);
|
|
2494
|
+
if (subTableType.startsWith('tag_') || source.tagFilter === true) return 'tag';
|
|
2495
|
+
if (subTableType.startsWith('cluster_') || source.clusterFilter === true) return 'cluster';
|
|
2496
|
+
return 'tag_or_cluster';
|
|
2497
|
+
}
|
|
2498
|
+
|
|
2499
|
+
function expectedMetadataTypes(key, metadataKind) {
|
|
2500
|
+
if (['event_name', 'eventName'].includes(key)) return new Set(['event']);
|
|
2501
|
+
if (['metric_key', 'metricName'].includes(key)) return new Set(['metric']);
|
|
2502
|
+
if (!['measure_key', 'measureKey', 'quota', 'columnName', 'column_name', 'prop'].includes(key)) return null;
|
|
2503
|
+
if (metadataKind === 'user_property') return new Set(['user_prop']);
|
|
2504
|
+
if (metadataKind === 'tag') return new Set(['tag', 'user_tag']);
|
|
2505
|
+
if (metadataKind === 'cluster') return new Set(['cluster', 'user_cluster']);
|
|
2506
|
+
if (metadataKind === 'tag_or_cluster') return new Set(['tag', 'user_tag', 'cluster', 'user_cluster']);
|
|
2507
|
+
return new Set(['event_prop']);
|
|
2508
|
+
}
|
|
2509
|
+
|
|
2510
|
+
function addMetadataCandidate(candidate, found, expectedTypes) {
|
|
2511
|
+
for (const item of metadataByResourceKey.get(text(candidate)) ?? []) {
|
|
2512
|
+
if (!expectedTypes || expectedTypes.has(text(item.resource_type))) {
|
|
2513
|
+
found.set(text(item.evidence_id), item);
|
|
2514
|
+
}
|
|
2515
|
+
}
|
|
2516
|
+
}
|
|
2517
|
+
|
|
2518
|
+
function metadataUsage() {
|
|
2519
|
+
const result = new Map();
|
|
2520
|
+
for (const report of reports) {
|
|
2521
|
+
for (const item of metadataRefsForReport(report)) {
|
|
2522
|
+
const id = text(item.evidence_id);
|
|
2523
|
+
if (!result.has(id)) result.set(id, []);
|
|
2524
|
+
result.get(id).push({ report_id: text(report.resource_key), report_name: text(report.title) });
|
|
2525
|
+
}
|
|
2526
|
+
}
|
|
2527
|
+
return result;
|
|
2528
|
+
}
|
|
2529
|
+
|
|
2530
|
+
function metadataMetricUsage() {
|
|
2531
|
+
const result = new Map();
|
|
2532
|
+
for (const metric of metrics) {
|
|
2533
|
+
for (const item of metadataRefsForMetric(metric)) {
|
|
2534
|
+
const id = text(item.evidence_id);
|
|
2535
|
+
if (!result.has(id)) result.set(id, []);
|
|
2536
|
+
result.get(id).push({ metric_id: text(metric.resource_key), metric_name: text(metric.title) });
|
|
2537
|
+
}
|
|
2538
|
+
}
|
|
2539
|
+
return result;
|
|
2540
|
+
}
|
|
2541
|
+
|
|
2542
|
+
function index(values, key) { return new Map(values.map((row) => [text(row[key]), row])); }
|
|
2543
|
+
function fileMap(values, key, fallback) { return new Map(values.map((row) => [text(row[key]), `${safeFilePart(text(row[key]), fallback)}.md`])); }
|
|
2544
|
+
|
|
2545
|
+
function groupBy(values, key) {
|
|
2546
|
+
const result = new Map();
|
|
2547
|
+
for (const value of values) {
|
|
2548
|
+
const identity = text(value[key]);
|
|
2549
|
+
if (!result.has(identity)) result.set(identity, []);
|
|
2550
|
+
result.get(identity).push(value);
|
|
2551
|
+
}
|
|
2552
|
+
return result;
|
|
2553
|
+
}
|
|
2554
|
+
|
|
2555
|
+
function valueCounts(values, key) {
|
|
2556
|
+
return Object.fromEntries([...groupBy(values, key).entries()]
|
|
2557
|
+
.sort(([left], [right]) => left.localeCompare(right, 'en'))
|
|
2558
|
+
.map(([identity, items]) => [identity, items.length]));
|
|
2559
|
+
}
|
|
2560
|
+
|
|
2561
|
+
function parseEmbeddedJson(value) {
|
|
2562
|
+
if (value && typeof value === 'object') return value;
|
|
2563
|
+
const source = text(value);
|
|
2564
|
+
if (!source) return {};
|
|
2565
|
+
try {
|
|
2566
|
+
return JSON.parse(source);
|
|
2567
|
+
} catch {
|
|
2568
|
+
return { raw_value: source };
|
|
2569
|
+
}
|
|
2570
|
+
}
|
|
2571
|
+
|
|
2572
|
+
function uniqueObjects(values, key) {
|
|
2573
|
+
const result = [];
|
|
2574
|
+
const seen = new Set();
|
|
2575
|
+
for (const value of values) {
|
|
2576
|
+
const item = object(value);
|
|
2577
|
+
const identity = text(item[key]);
|
|
2578
|
+
if (identity && !seen.has(identity)) {
|
|
2579
|
+
seen.add(identity);
|
|
2580
|
+
result.push(item);
|
|
2581
|
+
}
|
|
2582
|
+
}
|
|
2583
|
+
return result;
|
|
2584
|
+
}
|
|
2585
|
+
|
|
2586
|
+
function materialize(output, replace, write) {
|
|
2587
|
+
const parent = path.dirname(output);
|
|
2588
|
+
fs.mkdirSync(parent, { recursive: true });
|
|
2589
|
+
if (fs.existsSync(output) && !replace) fail(`Output already exists: ${output}. Pass --force to replace it.`);
|
|
2590
|
+
const nonce = `${process.pid}-${randomBytes(6).toString('hex')}`;
|
|
2591
|
+
const staging = path.join(parent, `.${path.basename(output)}.staging-${nonce}`);
|
|
2592
|
+
const backup = path.join(parent, `.${path.basename(output)}.backup-${nonce}`);
|
|
2593
|
+
fs.mkdirSync(staging, { recursive: false });
|
|
2594
|
+
let moved = false;
|
|
2595
|
+
try {
|
|
2596
|
+
write(staging);
|
|
2597
|
+
if (fs.existsSync(output)) {
|
|
2598
|
+
fs.renameSync(output, backup);
|
|
2599
|
+
moved = true;
|
|
2600
|
+
}
|
|
2601
|
+
fs.renameSync(staging, output);
|
|
2602
|
+
if (moved) fs.rmSync(backup, { recursive: true, force: true });
|
|
2603
|
+
} catch (error) {
|
|
2604
|
+
fs.rmSync(staging, { recursive: true, force: true });
|
|
2605
|
+
if (moved && !fs.existsSync(output)) fs.renameSync(backup, output);
|
|
2606
|
+
throw error;
|
|
2607
|
+
}
|
|
2608
|
+
}
|
|
2609
|
+
|
|
2610
|
+
function assertSafePaths(source, output) {
|
|
2611
|
+
if (!fs.existsSync(source) || !fs.statSync(source).isDirectory()) fail(`Asset package directory does not exist: ${source}`);
|
|
2612
|
+
if (!fs.existsSync(domainPlanPath) || !fs.statSync(domainPlanPath).isFile()) fail(`Business domain plan does not exist: ${domainPlanPath}`);
|
|
2613
|
+
if (output === path.parse(output).root) fail('Output cannot be a filesystem root.');
|
|
2614
|
+
if (output === source || output.startsWith(`${source}${path.sep}`) || source.startsWith(`${output}${path.sep}`)) {
|
|
2615
|
+
fail('Asset package and output directories must not contain each other.');
|
|
2616
|
+
}
|
|
2617
|
+
}
|
|
2618
|
+
|
|
2619
|
+
function parseArgs(values) {
|
|
2620
|
+
const result = {};
|
|
2621
|
+
for (let index = 0; index < values.length; index += 1) {
|
|
2622
|
+
const token = values[index];
|
|
2623
|
+
if (!token.startsWith('--')) fail(`Unexpected argument: ${token}`);
|
|
2624
|
+
const name = token.slice(2);
|
|
2625
|
+
if (['force', 'allow-truncated', 'allow-all-visible'].includes(name)) result[name] = true;
|
|
2626
|
+
else {
|
|
2627
|
+
const value = values[index + 1];
|
|
2628
|
+
if (!value || value.startsWith('--')) fail(`Missing value for --${name}.`);
|
|
2629
|
+
result[name] = value;
|
|
2630
|
+
index += 1;
|
|
2631
|
+
}
|
|
2632
|
+
}
|
|
2633
|
+
return result;
|
|
2634
|
+
}
|
|
2635
|
+
|
|
2636
|
+
function requiredArg(values, name) {
|
|
2637
|
+
const value = text(values[name]);
|
|
2638
|
+
if (!value) fail(`--${name} is required.`);
|
|
2639
|
+
return value;
|
|
2640
|
+
}
|
|
2641
|
+
|
|
2642
|
+
function readObject(file, label) {
|
|
2643
|
+
if (!fs.existsSync(file)) fail(`Missing ${label}: ${file}`);
|
|
2644
|
+
const value = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
2645
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) fail(`${label} must be a JSON object.`);
|
|
2646
|
+
return value;
|
|
2647
|
+
}
|
|
2648
|
+
|
|
2649
|
+
function readJsonl(file) {
|
|
2650
|
+
if (!fs.existsSync(file)) fail(`Missing JSONL file: ${file}`);
|
|
2651
|
+
return fs.readFileSync(file, 'utf8').split(/\r?\n/).filter((line) => line.trim()).map((line, lineIndex) => {
|
|
2652
|
+
try {
|
|
2653
|
+
const value = JSON.parse(line);
|
|
2654
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('expected a JSON object');
|
|
2655
|
+
return value;
|
|
2656
|
+
} catch (error) {
|
|
2657
|
+
fail(`${file} line ${lineIndex + 1}: ${error.message}`);
|
|
2658
|
+
}
|
|
2659
|
+
});
|
|
2660
|
+
}
|
|
2661
|
+
|
|
2662
|
+
function jsonObject(value) {
|
|
2663
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) return value;
|
|
2664
|
+
const content = text(value);
|
|
2665
|
+
if (!content) return {};
|
|
2666
|
+
try {
|
|
2667
|
+
const parsed = JSON.parse(content);
|
|
2668
|
+
return object(parsed);
|
|
2669
|
+
} catch {
|
|
2670
|
+
return {};
|
|
2671
|
+
}
|
|
2672
|
+
}
|
|
2673
|
+
|
|
2674
|
+
function readPackageJson(relativePath) {
|
|
2675
|
+
const relative = text(relativePath);
|
|
2676
|
+
if (!relative || path.isAbsolute(relative) || /[\x00]/.test(relative)) return {};
|
|
2677
|
+
const file = path.resolve(packageRoot, relative);
|
|
2678
|
+
if (file !== packageRoot && !file.startsWith(`${packageRoot}${path.sep}`)) return {};
|
|
2679
|
+
if (!fs.existsSync(file) || !fs.statSync(file).isFile()) return {};
|
|
2680
|
+
if (packageJsonCache.has(file)) return packageJsonCache.get(file);
|
|
2681
|
+
try {
|
|
2682
|
+
const parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
2683
|
+
const result = object(parsed);
|
|
2684
|
+
packageJsonCache.set(file, result);
|
|
2685
|
+
return result;
|
|
2686
|
+
} catch {
|
|
2687
|
+
packageJsonCache.set(file, {});
|
|
2688
|
+
return {};
|
|
2689
|
+
}
|
|
2690
|
+
}
|
|
2691
|
+
|
|
2692
|
+
function compactSqlFragment(value) {
|
|
2693
|
+
const normalized = sanitizeVisibleSqlSemanticText(value).replace(/\s+/g, ' ');
|
|
2694
|
+
if (!normalized) return '';
|
|
2695
|
+
return normalized.length <= 260 ? normalized : `${normalized.slice(0, 240)}...`;
|
|
2696
|
+
}
|
|
2697
|
+
|
|
2698
|
+
function object(value) { return value && typeof value === 'object' && !Array.isArray(value) ? value : {}; }
|
|
2699
|
+
function array(value) { return Array.isArray(value) ? value : []; }
|
|
2700
|
+
function text(value) {
|
|
2701
|
+
if (value == null) return '';
|
|
2702
|
+
const normalized = String(value).trim();
|
|
2703
|
+
return normalized === 'undefined' || normalized === '[object Object]' || normalized === 'null' ? '' : normalized;
|
|
2704
|
+
}
|
|
2705
|
+
function count(value) { const parsed = Number(value); return Number.isFinite(parsed) && parsed >= 0 ? parsed : 0; }
|
|
2706
|
+
function sort(values, key) { return [...values].sort((a, b) => text(a[key]).localeCompare(text(b[key]), 'en')); }
|
|
2707
|
+
function jsonFence(value) { return `\`\`\`json\n${JSON.stringify(value, null, 2)}\n\`\`\``; }
|
|
2708
|
+
function jsonInline(value) { return value == null ? 'none' : `\`${JSON.stringify(value)}\``; }
|
|
2709
|
+
function yaml(value) { return JSON.stringify(text(value)); }
|
|
2710
|
+
function cleanDisplayText(value) {
|
|
2711
|
+
return cleanQuestionMarkPlaceholders(text(value)
|
|
2712
|
+
.replace(new RegExp('\\r?\\n', 'g'), ' ')
|
|
2713
|
+
.replace(/�/g, ''))
|
|
2714
|
+
.replace(/^(?:[??]\s*){2,}(?=\S)/, '')
|
|
2715
|
+
.replace(/\s+/g, ' ')
|
|
2716
|
+
.trim();
|
|
2717
|
+
}
|
|
2718
|
+
|
|
2719
|
+
function cleanQuestionMarkPlaceholders(value) {
|
|
2720
|
+
return text(value).replace(new RegExp('(^|\\s)(?:[??]\\s*){2,}(?=\\S)', 'g'), '$1');
|
|
2721
|
+
}
|
|
2722
|
+
|
|
2723
|
+
function heading(value) { return cleanDisplayText(value).replace(/^#+\s*/, ''); }
|
|
2724
|
+
function link(value) { return cleanDisplayText(value).replace(/([\[\]])/g, '\\$1'); }
|
|
2725
|
+
function table(value) { return cleanDisplayText(value).replace(/\|/g, '\\|'); }
|
|
2726
|
+
|
|
2727
|
+
function safeFilePart(value, fallback) {
|
|
2728
|
+
const normalized = value.normalize('NFKC').replace(/[^A-Za-z0-9._-]+/g, '-').replace(/^-+|-+$/g, '');
|
|
2729
|
+
if (normalized && normalized.length <= 80) return normalized;
|
|
2730
|
+
return `${fallback}-${createHash('sha256').update(value).digest('hex').slice(0, 16)}`;
|
|
2731
|
+
}
|
|
2732
|
+
|
|
2733
|
+
function writeText(file, value) { fs.writeFileSync(file, value.endsWith('\n') ? value : `${value}\n`, 'utf8'); }
|
|
2734
|
+
function writeJson(file, value) { writeText(file, JSON.stringify(value, null, 2)); }
|
|
2735
|
+
function fail(message) { throw new Error(message); }
|