create-yss-spec 3.5.2 → 3.5.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/template/asset-runtime.js +3 -1
- package/template/.agents/skills/.strategic-design-skills-manifest.json +1 -1
- package/template/.agents/skills/codebase-design/SKILL.md +4 -0
- package/template/{.codex/skills/improve-codebase-architecture/SKILL.md → .agents/skills/codebase-design/references/architecture-audit.md} +3 -9
- package/template/.agents/skills/frontend-commit/SKILL.md +4 -93
- package/template/.agents/skills/git-commit-core/SKILL.md +18 -0
- package/template/.agents/skills/java-backend-commit/SKILL.md +4 -96
- package/template/.agents/skills/llm-wiki/SKILL.md +12 -13
- package/template/.agents/skills/llm-wiki/assets/CLAUDE.md.template +7 -1
- package/template/.agents/skills/llm-wiki/references/compile.md +21 -114
- package/template/.agents/skills/llm-wiki/references/ingest.md +8 -33
- package/template/.agents/skills/llm-wiki/references/lint.md +9 -46
- package/template/.agents/skills/llm-wiki/references/query.md +9 -18
- package/template/.agents/skills/llm-wiki/references/schema.md +56 -29
- package/template/.agents/skills/llm-wiki/references/transactions.md +48 -0
- package/template/.agents/skills/llm-wiki/references/writing.md +4 -4
- package/template/.agents/skills/llm-wiki/scripts/advise.mjs +46 -58
- package/template/.agents/skills/llm-wiki/scripts/advise.test.mjs +11 -16
- package/template/.agents/skills/llm-wiki/scripts/core.mjs +262 -0
- package/template/.agents/skills/llm-wiki/scripts/extract.mjs +35 -18
- package/template/.agents/skills/llm-wiki/scripts/extract.test.mjs +5 -8
- package/template/.agents/skills/llm-wiki/scripts/feedback.mjs +51 -0
- package/template/.agents/skills/llm-wiki/scripts/inventory.mjs +43 -171
- package/template/.agents/skills/llm-wiki/scripts/inventory.test.mjs +25 -18
- package/template/.agents/skills/llm-wiki/scripts/lint-wikilinks.mjs +69 -92
- package/template/.agents/skills/llm-wiki/scripts/lint-wikilinks.test.mjs +25 -37
- package/template/.agents/skills/llm-wiki/scripts/migrate.mjs +51 -0
- package/template/.agents/skills/llm-wiki/scripts/query.mjs +149 -0
- package/template/.agents/skills/llm-wiki/scripts/regressions.test.mjs +101 -0
- package/template/.agents/skills/llm-wiki/scripts/sources.mjs +166 -0
- package/template/.agents/skills/llm-wiki/scripts/transaction.mjs +384 -0
- package/template/.agents/skills/llm-wiki/scripts/v2.test.mjs +354 -0
- package/template/.agents/skills/prototype/LOGIC.md +1 -1
- package/template/.agents/skills/prototype/SKILL.md +5 -19
- package/template/.agents/skills/setup-matt-pocock-skills/SKILL.md +3 -3
- package/template/.agents/skills/yss-implementation-contract-compiler/SKILL.md +2 -0
- package/template/.agents/skills/yss-implementation-contract-compiler/references/boundaries.md +2 -3
- package/template/.agents/skills/yss-implementation-contract-compiler/references/yss-skill-execution-result.md +14 -0
- package/template/.agents/skills/yss-product-lifecycle/SKILL.md +8 -8
- package/template/.agents/skills/yss-product-lifecycle/references/external-input-questionnaire.md +19 -0
- package/template/.agents/skills/yss-product-lifecycle/references/matt-yss-adapter.md +5 -5
- package/template/.agents/skills/yss-product-lifecycle/references/orchestration-contract.yaml +20 -4
- package/template/.agents/skills/yss-product-lifecycle/references/orchestration.md +2 -2
- package/template/.agents/skills/yss-product-lifecycle/references/plan-requirements.md +10 -0
- package/template/.agents/skills/yss-product-lifecycle/references/state-model.md +1 -1
- package/template/.agents/skills/yss-research/SKILL.md +4 -0
- package/template/.codex/skills/codebase-design/SKILL.md +4 -0
- package/template/{.pi/skills/improve-codebase-architecture/SKILL.md → .codex/skills/codebase-design/references/architecture-audit.md} +3 -9
- package/template/.codex/skills/frontend-commit/SKILL.md +4 -93
- package/template/.codex/skills/git-commit-core/SKILL.md +18 -0
- package/template/.codex/skills/java-backend-commit/SKILL.md +4 -96
- package/template/.codex/skills/llm-wiki/SKILL.md +12 -13
- package/template/.codex/skills/llm-wiki/assets/CLAUDE.md.template +7 -1
- package/template/.codex/skills/llm-wiki/references/compile.md +21 -114
- package/template/.codex/skills/llm-wiki/references/ingest.md +8 -33
- package/template/.codex/skills/llm-wiki/references/lint.md +9 -46
- package/template/.codex/skills/llm-wiki/references/query.md +9 -18
- package/template/.codex/skills/llm-wiki/references/schema.md +56 -29
- package/template/.codex/skills/llm-wiki/references/transactions.md +48 -0
- package/template/.codex/skills/llm-wiki/references/writing.md +4 -4
- package/template/.codex/skills/llm-wiki/scripts/advise.mjs +46 -58
- package/template/.codex/skills/llm-wiki/scripts/advise.test.mjs +11 -16
- package/template/.codex/skills/llm-wiki/scripts/core.mjs +262 -0
- package/template/.codex/skills/llm-wiki/scripts/extract.mjs +35 -18
- package/template/.codex/skills/llm-wiki/scripts/extract.test.mjs +5 -8
- package/template/.codex/skills/llm-wiki/scripts/feedback.mjs +51 -0
- package/template/.codex/skills/llm-wiki/scripts/inventory.mjs +43 -171
- package/template/.codex/skills/llm-wiki/scripts/inventory.test.mjs +25 -18
- package/template/.codex/skills/llm-wiki/scripts/lint-wikilinks.mjs +69 -92
- package/template/.codex/skills/llm-wiki/scripts/lint-wikilinks.test.mjs +25 -37
- package/template/.codex/skills/llm-wiki/scripts/migrate.mjs +51 -0
- package/template/.codex/skills/llm-wiki/scripts/query.mjs +149 -0
- package/template/.codex/skills/llm-wiki/scripts/regressions.test.mjs +101 -0
- package/template/.codex/skills/llm-wiki/scripts/sources.mjs +166 -0
- package/template/.codex/skills/llm-wiki/scripts/transaction.mjs +384 -0
- package/template/.codex/skills/llm-wiki/scripts/v2.test.mjs +354 -0
- package/template/.codex/skills/prototype/LOGIC.md +1 -1
- package/template/.codex/skills/prototype/SKILL.md +5 -19
- package/template/.codex/skills/setup-matt-pocock-skills/SKILL.md +3 -3
- package/template/.codex/skills/yss-implementation-contract-compiler/SKILL.md +2 -0
- package/template/.codex/skills/yss-implementation-contract-compiler/references/boundaries.md +2 -3
- package/template/.codex/skills/yss-implementation-contract-compiler/references/yss-skill-execution-result.md +14 -0
- package/template/.codex/skills/yss-product-lifecycle/SKILL.md +8 -8
- package/template/.codex/skills/yss-product-lifecycle/references/external-input-questionnaire.md +19 -0
- package/template/.codex/skills/yss-product-lifecycle/references/matt-yss-adapter.md +5 -5
- package/template/.codex/skills/yss-product-lifecycle/references/orchestration-contract.yaml +20 -4
- package/template/.codex/skills/yss-product-lifecycle/references/orchestration.md +2 -2
- package/template/.codex/skills/yss-product-lifecycle/references/plan-requirements.md +10 -0
- package/template/.codex/skills/yss-product-lifecycle/references/state-model.md +1 -1
- package/template/.codex/skills/yss-research/SKILL.md +4 -0
- package/template/.cursor/skills/codebase-design/SKILL.md +4 -0
- package/template/.cursor/skills/{improve-codebase-architecture/SKILL.md → codebase-design/references/architecture-audit.md} +3 -9
- package/template/.cursor/skills/frontend-commit/SKILL.md +4 -93
- package/template/.cursor/skills/git-commit-core/SKILL.md +18 -0
- package/template/.cursor/skills/java-backend-commit/SKILL.md +4 -96
- package/template/.cursor/skills/llm-wiki/SKILL.md +12 -13
- package/template/.cursor/skills/llm-wiki/assets/CLAUDE.md.template +7 -1
- package/template/.cursor/skills/llm-wiki/references/compile.md +21 -114
- package/template/.cursor/skills/llm-wiki/references/ingest.md +8 -33
- package/template/.cursor/skills/llm-wiki/references/lint.md +9 -46
- package/template/.cursor/skills/llm-wiki/references/query.md +9 -18
- package/template/.cursor/skills/llm-wiki/references/schema.md +56 -29
- package/template/.cursor/skills/llm-wiki/references/transactions.md +48 -0
- package/template/.cursor/skills/llm-wiki/references/writing.md +4 -4
- package/template/.cursor/skills/llm-wiki/scripts/advise.mjs +46 -58
- package/template/.cursor/skills/llm-wiki/scripts/advise.test.mjs +11 -16
- package/template/.cursor/skills/llm-wiki/scripts/core.mjs +262 -0
- package/template/.cursor/skills/llm-wiki/scripts/extract.mjs +35 -18
- package/template/.cursor/skills/llm-wiki/scripts/extract.test.mjs +5 -8
- package/template/.cursor/skills/llm-wiki/scripts/feedback.mjs +51 -0
- package/template/.cursor/skills/llm-wiki/scripts/inventory.mjs +43 -171
- package/template/.cursor/skills/llm-wiki/scripts/inventory.test.mjs +25 -18
- package/template/.cursor/skills/llm-wiki/scripts/lint-wikilinks.mjs +69 -92
- package/template/.cursor/skills/llm-wiki/scripts/lint-wikilinks.test.mjs +25 -37
- package/template/.cursor/skills/llm-wiki/scripts/migrate.mjs +51 -0
- package/template/.cursor/skills/llm-wiki/scripts/query.mjs +149 -0
- package/template/.cursor/skills/llm-wiki/scripts/regressions.test.mjs +101 -0
- package/template/.cursor/skills/llm-wiki/scripts/sources.mjs +166 -0
- package/template/.cursor/skills/llm-wiki/scripts/transaction.mjs +384 -0
- package/template/.cursor/skills/llm-wiki/scripts/v2.test.mjs +354 -0
- package/template/.cursor/skills/prototype/LOGIC.md +1 -1
- package/template/.cursor/skills/prototype/SKILL.md +5 -19
- package/template/.cursor/skills/setup-matt-pocock-skills/SKILL.md +3 -3
- package/template/.cursor/skills/yss-implementation-contract-compiler/SKILL.md +2 -0
- package/template/.cursor/skills/yss-implementation-contract-compiler/references/boundaries.md +2 -3
- package/template/.cursor/skills/yss-implementation-contract-compiler/references/yss-skill-execution-result.md +14 -0
- package/template/.cursor/skills/yss-product-lifecycle/SKILL.md +8 -8
- package/template/.cursor/skills/yss-product-lifecycle/references/external-input-questionnaire.md +19 -0
- package/template/.cursor/skills/yss-product-lifecycle/references/matt-yss-adapter.md +5 -5
- package/template/.cursor/skills/yss-product-lifecycle/references/orchestration-contract.yaml +20 -4
- package/template/.cursor/skills/yss-product-lifecycle/references/orchestration.md +2 -2
- package/template/.cursor/skills/yss-product-lifecycle/references/plan-requirements.md +10 -0
- package/template/.cursor/skills/yss-product-lifecycle/references/state-model.md +1 -1
- package/template/.cursor/skills/yss-research/SKILL.md +4 -0
- package/template/.pi/skills/codebase-design/SKILL.md +4 -0
- package/template/{.agents/skills/improve-codebase-architecture/SKILL.md → .pi/skills/codebase-design/references/architecture-audit.md} +3 -9
- package/template/.pi/skills/frontend-commit/SKILL.md +4 -93
- package/template/.pi/skills/git-commit-core/SKILL.md +18 -0
- package/template/.pi/skills/java-backend-commit/SKILL.md +4 -96
- package/template/.pi/skills/llm-wiki/SKILL.md +12 -13
- package/template/.pi/skills/llm-wiki/assets/CLAUDE.md.template +7 -1
- package/template/.pi/skills/llm-wiki/references/compile.md +21 -114
- package/template/.pi/skills/llm-wiki/references/ingest.md +8 -33
- package/template/.pi/skills/llm-wiki/references/lint.md +9 -46
- package/template/.pi/skills/llm-wiki/references/query.md +9 -18
- package/template/.pi/skills/llm-wiki/references/schema.md +56 -29
- package/template/.pi/skills/llm-wiki/references/transactions.md +48 -0
- package/template/.pi/skills/llm-wiki/references/writing.md +4 -4
- package/template/.pi/skills/llm-wiki/scripts/advise.mjs +46 -58
- package/template/.pi/skills/llm-wiki/scripts/advise.test.mjs +11 -16
- package/template/.pi/skills/llm-wiki/scripts/core.mjs +262 -0
- package/template/.pi/skills/llm-wiki/scripts/extract.mjs +35 -18
- package/template/.pi/skills/llm-wiki/scripts/extract.test.mjs +5 -8
- package/template/.pi/skills/llm-wiki/scripts/feedback.mjs +51 -0
- package/template/.pi/skills/llm-wiki/scripts/inventory.mjs +43 -171
- package/template/.pi/skills/llm-wiki/scripts/inventory.test.mjs +25 -18
- package/template/.pi/skills/llm-wiki/scripts/lint-wikilinks.mjs +69 -92
- package/template/.pi/skills/llm-wiki/scripts/lint-wikilinks.test.mjs +25 -37
- package/template/.pi/skills/llm-wiki/scripts/migrate.mjs +51 -0
- package/template/.pi/skills/llm-wiki/scripts/query.mjs +149 -0
- package/template/.pi/skills/llm-wiki/scripts/regressions.test.mjs +101 -0
- package/template/.pi/skills/llm-wiki/scripts/sources.mjs +166 -0
- package/template/.pi/skills/llm-wiki/scripts/transaction.mjs +384 -0
- package/template/.pi/skills/llm-wiki/scripts/v2.test.mjs +354 -0
- package/template/.pi/skills/prototype/LOGIC.md +1 -1
- package/template/.pi/skills/prototype/SKILL.md +5 -19
- package/template/.pi/skills/setup-matt-pocock-skills/SKILL.md +3 -3
- package/template/.pi/skills/yss-implementation-contract-compiler/SKILL.md +2 -0
- package/template/.pi/skills/yss-implementation-contract-compiler/references/boundaries.md +2 -3
- package/template/.pi/skills/yss-implementation-contract-compiler/references/yss-skill-execution-result.md +14 -0
- package/template/.pi/skills/yss-product-lifecycle/SKILL.md +8 -8
- package/template/.pi/skills/yss-product-lifecycle/references/external-input-questionnaire.md +19 -0
- package/template/.pi/skills/yss-product-lifecycle/references/matt-yss-adapter.md +5 -5
- package/template/.pi/skills/yss-product-lifecycle/references/orchestration-contract.yaml +20 -4
- package/template/.pi/skills/yss-product-lifecycle/references/orchestration.md +2 -2
- package/template/.pi/skills/yss-product-lifecycle/references/plan-requirements.md +10 -0
- package/template/.pi/skills/yss-product-lifecycle/references/state-model.md +1 -1
- package/template/.pi/skills/yss-research/SKILL.md +4 -0
- package/template/.template-spec/agents/README.md +1 -1
- package/template/.template-spec/agents/digital-human-roles.yaml +1 -1
- package/template/.template-spec/agents/skill-migrations.md +15 -0
- package/template/.template-spec/agents/yss-skill-registry.yaml +10 -29
- package/template/.template-spec/architecture/README.md +3 -34
- package/template/.template-spec/plan/templates/competitive-analysis-template.md +2 -2
- package/template/.template-spec/process/contract-reading.md +24 -0
- package/template/.template-spec/process/lifecycle-registry-baseline.json +3 -1
- package/template/.template-spec/process/lifecycle-registry.yaml +6 -0
- package/template/.template-spec/process/research-completion.md +23 -0
- package/template/.template-spec/process/schemas/digital-human-task-package.schema.json +202 -12
- package/template/.template-spec/process/schemas/frontend-implementation-evidence.schema.json +7 -15
- package/template/.template-spec/process/subagent-collaboration.md +8 -0
- package/template/.template-spec/process/templates/frontend-implementation-verification-template.yaml +15 -1
- package/template/AGENTS.md +1 -1
- package/template/CONTEXT.md +5 -1
- package/template/README.md +12 -24
- package/template/scripts/contract +5 -5
- package/template/scripts/lib/contract-views.mjs +57 -2
- package/template/scripts/lib/execution-evidence.mjs +51 -0
- package/template/scripts/lib/harness-execution-scope.mjs +1 -1
- package/template/scripts/lib/implementation-contract-compiler.mjs +2 -0
- package/template/scripts/lib/lifecycle-context-query.mjs +15 -3
- package/template/scripts/lib/lifecycle-status.mjs +32 -6
- package/template/scripts/lib/lifecycle-transition.mjs +19 -1
- package/template/scripts/lib/maintenance-research.mjs +63 -0
- package/template/scripts/lib/read-only-intake.mjs +132 -0
- package/template/scripts/lib/skill-registry.mjs +2 -1
- package/template/scripts/lib/skill-supply-chain.mjs +4 -0
- package/template/scripts/lib/slice-task-package.mjs +1 -1
- package/template/scripts/lib/task-package.mjs +9 -2
- package/template/scripts/lifecycle-status +2 -2
- package/template/scripts/prepare-read-only-intake +16 -0
- package/template/scripts/run-read-only-intake +14 -0
- package/template/scripts/verify-digital-human-task-package +2 -2
- package/template/scripts/verify-frontend-implementation-evidence +24 -1
- package/template/scripts/verify-maintenance-research +13 -0
- package/template/skills-lock.json +19 -76
- package/template.manifest.json +118 -29
- package/template.snapshot.json +5 -6
- package/template/.agents/skills/grill-with-docs/SKILL.md +0 -20
- package/template/.agents/skills/grill-with-docs/agents/openai.yaml +0 -5
- package/template/.agents/skills/improve-codebase-architecture/agents/openai.yaml +0 -5
- package/template/.agents/skills/prototype/UI.md +0 -112
- package/template/.agents/skills/setup-matt-pocock-skills/domain.md +0 -37
- package/template/.agents/skills/to-questionnaire/SKILL.md +0 -55
- package/template/.agents/skills/to-questionnaire/agents/openai.yaml +0 -5
- package/template/.agents/skills/wait-what/SKILL.md +0 -7
- package/template/.agents/skills/wait-what/agents/openai.yaml +0 -5
- package/template/.codex/skills/data-analytics/.app.json +0 -84
- package/template/.codex/skills/data-analytics/.codex-plugin/plugin.json +0 -66
- package/template/.codex/skills/data-analytics/.mcp.json +0 -30
- package/template/.codex/skills/data-analytics/AGENTS.md +0 -20
- package/template/.codex/skills/data-analytics/DEPENDENCIES.MD +0 -27
- package/template/.codex/skills/data-analytics/README.md +0 -60
- package/template/.codex/skills/data-analytics/__yss_dotfile__.gitignore +0 -2
- package/template/.codex/skills/data-analytics/assets/datascience-artifact-widget.html +0 -16
- package/template/.codex/skills/data-analytics/assets/datascience-artifact-widget.html.gz.b64.part001 +0 -1
- package/template/.codex/skills/data-analytics/assets/datascience-artifact-widget.html.gz.b64.part002 +0 -1
- package/template/.codex/skills/data-analytics/assets/datascience-artifact-widget.html.gz.b64.part003 +0 -1
- package/template/.codex/skills/data-analytics/assets/datascience-artifact-widget.html.gz.b64.part004 +0 -1
- package/template/.codex/skills/data-analytics/assets/datascience-artifact-widget.html.gz.b64.part005 +0 -1
- package/template/.codex/skills/data-analytics/assets/datascience-artifact-widget.html.gz.b64.part006 +0 -1
- package/template/.codex/skills/data-analytics/assets/datascience-chart-widget.html +0 -16
- package/template/.codex/skills/data-analytics/assets/datascience-chart-widget.html.gz.b64.part001 +0 -1
- package/template/.codex/skills/data-analytics/assets/datascience-chart-widget.html.gz.b64.part002 +0 -1
- package/template/.codex/skills/data-analytics/assets/datascience-chart-widget.html.gz.b64.part003 +0 -1
- package/template/.codex/skills/data-analytics/assets/datascience-chart-widget.html.gz.b64.part004 +0 -1
- package/template/.codex/skills/data-analytics/assets/datascience-chart-widget.html.gz.b64.part005 +0 -1
- package/template/.codex/skills/data-analytics/assets/datascience-chart-widget.html.gz.b64.part006 +0 -1
- package/template/.codex/skills/data-analytics/assets/datascience-chart-widget.html.gz.b64.part007 +0 -1
- package/template/.codex/skills/data-analytics/assets/datascience-small.svg +0 -5
- package/template/.codex/skills/data-analytics/assets/datascience-table-widget.html +0 -16
- package/template/.codex/skills/data-analytics/assets/datascience-table-widget.html.gz.b64.part001 +0 -1
- package/template/.codex/skills/data-analytics/assets/datascience-table-widget.html.gz.b64.part002 +0 -1
- package/template/.codex/skills/data-analytics/assets/datascience-table-widget.html.gz.b64.part003 +0 -1
- package/template/.codex/skills/data-analytics/assets/datascience-table-widget.html.gz.b64.part004 +0 -1
- package/template/.codex/skills/data-analytics/assets/datascience.png +0 -0
- package/template/.codex/skills/data-analytics/assets/datascience.svg +0 -10
- package/template/.codex/skills/data-analytics/mcp/server.cjs +0 -2964
- package/template/.codex/skills/data-analytics/package-lock.json +0 -3048
- package/template/.codex/skills/data-analytics/package.json +0 -43
- package/template/.codex/skills/data-analytics/scripts/normalize-widget-assets.mjs +0 -75
- package/template/.codex/skills/data-analytics/skills/analyze-data-quality/SKILL.md +0 -137
- package/template/.codex/skills/data-analytics/skills/analyze-data-quality/agents/openai.yaml +0 -6
- package/template/.codex/skills/data-analytics/skills/analyze-data-quality/references/quality-checks.md +0 -29
- package/template/.codex/skills/data-analytics/skills/build-dashboard/SKILL.md +0 -148
- package/template/.codex/skills/data-analytics/skills/build-dashboard/agents/openai.yaml +0 -6
- package/template/.codex/skills/data-analytics/skills/build-dashboard/specifications/bi-platform-dashboard.md +0 -18
- package/template/.codex/skills/data-analytics/skills/build-dashboard/specifications/html-dashboard.md +0 -24
- package/template/.codex/skills/data-analytics/skills/build-dashboard/specifications/mcp-artifact-dashboard.md +0 -71
- package/template/.codex/skills/data-analytics/skills/build-dashboard/specifications/streamlit-dashboard.md +0 -85
- package/template/.codex/skills/data-analytics/skills/build-report/SKILL.md +0 -207
- package/template/.codex/skills/data-analytics/skills/build-report/agents/openai.yaml +0 -6
- package/template/.codex/skills/data-analytics/skills/build-report/assets/executive-report-shell.html +0 -70
- package/template/.codex/skills/data-analytics/skills/build-report/assets/technical-report-shell.html +0 -66
- package/template/.codex/skills/data-analytics/skills/build-report/report-to-google-doc/SKILL.md +0 -108
- package/template/.codex/skills/data-analytics/skills/build-report/report-to-google-doc/agents/openai.yaml +0 -6
- package/template/.codex/skills/data-analytics/skills/build-report/report-to-google-doc/scripts/report_to_google_doc/__init__.py +0 -1
- package/template/.codex/skills/data-analytics/skills/build-report/report-to-google-doc/scripts/report_to_google_doc/cli.py +0 -52
- package/template/.codex/skills/data-analytics/skills/build-report/report-to-google-doc/scripts/report_to_google_doc/constants.py +0 -61
- package/template/.codex/skills/data-analytics/skills/build-report/report-to-google-doc/scripts/report_to_google_doc/docx_writer.py +0 -362
- package/template/.codex/skills/data-analytics/skills/build-report/report-to-google-doc/scripts/report_to_google_doc/html_parser.py +0 -829
- package/template/.codex/skills/data-analytics/skills/build-report/report-to-google-doc/scripts/report_to_google_doc/model.py +0 -30
- package/template/.codex/skills/data-analytics/skills/build-report/report-to-google-doc/scripts/report_to_google_doc/plan.py +0 -127
- package/template/.codex/skills/data-analytics/skills/build-report/report-to-google-doc/scripts/report_to_google_doc/quality.py +0 -374
- package/template/.codex/skills/data-analytics/skills/build-report/report-to-google-doc/scripts/report_to_google_doc/rendering.py +0 -613
- package/template/.codex/skills/data-analytics/skills/build-report/report-to-google-doc/scripts/report_to_google_doc/table_utils.py +0 -54
- package/template/.codex/skills/data-analytics/skills/build-report/report-to-google-doc/scripts/report_to_google_doc/utils.py +0 -16
- package/template/.codex/skills/data-analytics/skills/build-report/report-to-google-doc/scripts/report_to_google_doc_plan.py +0 -9
- package/template/.codex/skills/data-analytics/skills/build-report/report-to-google-doc/tests/test_delivery_plan.py +0 -45
- package/template/.codex/skills/data-analytics/skills/build-report/report-to-google-slides/SKILL.md +0 -77
- package/template/.codex/skills/data-analytics/skills/build-report/report-to-google-slides/agents/openai.yaml +0 -6
- package/template/.codex/skills/data-analytics/skills/build-report/report-to-google-slides/scripts/report_to_google_slides.py +0 -2379
- package/template/.codex/skills/data-analytics/skills/build-report/report-to-pdf/SKILL.md +0 -88
- package/template/.codex/skills/data-analytics/skills/build-report/report-to-pdf/agents/openai.yaml +0 -6
- package/template/.codex/skills/data-analytics/skills/build-report/specifications/executive-report.md +0 -97
- package/template/.codex/skills/data-analytics/skills/build-report/specifications/mcp-app-report.md +0 -59
- package/template/.codex/skills/data-analytics/skills/build-report/specifications/technical-report.md +0 -75
- package/template/.codex/skills/data-analytics/skills/design-kpis/SKILL.md +0 -103
- package/template/.codex/skills/data-analytics/skills/design-kpis/agents/openai.yaml +0 -6
- package/template/.codex/skills/data-analytics/skills/gather-business-context/SKILL.md +0 -68
- package/template/.codex/skills/data-analytics/skills/gather-business-context/agents/openai.yaml +0 -6
- package/template/.codex/skills/data-analytics/skills/index/SKILL.md +0 -251
- package/template/.codex/skills/data-analytics/skills/index/agents/openai.yaml +0 -6
- package/template/.codex/skills/data-analytics/skills/jupyter-notebooks/SKILL.md +0 -131
- package/template/.codex/skills/data-analytics/skills/jupyter-notebooks/agents/openai.yaml +0 -6
- package/template/.codex/skills/data-analytics/skills/kpi-reporting/SKILL.md +0 -141
- package/template/.codex/skills/data-analytics/skills/kpi-reporting/agents/openai.yaml +0 -6
- package/template/.codex/skills/data-analytics/skills/kpi-reporting/references/report-templates.md +0 -32
- package/template/.codex/skills/data-analytics/skills/market-sizing/SKILL.md +0 -106
- package/template/.codex/skills/data-analytics/skills/market-sizing/agents/openai.yaml +0 -6
- package/template/.codex/skills/data-analytics/skills/metric-diagnostics/SKILL.md +0 -130
- package/template/.codex/skills/data-analytics/skills/metric-diagnostics/agents/openai.yaml +0 -6
- package/template/.codex/skills/data-analytics/skills/product-business-analysis/SKILL.md +0 -141
- package/template/.codex/skills/data-analytics/skills/product-business-analysis/agents/openai.yaml +0 -6
- package/template/.codex/skills/data-analytics/skills/spreadsheets/SKILL.md +0 -178
- package/template/.codex/skills/data-analytics/skills/spreadsheets/agents/openai.yaml +0 -9
- package/template/.codex/skills/data-analytics/skills/spreadsheets/assets/file-spreadsheet.png +0 -0
- package/template/.codex/skills/data-analytics/skills/spreadsheets/charts.md +0 -31
- package/template/.codex/skills/data-analytics/skills/spreadsheets/references/artifact_tool_api.md +0 -466
- package/template/.codex/skills/data-analytics/skills/spreadsheets/style_guidelines.md +0 -99
- package/template/.codex/skills/data-analytics/skills/user-context/SKILL.md +0 -197
- package/template/.codex/skills/data-analytics/skills/user-context/agents/openai.yaml +0 -6
- package/template/.codex/skills/data-analytics/skills/user-context/plugin-author-config/automation-config.md +0 -26
- package/template/.codex/skills/data-analytics/skills/user-context/plugin-author-config/source-category-config.json +0 -51
- package/template/.codex/skills/data-analytics/skills/user-context/plugin-author-config/user-context-config.md +0 -66
- package/template/.codex/skills/data-analytics/skills/user-context/references/automation.md +0 -69
- package/template/.codex/skills/data-analytics/skills/user-context/references/onboarding-examples.md +0 -204
- package/template/.codex/skills/data-analytics/skills/user-context/references/onboarding-state-template.json +0 -87
- package/template/.codex/skills/data-analytics/skills/user-context/references/onboarding.md +0 -497
- package/template/.codex/skills/data-analytics/skills/user-context/references/semantic-layer/connector-playbook.md +0 -74
- package/template/.codex/skills/data-analytics/skills/user-context/references/semantic-layer/setup.md +0 -65
- package/template/.codex/skills/data-analytics/skills/user-context/references/semantic-layer/skill-template.md +0 -160
- package/template/.codex/skills/data-analytics/skills/user-context/references/semantic-layer/source-intake.md +0 -75
- package/template/.codex/skills/data-analytics/skills/user-context/references/semantic-layer/weekly-polling-automation.md +0 -96
- package/template/.codex/skills/data-analytics/skills/user-context/references/source-category-runtime.md +0 -263
- package/template/.codex/skills/data-analytics/skills/user-context/scripts/data_analytics_preflight.py +0 -1462
- package/template/.codex/skills/data-analytics/skills/user-context/scripts/init_user_context_state.py +0 -128
- package/template/.codex/skills/data-analytics/skills/user-context/scripts/reset_user_context_state.py +0 -101
- package/template/.codex/skills/data-analytics/skills/user-context/scripts/validate_user_context_preflight.py +0 -499
- package/template/.codex/skills/data-analytics/skills/user-context/tests/test_state_helpers.py +0 -978
- package/template/.codex/skills/data-analytics/skills/validate-data/SKILL.md +0 -126
- package/template/.codex/skills/data-analytics/skills/validate-data/agents/openai.yaml +0 -6
- package/template/.codex/skills/data-analytics/skills/validate-data/references/validation-methods.md +0 -70
- package/template/.codex/skills/data-analytics/skills/visualize-data/SKILL.md +0 -157
- package/template/.codex/skills/data-analytics/skills/visualize-data/agents/openai.yaml +0 -6
- package/template/.codex/skills/data-analytics/skills/visualize-data/references/seaborn-templates.md +0 -774
- package/template/.codex/skills/data-analytics/src/DESIGN.md +0 -222
- package/template/.codex/skills/data-analytics/src/analytics-app/App.tsx +0 -3666
- package/template/.codex/skills/data-analytics/src/analytics-app/analytics-layout.test.mjs +0 -136
- package/template/.codex/skills/data-analytics/src/analytics-app/charting/ChartFrame.tsx +0 -54
- package/template/.codex/skills/data-analytics/src/analytics-app/charting/ChartLegend.tsx +0 -96
- package/template/.codex/skills/data-analytics/src/analytics-app/charting/ChartRenderer.tsx +0 -1647
- package/template/.codex/skills/data-analytics/src/analytics-app/charting/ChartTooltip.tsx +0 -245
- package/template/.codex/skills/data-analytics/src/analytics-app/charting/chart-app-helpers.tsx +0 -462
- package/template/.codex/skills/data-analytics/src/analytics-app/charting/chart-capabilities.ts +0 -107
- package/template/.codex/skills/data-analytics/src/analytics-app/charting/chart-compatibility.ts +0 -164
- package/template/.codex/skills/data-analytics/src/analytics-app/charting/chart-contract.ts +0 -192
- package/template/.codex/skills/data-analytics/src/analytics-app/charting/chart-theme.ts +0 -203
- package/template/.codex/skills/data-analytics/src/analytics-app/charting/chart-tokens.css +0 -619
- package/template/.codex/skills/data-analytics/src/analytics-app/charting/chart-transforms.ts +0 -402
- package/template/.codex/skills/data-analytics/src/analytics-app/fonts/SystemSansVariableVF.woff2 +0 -0
- package/template/.codex/skills/data-analytics/src/analytics-app/imageExport.ts +0 -370
- package/template/.codex/skills/data-analytics/src/analytics-app/layout/AnalyticsLayoutCanvas.tsx +0 -536
- package/template/.codex/skills/data-analytics/src/analytics-app/layout/RichMarkdown.tsx +0 -681
- package/template/.codex/skills/data-analytics/src/analytics-app/layout/analyticsLayoutCore.ts +0 -164
- package/template/.codex/skills/data-analytics/src/analytics-app/main.tsx +0 -21
- package/template/.codex/skills/data-analytics/src/analytics-app/scripts/chart_contract.py +0 -472
- package/template/.codex/skills/data-analytics/src/analytics-app/scripts/check_analytics_app_runtime_links.py +0 -75
- package/template/.codex/skills/data-analytics/src/analytics-app/scripts/design_contract.py +0 -142
- package/template/.codex/skills/data-analytics/src/analytics-app/scripts/package_utils.py +0 -142
- package/template/.codex/skills/data-analytics/src/analytics-app/scripts/tests/test_package_utils.py +0 -377
- package/template/.codex/skills/data-analytics/src/analytics-app/styles.css +0 -3468
- package/template/.codex/skills/data-analytics/src/analytics-app/tables/DataTable.d.ts +0 -44
- package/template/.codex/skills/data-analytics/src/analytics-app/tables/DataTable.jsx +0 -639
- package/template/.codex/skills/data-analytics/src/analytics-app/tables/data-table.css +0 -327
- package/template/.codex/skills/data-analytics/src/analytics-app/tokens.css +0 -225
- package/template/.codex/skills/data-analytics/src/analytics-app/types.ts +0 -204
- package/template/.codex/skills/data-analytics/src/analytics-app-core.md +0 -85
- package/template/.codex/skills/data-analytics/src/codex-style-contract.md +0 -30
- package/template/.codex/skills/data-analytics/src/datascience-artifact-widget.html +0 -12
- package/template/.codex/skills/data-analytics/src/datascience-artifact-widget.jsx +0 -1008
- package/template/.codex/skills/data-analytics/src/datascience-chart-widget.css +0 -2117
- package/template/.codex/skills/data-analytics/src/datascience-chart-widget.html +0 -122
- package/template/.codex/skills/data-analytics/src/datascience-chart-widget.js +0 -3588
- package/template/.codex/skills/data-analytics/src/datascience-table-widget.css +0 -483
- package/template/.codex/skills/data-analytics/src/datascience-table-widget.html +0 -32
- package/template/.codex/skills/data-analytics/src/datascience-table-widget.js +0 -333
- package/template/.codex/skills/data-analytics/src/mcp-host.js +0 -153
- package/template/.codex/skills/data-analytics/src/recharts-config.js +0 -315
- package/template/.codex/skills/data-analytics/src/recharts-renderer.jsx +0 -294
- package/template/.codex/skills/data-analytics/src/sql-source-view.css +0 -33
- package/template/.codex/skills/data-analytics/src/sql-source-view.js +0 -157
- package/template/.codex/skills/data-analytics/src/styles/codex-theme.css +0 -121
- package/template/.codex/skills/data-analytics/src/table-renderer.jsx +0 -28
- package/template/.codex/skills/data-analytics/tests/chart-transforms.test.mjs +0 -87
- package/template/.codex/skills/data-analytics/tests/funnel-smoke.html +0 -105
- package/template/.codex/skills/data-analytics/tests/inline-widget-compare.html +0 -233
- package/template/.codex/skills/data-analytics/tests/mcp-server.test.mjs +0 -1500
- package/template/.codex/skills/data-analytics/tests/native-style-contract.test.mjs +0 -219
- package/template/.codex/skills/data-analytics/tests/recharts-config.test.mjs +0 -144
- package/template/.codex/skills/data-analytics/tests/recharts-renderer.test.mjs +0 -11
- package/template/.codex/skills/data-analytics/tests/rich-markdown.test.mjs +0 -54
- package/template/.codex/skills/data-analytics/tests/widget-render-harness.html +0 -321
- package/template/.codex/skills/data-analytics/tsconfig.json +0 -19
- package/template/.codex/skills/data-analytics/vite.config.ts +0 -46
- package/template/.codex/skills/grill-with-docs/SKILL.md +0 -20
- package/template/.codex/skills/grill-with-docs/agents/openai.yaml +0 -5
- package/template/.codex/skills/improve-codebase-architecture/agents/openai.yaml +0 -5
- package/template/.codex/skills/prototype/UI.md +0 -112
- package/template/.codex/skills/setup-matt-pocock-skills/domain.md +0 -37
- package/template/.codex/skills/to-questionnaire/SKILL.md +0 -55
- package/template/.codex/skills/to-questionnaire/agents/openai.yaml +0 -5
- package/template/.codex/skills/wait-what/SKILL.md +0 -7
- package/template/.codex/skills/wait-what/agents/openai.yaml +0 -5
- package/template/.cursor/skills/grill-with-docs/SKILL.md +0 -20
- package/template/.cursor/skills/grill-with-docs/agents/openai.yaml +0 -5
- package/template/.cursor/skills/improve-codebase-architecture/agents/openai.yaml +0 -5
- package/template/.cursor/skills/prototype/UI.md +0 -112
- package/template/.cursor/skills/setup-matt-pocock-skills/domain.md +0 -37
- package/template/.cursor/skills/to-questionnaire/SKILL.md +0 -55
- package/template/.cursor/skills/to-questionnaire/agents/openai.yaml +0 -5
- package/template/.cursor/skills/wait-what/SKILL.md +0 -7
- package/template/.cursor/skills/wait-what/agents/openai.yaml +0 -5
- package/template/.pi/skills/grill-with-docs/SKILL.md +0 -20
- package/template/.pi/skills/grill-with-docs/agents/openai.yaml +0 -5
- package/template/.pi/skills/improve-codebase-architecture/agents/openai.yaml +0 -5
- package/template/.pi/skills/prototype/UI.md +0 -112
- package/template/.pi/skills/setup-matt-pocock-skills/domain.md +0 -37
- package/template/.pi/skills/to-questionnaire/SKILL.md +0 -55
- package/template/.pi/skills/to-questionnaire/agents/openai.yaml +0 -5
- package/template/.pi/skills/wait-what/SKILL.md +0 -7
- package/template/.pi/skills/wait-what/agents/openai.yaml +0 -5
- package/template/.template-spec/agents/domain.md +0 -31
- /package/template/.agents/skills/{improve-codebase-architecture → codebase-design/references}/HTML-REPORT.md +0 -0
- /package/template/.codex/skills/{improve-codebase-architecture → codebase-design/references}/HTML-REPORT.md +0 -0
- /package/template/.cursor/skills/{improve-codebase-architecture → codebase-design/references}/HTML-REPORT.md +0 -0
- /package/template/.pi/skills/{improve-codebase-architecture → codebase-design/references}/HTML-REPORT.md +0 -0
|
@@ -1,2964 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
const fs = require("node:fs");
|
|
4
|
-
const crypto = require("node:crypto");
|
|
5
|
-
const os = require("node:os");
|
|
6
|
-
const path = require("node:path");
|
|
7
|
-
const readline = require("node:readline");
|
|
8
|
-
const childProcess = require("node:child_process");
|
|
9
|
-
const zlib = require("node:zlib");
|
|
10
|
-
|
|
11
|
-
const SERVER_NAME = "datascience-widgets";
|
|
12
|
-
const PLUGIN_ROOT = path.resolve(__dirname, "..");
|
|
13
|
-
const PLUGIN_MANIFEST = JSON.parse(
|
|
14
|
-
fs.readFileSync(path.join(PLUGIN_ROOT, ".codex-plugin", "plugin.json"), "utf8"),
|
|
15
|
-
);
|
|
16
|
-
const SERVER_VERSION = PLUGIN_MANIFEST.version || "0.1.1";
|
|
17
|
-
const DATA_ANALYTICS_ICON = {
|
|
18
|
-
src: assetDataUrl("datascience-small.svg", "image/svg+xml"),
|
|
19
|
-
mimeType: "image/svg+xml",
|
|
20
|
-
sizes: ["24x24"],
|
|
21
|
-
};
|
|
22
|
-
const DATA_ANALYTICS_LOGO = {
|
|
23
|
-
src: assetDataUrl("datascience.png", "image/png"),
|
|
24
|
-
mimeType: "image/png",
|
|
25
|
-
sizes: ["360x360"],
|
|
26
|
-
};
|
|
27
|
-
const DATA_ANALYTICS_ICONS = [DATA_ANALYTICS_ICON, DATA_ANALYTICS_LOGO];
|
|
28
|
-
const TABLE_WIDGET_URI = "ui://widget/datascience-table.html";
|
|
29
|
-
const CHART_WIDGET_URI = "ui://widget/datascience-chart.html";
|
|
30
|
-
const ARTIFACT_WIDGET_URI = "ui://widget/datascience-artifact.html";
|
|
31
|
-
const WIDGET_MIME_TYPE = "text/html;profile=mcp-app";
|
|
32
|
-
const MEASURE_NAMES_FIELD = "__measure_names__";
|
|
33
|
-
const WIDGET_RESOURCE_DOMAINS = ["https://cdn.openai.com"];
|
|
34
|
-
const TOOL_NAMES = {
|
|
35
|
-
validateArtifact: "validate_artifact",
|
|
36
|
-
renderArtifact: "render_artifact",
|
|
37
|
-
exportArtifactPackage: "export_artifact_package",
|
|
38
|
-
renderChart: "render_chart",
|
|
39
|
-
renderTable: "render_table",
|
|
40
|
-
};
|
|
41
|
-
const MAX_WIDGET_ROWS = 500;
|
|
42
|
-
const MAX_WIDGET_COLUMNS = 80;
|
|
43
|
-
const MAX_WIDGET_DATA_POINTS = 2000;
|
|
44
|
-
const MAX_WIDGET_CELLS = 12000;
|
|
45
|
-
const MAX_WIDGET_FIELD_NAME_CHARS = 128;
|
|
46
|
-
const MAX_WIDGET_CELL_STRING_CHARS = 4000;
|
|
47
|
-
const MAX_WIDGET_PAYLOAD_BYTES = 750000;
|
|
48
|
-
const MAX_ARTIFACT_DATASETS = 50;
|
|
49
|
-
const MAX_ARTIFACT_ROWS_PER_DATASET = 2000;
|
|
50
|
-
const MAX_ARTIFACT_PAYLOAD_BYTES = 3_000_000;
|
|
51
|
-
const MAX_ARTIFACT_INLINE_SOURCE_CHARS = 200_000;
|
|
52
|
-
const CANONICAL_CHART_TYPES = [
|
|
53
|
-
"line",
|
|
54
|
-
"area",
|
|
55
|
-
"stackedArea",
|
|
56
|
-
"bar",
|
|
57
|
-
"histogram",
|
|
58
|
-
"scatter",
|
|
59
|
-
"heatmap",
|
|
60
|
-
"pie",
|
|
61
|
-
"leaderboard",
|
|
62
|
-
"sparkline",
|
|
63
|
-
"funnel",
|
|
64
|
-
"waterfall",
|
|
65
|
-
"boxPlot",
|
|
66
|
-
];
|
|
67
|
-
const CANONICAL_CHART_TYPE_SET = new Set(CANONICAL_CHART_TYPES);
|
|
68
|
-
const BAR_ORIENTATIONS = ["vertical", "horizontal"];
|
|
69
|
-
const BAR_GROUP_MODES = ["single", "grouped", "stacked", "stacked100"];
|
|
70
|
-
const TREND_POINT_MODES = ["always", "never"];
|
|
71
|
-
const SERIES_LINE_STYLES = ["solid", "dashed", "dotted"];
|
|
72
|
-
const COMPACT_BAR_CATEGORY_WARNING_THRESHOLD = 24;
|
|
73
|
-
const COMPACT_PIE_SEGMENT_WARNING_THRESHOLD = 8;
|
|
74
|
-
const UNSAFE_FIELD_PATTERNS = [
|
|
75
|
-
"password",
|
|
76
|
-
"passwd",
|
|
77
|
-
"pwd",
|
|
78
|
-
"secret",
|
|
79
|
-
"api[-_]?key",
|
|
80
|
-
"access[-_]?token",
|
|
81
|
-
"refresh[-_]?token",
|
|
82
|
-
"credential",
|
|
83
|
-
"authorization",
|
|
84
|
-
"cookie",
|
|
85
|
-
"private[-_]?key",
|
|
86
|
-
"session[-_]?id",
|
|
87
|
-
"ssn",
|
|
88
|
-
"social[-_]?security",
|
|
89
|
-
"credit[-_]?card",
|
|
90
|
-
"card[-_]?number",
|
|
91
|
-
"cvv",
|
|
92
|
-
"email[-_]?address",
|
|
93
|
-
"user[-_]?email",
|
|
94
|
-
"customer[-_]?email",
|
|
95
|
-
"phone[-_]?number",
|
|
96
|
-
"street[-_]?address",
|
|
97
|
-
"ip[-_]?address",
|
|
98
|
-
"raw[-_]?pii",
|
|
99
|
-
"hidden[-_]?reasoning",
|
|
100
|
-
];
|
|
101
|
-
const UNSAFE_FIELD_NAME_PATTERN = new RegExp(
|
|
102
|
-
`(^|[_\\-.])(${UNSAFE_FIELD_PATTERNS.join("|")})([_\\-.]|$)`,
|
|
103
|
-
"i",
|
|
104
|
-
);
|
|
105
|
-
|
|
106
|
-
const WIDGET_TOOL_URIS = {
|
|
107
|
-
[TOOL_NAMES.renderArtifact]: ARTIFACT_WIDGET_URI,
|
|
108
|
-
[TOOL_NAMES.renderChart]: CHART_WIDGET_URI,
|
|
109
|
-
[TOOL_NAMES.renderTable]: TABLE_WIDGET_URI,
|
|
110
|
-
};
|
|
111
|
-
const WIDGET_TOOL_VISIBILITY = {
|
|
112
|
-
[TOOL_NAMES.renderArtifact]: ["model"],
|
|
113
|
-
[TOOL_NAMES.renderChart]: ["model"],
|
|
114
|
-
[TOOL_NAMES.renderTable]: ["model"],
|
|
115
|
-
};
|
|
116
|
-
const TOOL_INVOCATION_LABELS = {
|
|
117
|
-
[TOOL_NAMES.renderArtifact]: {
|
|
118
|
-
invoking: "Rendering artifact",
|
|
119
|
-
invoked: "Rendered artifact",
|
|
120
|
-
},
|
|
121
|
-
[TOOL_NAMES.renderChart]: {
|
|
122
|
-
invoking: "Rendering chart",
|
|
123
|
-
invoked: "Rendered chart",
|
|
124
|
-
},
|
|
125
|
-
[TOOL_NAMES.renderTable]: {
|
|
126
|
-
invoking: "Rendering table",
|
|
127
|
-
invoked: "Rendered table",
|
|
128
|
-
},
|
|
129
|
-
};
|
|
130
|
-
const WIDGET_DESCRIPTIONS = {
|
|
131
|
-
[ARTIFACT_WIDGET_URI]:
|
|
132
|
-
[
|
|
133
|
-
"A hosted Data Analytics artifact app that renders dashboard and report manifests",
|
|
134
|
-
"with bounded snapshots, source details, shared chart/table components, and agent-mediated",
|
|
135
|
-
"refresh/export prompts.",
|
|
136
|
-
].join(" "),
|
|
137
|
-
[TABLE_WIDGET_URI]:
|
|
138
|
-
"A compact sortable Data Analytics table for reviewed query preview rows and lookup rows.",
|
|
139
|
-
[CHART_WIDGET_URI]:
|
|
140
|
-
[
|
|
141
|
-
"A compact Data Analytics chart card that accepts provenance, a reviewed table,",
|
|
142
|
-
"and a declarative chart spec, then expands into a full chart explorer",
|
|
143
|
-
"app with field wells for re-encoding exploration-ready rows.",
|
|
144
|
-
].join(" "),
|
|
145
|
-
};
|
|
146
|
-
const SERVER_INSTRUCTIONS = [
|
|
147
|
-
"Before rendering a report or dashboard artifact, call validate_artifact with the",
|
|
148
|
-
"complete manifest and bounded snapshot. Fix validation errors there first; do not use",
|
|
149
|
-
"render_artifact as an iterative validator because failed render attempts can create",
|
|
150
|
-
"visible placeholder cards. After validation passes, use render_artifact to host the",
|
|
151
|
-
"complete Data Analytics dashboard or report manifest with a bounded snapshot inside the MCP app;",
|
|
152
|
-
"this is the default reader handoff for report and dashboard work and should be attempted before",
|
|
153
|
-
"static HTML, localhost, or file:// delivery.",
|
|
154
|
-
"Artifact snapshots must be bounded: at most 50 datasets, 2,000 rows per dataset, 3MB total",
|
|
155
|
-
"payload, and 200k total inline source characters. Use the canonical artifact snapshot shape:",
|
|
156
|
-
"snapshot.datasets is an object keyed by dataset id, and each value is a plain array of row objects",
|
|
157
|
-
"like {\"weekly_revenue\":[{\"week\":\"2026-05-04\",\"arr\":123}]}. Do not put {columns, rows}",
|
|
158
|
-
"objects inside artifact snapshot datasets; table-shaped dataset objects are rejected.",
|
|
159
|
-
"Use snapshot.accessIssues only when required report/dashboard data is missing and the snapshot",
|
|
160
|
-
"status is partial or blocked. Do not use accessIssues for optional source limitations, denied",
|
|
161
|
-
"exploratory joins, methodology caveats, or provenance notes when the artifact is otherwise ready;",
|
|
162
|
-
"put those in manifest sources or markdown body blocks instead.",
|
|
163
|
-
"All artifacts must declare a reader-facing manifest.title plus top-level manifest.blocks.",
|
|
164
|
-
"Cards, charts, and tables define reusable renderable assets; blocks establish the artifact",
|
|
165
|
-
"reading order. Report artifacts must include at least one chart data visualization block and",
|
|
166
|
-
"a first markdown block whose body is a # heading matching manifest.title.",
|
|
167
|
-
"Native artifact charts must use encodings.x.field plus encodings.y.field or encodings.y.fields,",
|
|
168
|
-
"with optional encodings.color.field for grouped tidy data. Legacy manifest chart fields",
|
|
169
|
-
"xField and series are rejected; use validate_artifact to check chart shape before rendering.",
|
|
170
|
-
"When a validated MCP artifact report or dashboard needs a hosted Site Creator link, call",
|
|
171
|
-
"export_artifact_package and deploy that package instead of hand-rolling standalone HTML.",
|
|
172
|
-
"The exporter preserves the real artifact runtime and serves /api/manifest, /api/snapshot,",
|
|
173
|
-
"/api/package, /api/source-file, and /api/inline-chart-widget.",
|
|
174
|
-
"Use render_chart after a Data Analytics workflow has already produced a small,",
|
|
175
|
-
"shareable source query result.",
|
|
176
|
-
"Pass source, table, chart, and display for chart widgets.",
|
|
177
|
-
"Chart subtitles should add a reader-facing insight or takeaway not already covered by the",
|
|
178
|
-
"title. Do not use subtitles for source names, query ids, table names, SQL intent, metric",
|
|
179
|
-
"definitions, or provenance; put those details in source.query/source metadata instead.",
|
|
180
|
-
"For chart widgets, make table exploration-ready: include useful dimensions,",
|
|
181
|
-
"measures, time columns, and grouping columns returned by the reviewed query, not only the",
|
|
182
|
-
"plotted chart fields.",
|
|
183
|
-
"For scatter widgets, prefer one row per meaningful observation rather than a few broad",
|
|
184
|
-
"aggregates, with a stable point label, numeric x and y measures at the same grain,",
|
|
185
|
-
"denominator or sample-size fields, one volume/size candidate, and one interpretable grouping",
|
|
186
|
-
"or filter field when safe.",
|
|
187
|
-
"Treat by <dimension> in a chart title, subtitle, or visible header as an encoding contract.",
|
|
188
|
-
"An x/y axis dimension already satisfies that contract. If <dimension> is not on an axis and",
|
|
189
|
-
"is not otherwise visibly encoded through color/series, grouped or stacked marks, faceting,",
|
|
190
|
-
"or direct labels, remove by <dimension> from the visible text. For render_chart, a time or",
|
|
191
|
-
"category x-axis chart titled ... by segment or ... by market must bind that second dimension",
|
|
192
|
-
"through chart.fields.color.field or an equivalent visible grouping rather than only retaining",
|
|
193
|
-
"it in the source table.",
|
|
194
|
-
"When a grouped chart uses color, series, grouped, stacked, or faceted behavior, make the",
|
|
195
|
-
"group names visible with a legend or direct labels.",
|
|
196
|
-
"Only set chart.fields.color.field when it is a meaningful",
|
|
197
|
-
"grouping dimension such as segment, product_line, or series; omit color for single-series charts.",
|
|
198
|
-
"For trend charts, chart.fields.lineStyle.field may point to a text column with solid, dashed,",
|
|
199
|
-
"or dotted values so grouped lines and their legends use different stroke styles.",
|
|
200
|
-
'Use chart.type "bar" plus chart.options.orientation and chart.options.grouping',
|
|
201
|
-
"for bar-family charts.",
|
|
202
|
-
"Prefer tidy long rows, keep the payload compact, set row_count and truncated when sampling,",
|
|
203
|
-
"and order sampled rows deterministically.",
|
|
204
|
-
"After running a durable query, use render_table to show a compact preview of reviewed",
|
|
205
|
-
"rows before or alongside interpretation.",
|
|
206
|
-
"Source SQL belongs in source.query.sql and must be runnable SQL, not prose. Put the",
|
|
207
|
-
"human-readable query summary in source.query.description. Source metadata should name actual",
|
|
208
|
-
"tables such as example.analytics.fact_revenue, and metric definitions should state calculations,",
|
|
209
|
-
"windows, units, denominators, and material exclusions.",
|
|
210
|
-
"Include reviewed analytical dimensions such as customer, account, company, segment, and product names when relevant.",
|
|
211
|
-
"Do not send hidden reasoning, credentials, secrets, or direct personal contact/payment identifiers to widgets.",
|
|
212
|
-
].join(" ");
|
|
213
|
-
|
|
214
|
-
function assetDataUrl(fileName, mimeType) {
|
|
215
|
-
const assetBytes = fs.readFileSync(path.join(PLUGIN_ROOT, "assets", fileName));
|
|
216
|
-
return `data:${mimeType};base64,${assetBytes.toString("base64")}`;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
function objectSchema(properties, required = [], additionalProperties = false) {
|
|
220
|
-
return { type: "object", properties, required, additionalProperties };
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
function toolUiMeta(resourceUri, toolName = null) {
|
|
224
|
-
const ui = { resourceUri };
|
|
225
|
-
const visibility = WIDGET_TOOL_VISIBILITY[toolName || ""];
|
|
226
|
-
if (visibility) ui.visibility = visibility;
|
|
227
|
-
const meta = {
|
|
228
|
-
ui,
|
|
229
|
-
"ui/resourceUri": resourceUri,
|
|
230
|
-
"openai/outputTemplate": resourceUri,
|
|
231
|
-
"openai/widgetAccessible": true,
|
|
232
|
-
};
|
|
233
|
-
const invocationLabels = TOOL_INVOCATION_LABELS[toolName];
|
|
234
|
-
if (invocationLabels != null) {
|
|
235
|
-
meta["openai/toolInvocation/invoking"] = invocationLabels.invoking;
|
|
236
|
-
meta["openai/toolInvocation/invoked"] = invocationLabels.invoked;
|
|
237
|
-
}
|
|
238
|
-
return meta;
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
function tool(name, title, description, inputSchema, outputTemplate, annotations = {}) {
|
|
242
|
-
const definition = {
|
|
243
|
-
name,
|
|
244
|
-
title,
|
|
245
|
-
description,
|
|
246
|
-
inputSchema,
|
|
247
|
-
annotations: {
|
|
248
|
-
readOnlyHint: true,
|
|
249
|
-
destructiveHint: false,
|
|
250
|
-
idempotentHint: true,
|
|
251
|
-
openWorldHint: false,
|
|
252
|
-
...annotations,
|
|
253
|
-
},
|
|
254
|
-
};
|
|
255
|
-
if (outputTemplate) definition._meta = toolUiMeta(outputTemplate, name);
|
|
256
|
-
return definition;
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
function toolDefinitions() {
|
|
260
|
-
const scalar = { type: ["string", "number", "boolean", "null"] };
|
|
261
|
-
const row = {
|
|
262
|
-
type: "object",
|
|
263
|
-
description: [
|
|
264
|
-
"One reviewed result row. For chart widgets, include all useful fields needed",
|
|
265
|
-
"for re-encoding in the expanded explorer, not only plotted x/y/series.",
|
|
266
|
-
].join(" "),
|
|
267
|
-
additionalProperties: scalar,
|
|
268
|
-
};
|
|
269
|
-
const columnType = {
|
|
270
|
-
type: ["string", "null"],
|
|
271
|
-
enum: ["text", "number", "percent", "currency", "date", null],
|
|
272
|
-
};
|
|
273
|
-
const visualizationType = {
|
|
274
|
-
type: "string",
|
|
275
|
-
enum: CANONICAL_CHART_TYPES,
|
|
276
|
-
description:
|
|
277
|
-
'Use canonical chart.type values. Bar-family charts should use "bar" with chart.options.orientation and chart.options.grouping.',
|
|
278
|
-
};
|
|
279
|
-
const column = objectSchema(
|
|
280
|
-
{
|
|
281
|
-
key: { type: "string" },
|
|
282
|
-
label: { type: ["string", "null"] },
|
|
283
|
-
type: columnType,
|
|
284
|
-
format: {
|
|
285
|
-
type: ["string", "null"],
|
|
286
|
-
enum: ["compact", "number", "percent", "currency", null],
|
|
287
|
-
},
|
|
288
|
-
unit: { type: ["string", "null"] },
|
|
289
|
-
align: { type: ["string", "null"], enum: ["left", "right", "center", null] },
|
|
290
|
-
},
|
|
291
|
-
["key"],
|
|
292
|
-
);
|
|
293
|
-
column.description = [
|
|
294
|
-
"Column metadata for reviewed query results. Include useful fields from the source query",
|
|
295
|
-
"so users can swap encodings in the expanded chart widget.",
|
|
296
|
-
].join(" ");
|
|
297
|
-
const sourceQuery = objectSchema({
|
|
298
|
-
engine: { type: ["string", "null"] },
|
|
299
|
-
id: { type: ["string", "null"] },
|
|
300
|
-
url: { type: ["string", "null"] },
|
|
301
|
-
sql: {
|
|
302
|
-
type: ["string", "null"],
|
|
303
|
-
description:
|
|
304
|
-
"Preferred field for the actual runnable SQL used to produce the exposed rows. Paste the executed SQL here; never put prose, methodology, or a summary here.",
|
|
305
|
-
},
|
|
306
|
-
description: {
|
|
307
|
-
type: ["string", "null"],
|
|
308
|
-
description:
|
|
309
|
-
"High-level human-readable description of what the query does. Do not put runnable SQL here.",
|
|
310
|
-
},
|
|
311
|
-
language: { type: ["string", "null"] },
|
|
312
|
-
executed_at: { type: ["string", "null"] },
|
|
313
|
-
tables_used: {
|
|
314
|
-
type: "array",
|
|
315
|
-
items: { type: "string" },
|
|
316
|
-
description:
|
|
317
|
-
"Actual source table names used by the query, preferably fully qualified names such as example.analytics.fact_revenue.",
|
|
318
|
-
},
|
|
319
|
-
filters: {
|
|
320
|
-
type: "array",
|
|
321
|
-
items: { type: "string" },
|
|
322
|
-
description:
|
|
323
|
-
"Human-readable static source predicates, including date windows, population/cohort criteria, material exclusions, and sampling or limit rules.",
|
|
324
|
-
},
|
|
325
|
-
metric_definitions: {
|
|
326
|
-
type: "array",
|
|
327
|
-
items: { type: "string" },
|
|
328
|
-
description:
|
|
329
|
-
"Formulas and scope rules for displayed metrics, including windows, exclusions, denominators, and units.",
|
|
330
|
-
},
|
|
331
|
-
});
|
|
332
|
-
const sourceSchema = objectSchema({
|
|
333
|
-
id: { type: ["string", "null"] },
|
|
334
|
-
label: { type: ["string", "null"] },
|
|
335
|
-
path: { type: ["string", "null"] },
|
|
336
|
-
href: { type: ["string", "null"] },
|
|
337
|
-
query: sourceQuery,
|
|
338
|
-
});
|
|
339
|
-
const resultTable = {
|
|
340
|
-
type: "object",
|
|
341
|
-
description: [
|
|
342
|
-
"Exploration-ready reviewed source query result. For chart widgets, prefer this over",
|
|
343
|
-
"pre-shaped data points and include useful dimensions, measures, time,",
|
|
344
|
-
"and grouping fields so users can change chart encodings in the expanded UI. For scatter",
|
|
345
|
-
"charts, prefer one row per meaningful observation rather than a few broad aggregates,",
|
|
346
|
-
"with a stable point label, numeric x and y measures at the same grain, denominator or",
|
|
347
|
-
"sample-size fields, one volume/size candidate, and one interpretable grouping or filter",
|
|
348
|
-
"field when safe. Keep the",
|
|
349
|
-
"payload compact; set row_count to the full reviewed result count and truncated=true",
|
|
350
|
-
"when rows are sampled.",
|
|
351
|
-
].join(" "),
|
|
352
|
-
properties: {
|
|
353
|
-
columns: {
|
|
354
|
-
type: "array",
|
|
355
|
-
items: column,
|
|
356
|
-
default: [],
|
|
357
|
-
description: "Column metadata for all useful fields exposed to the chart explorer.",
|
|
358
|
-
},
|
|
359
|
-
rows: {
|
|
360
|
-
type: "array",
|
|
361
|
-
items: row,
|
|
362
|
-
default: [],
|
|
363
|
-
description: "Reviewed rows to preview and re-encode. Prefer tidy long-form rows.",
|
|
364
|
-
},
|
|
365
|
-
row_count: {
|
|
366
|
-
type: ["integer", "null"],
|
|
367
|
-
minimum: 0,
|
|
368
|
-
description:
|
|
369
|
-
"Full reviewed result row count, even when rows contains a deterministic sample.",
|
|
370
|
-
},
|
|
371
|
-
truncated: {
|
|
372
|
-
type: ["boolean", "null"],
|
|
373
|
-
description: "True when rows is a safe deterministic sample of a larger reviewed result.",
|
|
374
|
-
},
|
|
375
|
-
},
|
|
376
|
-
additionalProperties: true,
|
|
377
|
-
};
|
|
378
|
-
const encoding = {
|
|
379
|
-
type: "object",
|
|
380
|
-
properties: {
|
|
381
|
-
field: { type: ["string", "null"] },
|
|
382
|
-
type: {
|
|
383
|
-
type: ["string", "null"],
|
|
384
|
-
enum: ["nominal", "ordinal", "quantitative", "temporal", "text", null],
|
|
385
|
-
},
|
|
386
|
-
aggregate: {
|
|
387
|
-
type: ["string", "null"],
|
|
388
|
-
enum: ["none", "sum", "avg", "min", "max", "count", null],
|
|
389
|
-
},
|
|
390
|
-
time_unit: {
|
|
391
|
-
type: ["string", "null"],
|
|
392
|
-
enum: ["none", "year", "quarter", "month", "week", "day", "hour", "minute", "second", null],
|
|
393
|
-
},
|
|
394
|
-
label: { type: ["string", "null"] },
|
|
395
|
-
unit: { type: ["string", "null"] },
|
|
396
|
-
},
|
|
397
|
-
additionalProperties: true,
|
|
398
|
-
};
|
|
399
|
-
const chartOptions = objectSchema({
|
|
400
|
-
orientation: { type: ["string", "null"], enum: [...BAR_ORIENTATIONS, null] },
|
|
401
|
-
grouping: { type: ["string", "null"], enum: [...BAR_GROUP_MODES, null] },
|
|
402
|
-
points: { type: ["string", "null"], enum: [...TREND_POINT_MODES, null] },
|
|
403
|
-
multi_measure_series: { type: ["boolean", "null"] },
|
|
404
|
-
});
|
|
405
|
-
const chart = objectSchema(
|
|
406
|
-
{
|
|
407
|
-
type: visualizationType,
|
|
408
|
-
fields: objectSchema(
|
|
409
|
-
{
|
|
410
|
-
x: encoding,
|
|
411
|
-
y: encoding,
|
|
412
|
-
size: encoding,
|
|
413
|
-
color: encoding,
|
|
414
|
-
lineStyle: encoding,
|
|
415
|
-
label: encoding,
|
|
416
|
-
},
|
|
417
|
-
["x", "y"],
|
|
418
|
-
),
|
|
419
|
-
options: chartOptions,
|
|
420
|
-
},
|
|
421
|
-
["type", "fields"],
|
|
422
|
-
);
|
|
423
|
-
const display = objectSchema({
|
|
424
|
-
unit: { type: ["string", "null"] },
|
|
425
|
-
baseline: { type: ["number", "null"] },
|
|
426
|
-
x_axis_title: {
|
|
427
|
-
type: ["string", "null"],
|
|
428
|
-
description: "X-axis title override. Defaults to the x field column label.",
|
|
429
|
-
},
|
|
430
|
-
y_axis_title: {
|
|
431
|
-
type: ["string", "null"],
|
|
432
|
-
description: "Y-axis title override. Defaults to the y field column label.",
|
|
433
|
-
},
|
|
434
|
-
controls: { type: ["boolean", "null"] },
|
|
435
|
-
});
|
|
436
|
-
const metric = objectSchema(
|
|
437
|
-
{
|
|
438
|
-
label: { type: "string" },
|
|
439
|
-
value: scalar,
|
|
440
|
-
delta: { type: ["string", "number", "null"] },
|
|
441
|
-
},
|
|
442
|
-
["label", "value"],
|
|
443
|
-
);
|
|
444
|
-
const common = {
|
|
445
|
-
title: { type: "string" },
|
|
446
|
-
subtitle: { type: ["string", "null"] },
|
|
447
|
-
source: sourceSchema,
|
|
448
|
-
};
|
|
449
|
-
|
|
450
|
-
const chartInputSchema = objectSchema(
|
|
451
|
-
{
|
|
452
|
-
title: { type: "string" },
|
|
453
|
-
subtitle: {
|
|
454
|
-
type: ["string", "null"],
|
|
455
|
-
description: [
|
|
456
|
-
"Optional reader-facing subtitle. Use it for a concise insight or takeaway not",
|
|
457
|
-
"covered by the title. Do not put source names, query ids, table names, SQL intent,",
|
|
458
|
-
"metric definitions, or provenance here; those belong in source.query/source metadata.",
|
|
459
|
-
].join(" "),
|
|
460
|
-
},
|
|
461
|
-
source: sourceSchema,
|
|
462
|
-
table: resultTable,
|
|
463
|
-
chart,
|
|
464
|
-
display,
|
|
465
|
-
},
|
|
466
|
-
["title", "source", "table", "chart"],
|
|
467
|
-
);
|
|
468
|
-
const tableInputSchema = objectSchema(
|
|
469
|
-
{
|
|
470
|
-
...common,
|
|
471
|
-
columns: { type: "array", items: column, default: [] },
|
|
472
|
-
rows: { type: "array", items: row, default: [] },
|
|
473
|
-
result_table: resultTable,
|
|
474
|
-
max_rows: { type: "integer", minimum: 1, maximum: 500, default: 50 },
|
|
475
|
-
metrics: { type: "array", items: metric, default: [] },
|
|
476
|
-
notes: { type: "array", items: { type: "string" }, default: [] },
|
|
477
|
-
},
|
|
478
|
-
["title", "source"],
|
|
479
|
-
);
|
|
480
|
-
const artifactMetricFormat = {
|
|
481
|
-
type: ["string", "null"],
|
|
482
|
-
enum: ["compact", "number", "percent", "currency", null],
|
|
483
|
-
};
|
|
484
|
-
const artifactCardMetric = objectSchema(
|
|
485
|
-
{
|
|
486
|
-
label: {
|
|
487
|
-
type: "string",
|
|
488
|
-
description: "Reader-facing metric label.",
|
|
489
|
-
},
|
|
490
|
-
field: {
|
|
491
|
-
type: "string",
|
|
492
|
-
description:
|
|
493
|
-
"Dataset field to render. Metric cards do not support valueField, deltaField, indicators, or standalone card format fields.",
|
|
494
|
-
},
|
|
495
|
-
format: artifactMetricFormat,
|
|
496
|
-
signed: {
|
|
497
|
-
type: ["boolean", "null"],
|
|
498
|
-
description: "Set true for signed deltas or movement metrics.",
|
|
499
|
-
},
|
|
500
|
-
},
|
|
501
|
-
["label", "field"],
|
|
502
|
-
);
|
|
503
|
-
const artifactCard = objectSchema(
|
|
504
|
-
{
|
|
505
|
-
id: { type: "string" },
|
|
506
|
-
description: { type: ["string", "null"] },
|
|
507
|
-
dataset: { type: "string" },
|
|
508
|
-
filter: { type: "object", additionalProperties: scalar },
|
|
509
|
-
metrics: {
|
|
510
|
-
type: "array",
|
|
511
|
-
minItems: 1,
|
|
512
|
-
items: artifactCardMetric,
|
|
513
|
-
description:
|
|
514
|
-
"Metric card values. Use metrics[].field; do not use legacy card fields such as valueField, deltaField, label, title, format, or indicators.",
|
|
515
|
-
},
|
|
516
|
-
},
|
|
517
|
-
["id", "dataset", "metrics"],
|
|
518
|
-
);
|
|
519
|
-
const artifactChartEncoding = objectSchema(
|
|
520
|
-
{
|
|
521
|
-
field: { type: ["string", "null"] },
|
|
522
|
-
fields: { type: "array", items: { type: "string" } },
|
|
523
|
-
type: {
|
|
524
|
-
type: ["string", "null"],
|
|
525
|
-
enum: ["nominal", "ordinal", "quantitative", "temporal", "text", null],
|
|
526
|
-
},
|
|
527
|
-
aggregate: {
|
|
528
|
-
type: ["string", "null"],
|
|
529
|
-
enum: ["none", "sum", "avg", "min", "max", "count", "countDistinct", null],
|
|
530
|
-
},
|
|
531
|
-
format: artifactMetricFormat,
|
|
532
|
-
label: { type: ["string", "null"] },
|
|
533
|
-
unit: { type: ["string", "null"] },
|
|
534
|
-
},
|
|
535
|
-
[],
|
|
536
|
-
true,
|
|
537
|
-
);
|
|
538
|
-
const artifactChartEncodings = objectSchema(
|
|
539
|
-
{
|
|
540
|
-
x: artifactChartEncoding,
|
|
541
|
-
y: artifactChartEncoding,
|
|
542
|
-
color: artifactChartEncoding,
|
|
543
|
-
size: artifactChartEncoding,
|
|
544
|
-
facet: artifactChartEncoding,
|
|
545
|
-
label: artifactChartEncoding,
|
|
546
|
-
tooltip: { type: "array", items: artifactChartEncoding },
|
|
547
|
-
},
|
|
548
|
-
[],
|
|
549
|
-
true,
|
|
550
|
-
);
|
|
551
|
-
const artifactSource = sourceSchema;
|
|
552
|
-
const artifactChart = objectSchema(
|
|
553
|
-
{
|
|
554
|
-
id: { type: "string" },
|
|
555
|
-
title: { type: "string" },
|
|
556
|
-
subtitle: { type: ["string", "null"] },
|
|
557
|
-
showDescription: { type: ["boolean", "null"] },
|
|
558
|
-
headerMarkdown: { type: ["string", "null"] },
|
|
559
|
-
intent: { type: ["string", "null"] },
|
|
560
|
-
question: { type: ["string", "null"] },
|
|
561
|
-
rationale: { type: ["string", "null"] },
|
|
562
|
-
comparisonContext: { type: "object", additionalProperties: true },
|
|
563
|
-
type: {
|
|
564
|
-
type: "string",
|
|
565
|
-
enum: [
|
|
566
|
-
"line",
|
|
567
|
-
"area",
|
|
568
|
-
"stackedArea",
|
|
569
|
-
"bar",
|
|
570
|
-
"horizontalBar",
|
|
571
|
-
"stackedBar",
|
|
572
|
-
"stackedBar100",
|
|
573
|
-
"horizontalStackedBar",
|
|
574
|
-
"horizontalStackedBar100",
|
|
575
|
-
"histogram",
|
|
576
|
-
"scatter",
|
|
577
|
-
"heatmap",
|
|
578
|
-
"pie",
|
|
579
|
-
"leaderboard",
|
|
580
|
-
"sparkline",
|
|
581
|
-
"funnel",
|
|
582
|
-
"waterfall",
|
|
583
|
-
"boxPlot",
|
|
584
|
-
],
|
|
585
|
-
},
|
|
586
|
-
dataset: { type: "string" },
|
|
587
|
-
sourceId: { type: ["string", "null"] },
|
|
588
|
-
source: sourceSchema,
|
|
589
|
-
encodings: artifactChartEncodings,
|
|
590
|
-
xAxisTitle: { type: ["string", "null"] },
|
|
591
|
-
yAxisTitle: { type: ["string", "null"] },
|
|
592
|
-
valueFormat: artifactMetricFormat,
|
|
593
|
-
unit: { type: ["string", "null"] },
|
|
594
|
-
layout: { type: ["string", "null"] },
|
|
595
|
-
combinationRationale: { type: ["string", "null"] },
|
|
596
|
-
maxRows: { type: ["integer", "null"], minimum: 1 },
|
|
597
|
-
referenceLines: { type: "array", items: { type: "object", additionalProperties: true } },
|
|
598
|
-
emptyState: { type: ["string", "null"] },
|
|
599
|
-
compatibleTypes: { type: "array", items: { type: "string" } },
|
|
600
|
-
surface: { type: "object", additionalProperties: true },
|
|
601
|
-
},
|
|
602
|
-
["id", "title", "type", "dataset"],
|
|
603
|
-
true,
|
|
604
|
-
);
|
|
605
|
-
const artifactTableColumn = objectSchema(
|
|
606
|
-
{
|
|
607
|
-
field: {
|
|
608
|
-
type: "string",
|
|
609
|
-
description:
|
|
610
|
-
"Dataset field to display. Artifact report tables use field, not the standalone table widget's key property.",
|
|
611
|
-
},
|
|
612
|
-
label: { type: "string" },
|
|
613
|
-
format: artifactMetricFormat,
|
|
614
|
-
movement: { type: ["boolean", "null"] },
|
|
615
|
-
role: { type: ["string", "null"] },
|
|
616
|
-
semantic: { type: ["string", "null"] },
|
|
617
|
-
type: { type: ["string", "null"], enum: ["text", "number", "percent", "currency", "date", null] },
|
|
618
|
-
unit: { type: ["string", "null"] },
|
|
619
|
-
align: { type: ["string", "null"], enum: ["left", "right", "center", null] },
|
|
620
|
-
},
|
|
621
|
-
["field", "label"],
|
|
622
|
-
);
|
|
623
|
-
const artifactTable = objectSchema(
|
|
624
|
-
{
|
|
625
|
-
id: { type: "string" },
|
|
626
|
-
title: { type: "string" },
|
|
627
|
-
subtitle: { type: ["string", "null"] },
|
|
628
|
-
showDescription: { type: ["boolean", "null"] },
|
|
629
|
-
headerMarkdown: { type: ["string", "null"] },
|
|
630
|
-
dataset: { type: "string" },
|
|
631
|
-
density: { type: ["string", "null"], enum: ["compact", "comfortable", "spacious", null] },
|
|
632
|
-
sourceId: { type: ["string", "null"] },
|
|
633
|
-
source: sourceSchema,
|
|
634
|
-
layout: { type: ["string", "null"] },
|
|
635
|
-
columns: {
|
|
636
|
-
type: "array",
|
|
637
|
-
minItems: 1,
|
|
638
|
-
items: artifactTableColumn,
|
|
639
|
-
description:
|
|
640
|
-
"Artifact table columns. Use columns[].field; do not use columns[].key in manifest.tables.",
|
|
641
|
-
},
|
|
642
|
-
},
|
|
643
|
-
["id", "title", "dataset", "columns"],
|
|
644
|
-
);
|
|
645
|
-
const artifactBlock = objectSchema(
|
|
646
|
-
{
|
|
647
|
-
id: { type: "string" },
|
|
648
|
-
type: {
|
|
649
|
-
type: "string",
|
|
650
|
-
enum: ["markdown", "metric-strip", "chart", "table", "html"],
|
|
651
|
-
},
|
|
652
|
-
body: {
|
|
653
|
-
type: "string",
|
|
654
|
-
description:
|
|
655
|
-
'Block body for markdown or html blocks. Markdown blocks render body; do not use "markdown", "content", "text", "title", "html", or "height" block fields.',
|
|
656
|
-
},
|
|
657
|
-
cardIds: {
|
|
658
|
-
type: "array",
|
|
659
|
-
items: { type: "string" },
|
|
660
|
-
description: 'Required for type "metric-strip"; references manifest.cards[].id.',
|
|
661
|
-
},
|
|
662
|
-
chartId: {
|
|
663
|
-
type: "string",
|
|
664
|
-
description: 'Required for type "chart"; references manifest.charts[].id.',
|
|
665
|
-
},
|
|
666
|
-
tableId: {
|
|
667
|
-
type: "string",
|
|
668
|
-
description: 'Required for type "table"; references manifest.tables[].id.',
|
|
669
|
-
},
|
|
670
|
-
sourceId: { type: ["string", "null"] },
|
|
671
|
-
layout: { type: ["string", "null"] },
|
|
672
|
-
},
|
|
673
|
-
["id", "type"],
|
|
674
|
-
);
|
|
675
|
-
const artifactManifest = {
|
|
676
|
-
type: "object",
|
|
677
|
-
description: [
|
|
678
|
-
"Dashboard/report manifest. All artifact manifests must declare renderable content as",
|
|
679
|
-
"top-level manifest.blocks. The blocks array establishes the artifact reading order.",
|
|
680
|
-
"manifest.title is required. Reports also render it as the first content heading.",
|
|
681
|
-
"Report artifacts must include at least one chart data visualization block.",
|
|
682
|
-
].join(" "),
|
|
683
|
-
properties: {
|
|
684
|
-
version: { type: "integer", enum: [1] },
|
|
685
|
-
surface: { type: ["string", "null"], enum: ["dashboard", "report", null] },
|
|
686
|
-
title: {
|
|
687
|
-
type: "string",
|
|
688
|
-
description:
|
|
689
|
-
"Reader-facing artifact title. Required for dashboards and reports; reports also render it as the first content heading.",
|
|
690
|
-
},
|
|
691
|
-
description: { type: ["string", "null"] },
|
|
692
|
-
generatedAt: { type: ["string", "null"] },
|
|
693
|
-
filters: { type: "array", items: { type: "object", additionalProperties: true }, default: [] },
|
|
694
|
-
cards: { type: "array", items: artifactCard, default: [] },
|
|
695
|
-
charts: { type: "array", items: artifactChart, default: [] },
|
|
696
|
-
tables: { type: "array", items: artifactTable, default: [] },
|
|
697
|
-
sources: { type: "array", items: artifactSource, default: [] },
|
|
698
|
-
blocks: {
|
|
699
|
-
type: "array",
|
|
700
|
-
items: artifactBlock,
|
|
701
|
-
default: [],
|
|
702
|
-
description:
|
|
703
|
-
"Top-level artifact blocks. Required for dashboards and reports; array order is the artifact reading path. Markdown blocks use type \"markdown\" with body: string. Metric strips use type \"metric-strip\" with cardIds. Custom HTML blocks use type \"html\" with raw HTML in body and auto-size to content. The content, text, markdown, title, html, and height fields are not part of the artifact block contract.",
|
|
704
|
-
},
|
|
705
|
-
},
|
|
706
|
-
required: ["version", "title", "blocks"],
|
|
707
|
-
additionalProperties: true,
|
|
708
|
-
};
|
|
709
|
-
const artifactSnapshot = {
|
|
710
|
-
type: "object",
|
|
711
|
-
description: [
|
|
712
|
-
"Bounded artifact snapshot: max 50 datasets and 2,000 rows per dataset.",
|
|
713
|
-
"Canonical shape: snapshot.datasets is an object keyed by dataset id, and each value",
|
|
714
|
-
"is a plain array of reviewed row objects. Example: {\"weekly_revenue\":[{\"week\":\"2026-05-04\",\"arr\":123}]}.",
|
|
715
|
-
"Do not use {columns, rows} for artifact datasets; table-shaped dataset objects are rejected.",
|
|
716
|
-
].join(" "),
|
|
717
|
-
properties: {
|
|
718
|
-
version: { type: "integer", enum: [1] },
|
|
719
|
-
generatedAt: { type: ["string", "null"] },
|
|
720
|
-
status: {
|
|
721
|
-
type: ["string", "null"],
|
|
722
|
-
enum: ["ready", "partial", "blocked", "fixture", null],
|
|
723
|
-
},
|
|
724
|
-
datasets: {
|
|
725
|
-
type: "object",
|
|
726
|
-
additionalProperties: { type: "array", items: row },
|
|
727
|
-
},
|
|
728
|
-
accessIssues: {
|
|
729
|
-
type: "array",
|
|
730
|
-
items: { type: "object", additionalProperties: true },
|
|
731
|
-
default: [],
|
|
732
|
-
description:
|
|
733
|
-
"Required-data access failures for partial or blocked artifacts only. Do not use for optional source limitations or caveats in ready artifacts.",
|
|
734
|
-
},
|
|
735
|
-
},
|
|
736
|
-
required: ["version", "datasets"],
|
|
737
|
-
additionalProperties: true,
|
|
738
|
-
};
|
|
739
|
-
const artifactInputSchema = objectSchema(
|
|
740
|
-
{
|
|
741
|
-
surface: { type: "string", enum: ["dashboard", "report"] },
|
|
742
|
-
manifest: artifactManifest,
|
|
743
|
-
snapshot: artifactSnapshot,
|
|
744
|
-
sources: {
|
|
745
|
-
type: "array",
|
|
746
|
-
items: artifactSource,
|
|
747
|
-
default: [],
|
|
748
|
-
description:
|
|
749
|
-
"Optional inline source query payloads for source ids or paths already declared in manifest.sources.",
|
|
750
|
-
},
|
|
751
|
-
package_info: {
|
|
752
|
-
type: ["object", "null"],
|
|
753
|
-
additionalProperties: true,
|
|
754
|
-
description:
|
|
755
|
-
"Optional provenance metadata for display only. File paths are never read by the MCP server; include bounded source query text explicitly in sources[].query.sql.",
|
|
756
|
-
},
|
|
757
|
-
},
|
|
758
|
-
["surface", "manifest", "snapshot"],
|
|
759
|
-
);
|
|
760
|
-
const artifactPackageExportInputSchema = {
|
|
761
|
-
...artifactInputSchema,
|
|
762
|
-
properties: {
|
|
763
|
-
...artifactInputSchema.properties,
|
|
764
|
-
output_dir: {
|
|
765
|
-
type: ["string", "null"],
|
|
766
|
-
description:
|
|
767
|
-
"Optional local output directory for the staged Site Creator package. Defaults to a deterministic directory under the OS temp directory.",
|
|
768
|
-
},
|
|
769
|
-
site_creator_project_id: {
|
|
770
|
-
type: ["string", "null"],
|
|
771
|
-
description:
|
|
772
|
-
"Optional existing Site Creator project id to write into dist/_appgen_meta/appgarden.json.",
|
|
773
|
-
},
|
|
774
|
-
},
|
|
775
|
-
};
|
|
776
|
-
return [
|
|
777
|
-
tool(
|
|
778
|
-
TOOL_NAMES.validateArtifact,
|
|
779
|
-
"Validate Artifact",
|
|
780
|
-
[
|
|
781
|
-
"Validate a Data Analytics dashboard/report manifest and bounded snapshot",
|
|
782
|
-
"without rendering a hosted widget. Use this first while iterating on artifact shape;",
|
|
783
|
-
"only call render_artifact after validation succeeds to avoid creating",
|
|
784
|
-
"visible broken placeholder cards. snapshot.accessIssues is reserved for missing",
|
|
785
|
-
"required data in partial or blocked artifacts; use markdown body blocks or source notes for optional",
|
|
786
|
-
"source limitations in ready artifacts. All artifacts require manifest.title and",
|
|
787
|
-
"manifest.blocks.",
|
|
788
|
-
].join(" "),
|
|
789
|
-
artifactInputSchema,
|
|
790
|
-
),
|
|
791
|
-
tool(
|
|
792
|
-
TOOL_NAMES.renderArtifact,
|
|
793
|
-
"Render Artifact",
|
|
794
|
-
[
|
|
795
|
-
"Render a hosted Data Analytics dashboard or report artifact from a generated manifest",
|
|
796
|
-
"and bounded snapshot. Use this when the user should see the full dashboard/report app",
|
|
797
|
-
"inside MCP without running a local server. Call validate_artifact first",
|
|
798
|
-
"while iterating on manifest shape so invalid attempts do not create visible broken",
|
|
799
|
-
"artifact cards. snapshot.accessIssues is reserved for missing required data in partial",
|
|
800
|
-
"or blocked artifacts; use markdown body blocks or source notes for optional source limitations in",
|
|
801
|
-
"ready artifacts. All artifacts require manifest.title and manifest.blocks.",
|
|
802
|
-
"Refresh and export controls are v1 agent-mediated prompts; do not",
|
|
803
|
-
"include live connector refresh actions.",
|
|
804
|
-
].join(" "),
|
|
805
|
-
artifactInputSchema,
|
|
806
|
-
ARTIFACT_WIDGET_URI,
|
|
807
|
-
),
|
|
808
|
-
tool(
|
|
809
|
-
TOOL_NAMES.exportArtifactPackage,
|
|
810
|
-
"Export Artifact Package",
|
|
811
|
-
[
|
|
812
|
-
"Materialize the current Data Analytics dashboard/report artifact as a Site Creator-ready",
|
|
813
|
-
"Cloudflare Worker package. This exporter preserves the real MCP artifact app runtime instead",
|
|
814
|
-
"of generating standalone report HTML. It writes dist/server/index.js, dist/client assets,",
|
|
815
|
-
"dist/_appgen_meta/appgarden.json, and an archive that serves /api/manifest, /api/snapshot,",
|
|
816
|
-
"/api/package, /api/source-file, and /api/inline-chart-widget from the validated payload.",
|
|
817
|
-
"Use this before publishing MCP artifact reports through Site Creator; do not hand-roll a",
|
|
818
|
-
"separate HTML renderer.",
|
|
819
|
-
].join(" "),
|
|
820
|
-
artifactPackageExportInputSchema,
|
|
821
|
-
undefined,
|
|
822
|
-
{
|
|
823
|
-
readOnlyHint: false,
|
|
824
|
-
idempotentHint: false,
|
|
825
|
-
},
|
|
826
|
-
),
|
|
827
|
-
tool(
|
|
828
|
-
TOOL_NAMES.renderChart,
|
|
829
|
-
"Render Chart",
|
|
830
|
-
[
|
|
831
|
-
"Render a compact Data Analytics chart from already-reviewed provenance and table data.",
|
|
832
|
-
"Pass source.query.sql with the actual SQL used to produce the chart table, plus source.query.description for the human-readable query summary, an exploration-ready table, chart, and display.",
|
|
833
|
-
"Use the subtitle for a reader-facing insight or takeaway not covered by the title, not for source names, query ids, table names, SQL intent, metric definitions, or provenance.",
|
|
834
|
-
"The table should retain useful",
|
|
835
|
-
"dimensions, measures, time columns, and grouping columns so users can change",
|
|
836
|
-
"chart fields in the expanded widget. Only pass chart.fields.color.field for meaningful",
|
|
837
|
-
"grouping dimensions like segment, product_line, or series; omit it for single-series charts.",
|
|
838
|
-
"For scatter charts, prefer one row per meaningful observation rather than a few broad aggregates; retain a stable point label, numeric x and y measures at the same grain, denominator or sample-size fields, one volume/size candidate, and one interpretable grouping or filter field when safe.",
|
|
839
|
-
"Treat by <dimension> in a visible chart title, subtitle, or header as an encoding contract:",
|
|
840
|
-
"if that dimension is not on an x/y axis, visibly encode it through chart.fields.color.field",
|
|
841
|
-
"or equivalent grouped, stacked, faceted, or direct-label behavior; when grouped, show a",
|
|
842
|
-
"legend or direct labels.",
|
|
843
|
-
"For line, area, stackedArea, and sparkline charts, chart.fields.lineStyle.field can reference a column with solid, dashed, or dotted values.",
|
|
844
|
-
'Use chart.type "bar" plus',
|
|
845
|
-
"chart.options.orientation and chart.options.grouping for bar-family charts.",
|
|
846
|
-
].join(" "),
|
|
847
|
-
chartInputSchema,
|
|
848
|
-
CHART_WIDGET_URI,
|
|
849
|
-
),
|
|
850
|
-
tool(
|
|
851
|
-
TOOL_NAMES.renderTable,
|
|
852
|
-
"Render Table",
|
|
853
|
-
[
|
|
854
|
-
"Render a compact sortable Data Analytics table from already-reviewed query preview rows",
|
|
855
|
-
"or exact lookup rows. Use after running a durable query when the user should see the",
|
|
856
|
-
"sampled rows that support the analysis. Pass source.query.sql with the same actual SQL",
|
|
857
|
-
"source payload shape used by chart widgets so the expanded table detail view can show the query.",
|
|
858
|
-
].join(" "),
|
|
859
|
-
tableInputSchema,
|
|
860
|
-
TABLE_WIDGET_URI,
|
|
861
|
-
),
|
|
862
|
-
];
|
|
863
|
-
}
|
|
864
|
-
|
|
865
|
-
function canonicalWidgetUri(uri) {
|
|
866
|
-
if (uri === ARTIFACT_WIDGET_URI) return ARTIFACT_WIDGET_URI;
|
|
867
|
-
if (uri === TABLE_WIDGET_URI) return TABLE_WIDGET_URI;
|
|
868
|
-
if (uri === CHART_WIDGET_URI) return CHART_WIDGET_URI;
|
|
869
|
-
return null;
|
|
870
|
-
}
|
|
871
|
-
|
|
872
|
-
function widgetResourceMeta(uri) {
|
|
873
|
-
const canonicalUri = canonicalWidgetUri(uri) || uri;
|
|
874
|
-
const description = WIDGET_DESCRIPTIONS[canonicalUri] || "A Data Analytics widget.";
|
|
875
|
-
return {
|
|
876
|
-
"openai/widgetDescription": description,
|
|
877
|
-
"openai/widgetPrefersBorder": false,
|
|
878
|
-
"openai/widgetCSP": { connect_domains: [], resource_domains: WIDGET_RESOURCE_DOMAINS, frame_domains: [] },
|
|
879
|
-
ui: {
|
|
880
|
-
prefersBorder: false,
|
|
881
|
-
csp: { connectDomains: [], resourceDomains: WIDGET_RESOURCE_DOMAINS, frameDomains: [] },
|
|
882
|
-
},
|
|
883
|
-
};
|
|
884
|
-
}
|
|
885
|
-
|
|
886
|
-
function resources() {
|
|
887
|
-
return [
|
|
888
|
-
{
|
|
889
|
-
uri: ARTIFACT_WIDGET_URI,
|
|
890
|
-
name: "datascience_artifact_app",
|
|
891
|
-
title: "Data Analytics artifact app",
|
|
892
|
-
description: WIDGET_DESCRIPTIONS[ARTIFACT_WIDGET_URI],
|
|
893
|
-
mimeType: WIDGET_MIME_TYPE,
|
|
894
|
-
_meta: widgetResourceMeta(ARTIFACT_WIDGET_URI),
|
|
895
|
-
},
|
|
896
|
-
{
|
|
897
|
-
uri: TABLE_WIDGET_URI,
|
|
898
|
-
name: "datascience_table_widget",
|
|
899
|
-
title: "Data Analytics table widget",
|
|
900
|
-
description: WIDGET_DESCRIPTIONS[TABLE_WIDGET_URI],
|
|
901
|
-
mimeType: WIDGET_MIME_TYPE,
|
|
902
|
-
_meta: widgetResourceMeta(TABLE_WIDGET_URI),
|
|
903
|
-
},
|
|
904
|
-
{
|
|
905
|
-
uri: CHART_WIDGET_URI,
|
|
906
|
-
name: "datascience_chart_widget",
|
|
907
|
-
title: "Data Analytics chart widget",
|
|
908
|
-
description: WIDGET_DESCRIPTIONS[CHART_WIDGET_URI],
|
|
909
|
-
mimeType: WIDGET_MIME_TYPE,
|
|
910
|
-
_meta: widgetResourceMeta(CHART_WIDGET_URI),
|
|
911
|
-
},
|
|
912
|
-
];
|
|
913
|
-
}
|
|
914
|
-
|
|
915
|
-
function resourceText(uri) {
|
|
916
|
-
const canonicalUri = canonicalWidgetUri(uri);
|
|
917
|
-
if (canonicalUri === ARTIFACT_WIDGET_URI) {
|
|
918
|
-
return readWidgetHtmlAsset("datascience-artifact-widget.html");
|
|
919
|
-
}
|
|
920
|
-
if (canonicalUri === TABLE_WIDGET_URI) {
|
|
921
|
-
return readWidgetHtmlAsset("datascience-table-widget.html");
|
|
922
|
-
}
|
|
923
|
-
if (canonicalUri === CHART_WIDGET_URI) {
|
|
924
|
-
return readWidgetHtmlAsset("datascience-chart-widget.html");
|
|
925
|
-
}
|
|
926
|
-
throw new Error(`unknown Data Analytics widget resource: ${uri}`);
|
|
927
|
-
}
|
|
928
|
-
|
|
929
|
-
function readWidgetHtmlAsset(fileName) {
|
|
930
|
-
const assetDir = path.join(PLUGIN_ROOT, "assets");
|
|
931
|
-
const prefix = `${fileName}.gz.b64.part`;
|
|
932
|
-
const chunks = fs
|
|
933
|
-
.readdirSync(assetDir)
|
|
934
|
-
.filter((name) => name.startsWith(prefix))
|
|
935
|
-
.sort();
|
|
936
|
-
if (!chunks.length) {
|
|
937
|
-
throw new Error(`missing widget asset chunks for ${fileName}; run npm run build from plugins/datascience`);
|
|
938
|
-
}
|
|
939
|
-
|
|
940
|
-
const encoded = chunks
|
|
941
|
-
.map((name) => fs.readFileSync(path.join(assetDir, name), "utf8").trim())
|
|
942
|
-
.join("");
|
|
943
|
-
return zlib.gunzipSync(Buffer.from(encoded, "base64")).toString("utf8");
|
|
944
|
-
}
|
|
945
|
-
|
|
946
|
-
function knownResourceUri(uri) {
|
|
947
|
-
return Boolean(canonicalWidgetUri(uri));
|
|
948
|
-
}
|
|
949
|
-
|
|
950
|
-
function isPlainObject(value) {
|
|
951
|
-
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
952
|
-
}
|
|
953
|
-
|
|
954
|
-
function asObject(value) {
|
|
955
|
-
return isPlainObject(value) ? { ...value } : {};
|
|
956
|
-
}
|
|
957
|
-
|
|
958
|
-
function asList(value) {
|
|
959
|
-
return Array.isArray(value) ? value : [];
|
|
960
|
-
}
|
|
961
|
-
|
|
962
|
-
function firstValue(...values) {
|
|
963
|
-
for (const value of values) {
|
|
964
|
-
if (value !== null && value !== undefined && value !== "") return value;
|
|
965
|
-
}
|
|
966
|
-
return null;
|
|
967
|
-
}
|
|
968
|
-
|
|
969
|
-
function titleCaseKey(key) {
|
|
970
|
-
return String(key)
|
|
971
|
-
.replace(/_/g, " ")
|
|
972
|
-
.replace(/\b\w/g, (letter) => letter.toUpperCase());
|
|
973
|
-
}
|
|
974
|
-
|
|
975
|
-
function isScalar(value) {
|
|
976
|
-
return value === null || ["string", "number", "boolean"].includes(typeof value);
|
|
977
|
-
}
|
|
978
|
-
|
|
979
|
-
function validateFieldName(key, fieldPath) {
|
|
980
|
-
if (key.length > MAX_WIDGET_FIELD_NAME_CHARS) {
|
|
981
|
-
throw new Error(`${fieldPath} field name exceeds ${MAX_WIDGET_FIELD_NAME_CHARS} characters`);
|
|
982
|
-
}
|
|
983
|
-
if (UNSAFE_FIELD_NAME_PATTERN.test(key)) {
|
|
984
|
-
throw new Error(`${fieldPath} field name ${JSON.stringify(key)} looks unsafe for widget rendering`);
|
|
985
|
-
}
|
|
986
|
-
}
|
|
987
|
-
|
|
988
|
-
function validateSafetyFlags(value, fieldPath) {
|
|
989
|
-
for (const key of ["safe_to_render", "reviewed"]) {
|
|
990
|
-
if (value[key] === false) throw new Error(`${fieldPath}.${key} must not be false for widget rendering`);
|
|
991
|
-
}
|
|
992
|
-
}
|
|
993
|
-
|
|
994
|
-
function validateCellValue(value, fieldPath) {
|
|
995
|
-
if (!isScalar(value)) throw new Error(`${fieldPath} must be a scalar widget cell value`);
|
|
996
|
-
if (typeof value === "string" && value.length > MAX_WIDGET_CELL_STRING_CHARS) {
|
|
997
|
-
throw new Error(`${fieldPath} string exceeds ${MAX_WIDGET_CELL_STRING_CHARS} characters`);
|
|
998
|
-
}
|
|
999
|
-
}
|
|
1000
|
-
|
|
1001
|
-
function validateRows(rows, fieldPath, maxRows = MAX_WIDGET_ROWS, maxCells = MAX_WIDGET_CELLS) {
|
|
1002
|
-
if (rows.length > maxRows) throw new Error(`${fieldPath} has ${rows.length} rows; maximum is ${maxRows}`);
|
|
1003
|
-
let cellCount = 0;
|
|
1004
|
-
rows.forEach((row, rowIndex) => {
|
|
1005
|
-
if (!isPlainObject(row)) throw new Error(`${fieldPath}[${rowIndex}] must be an object`);
|
|
1006
|
-
Object.entries(row).forEach(([key, value]) => {
|
|
1007
|
-
validateFieldName(key, `${fieldPath}[${rowIndex}]`);
|
|
1008
|
-
validateCellValue(value, `${fieldPath}[${rowIndex}].${key}`);
|
|
1009
|
-
cellCount += 1;
|
|
1010
|
-
if (cellCount > maxCells) throw new Error(`${fieldPath} has more than ${maxCells} cells`);
|
|
1011
|
-
});
|
|
1012
|
-
});
|
|
1013
|
-
return cellCount;
|
|
1014
|
-
}
|
|
1015
|
-
|
|
1016
|
-
function validateColumns(columns, fieldPath) {
|
|
1017
|
-
if (columns.length > MAX_WIDGET_COLUMNS) {
|
|
1018
|
-
throw new Error(`${fieldPath} has ${columns.length} columns; maximum is ${MAX_WIDGET_COLUMNS}`);
|
|
1019
|
-
}
|
|
1020
|
-
columns.forEach((column, columnIndex) => {
|
|
1021
|
-
if (!isPlainObject(column)) throw new Error(`${fieldPath}[${columnIndex}] must be an object`);
|
|
1022
|
-
if (typeof column.key === "string") validateFieldName(column.key, `${fieldPath}[${columnIndex}].key`);
|
|
1023
|
-
for (const metadataKey of ["label", "unit"]) {
|
|
1024
|
-
const value = column[metadataKey];
|
|
1025
|
-
if (typeof value === "string" && value.length > MAX_WIDGET_CELL_STRING_CHARS) {
|
|
1026
|
-
throw new Error(
|
|
1027
|
-
`${fieldPath}[${columnIndex}].${metadataKey} exceeds ${MAX_WIDGET_CELL_STRING_CHARS} characters`,
|
|
1028
|
-
);
|
|
1029
|
-
}
|
|
1030
|
-
}
|
|
1031
|
-
});
|
|
1032
|
-
}
|
|
1033
|
-
|
|
1034
|
-
function validateResultTable(resultTable, fieldPath) {
|
|
1035
|
-
if (!isPlainObject(resultTable)) return;
|
|
1036
|
-
validateSafetyFlags(resultTable, fieldPath);
|
|
1037
|
-
validateColumns(asList(resultTable.columns), `${fieldPath}.columns`);
|
|
1038
|
-
validateRows(asList(resultTable.rows), `${fieldPath}.rows`);
|
|
1039
|
-
}
|
|
1040
|
-
|
|
1041
|
-
function validateWidgetPayload(payload) {
|
|
1042
|
-
validateSafetyFlags(payload, "$");
|
|
1043
|
-
const encoded = JSON.stringify(payload);
|
|
1044
|
-
if (Buffer.byteLength(encoded, "utf8") > MAX_WIDGET_PAYLOAD_BYTES) {
|
|
1045
|
-
throw new Error(`widget payload exceeds ${MAX_WIDGET_PAYLOAD_BYTES} bytes; pass a compact reviewed sample`);
|
|
1046
|
-
}
|
|
1047
|
-
if (payload.table) validateResultTable(payload.table, "$.table");
|
|
1048
|
-
else validateResultTable(payload.result_table, "$.result_table");
|
|
1049
|
-
if (Array.isArray(payload.rows)) validateRows(payload.rows, "$.rows");
|
|
1050
|
-
if (Array.isArray(payload.columns)) validateColumns(payload.columns, "$.columns");
|
|
1051
|
-
if (Array.isArray(payload.data)) validateRows(payload.data, "$.data", MAX_WIDGET_DATA_POINTS);
|
|
1052
|
-
}
|
|
1053
|
-
|
|
1054
|
-
function validatedWidgetPayload(payload) {
|
|
1055
|
-
validateWidgetPayload(payload);
|
|
1056
|
-
return payload;
|
|
1057
|
-
}
|
|
1058
|
-
|
|
1059
|
-
function validateIdentifier(value, fieldPath) {
|
|
1060
|
-
if (value == null) return;
|
|
1061
|
-
if (typeof value !== "string") throw new Error(`${fieldPath} must be a string`);
|
|
1062
|
-
validateFieldName(value, fieldPath);
|
|
1063
|
-
}
|
|
1064
|
-
|
|
1065
|
-
function requireIdentifier(value, fieldPath) {
|
|
1066
|
-
if (typeof value !== "string" || !value) throw new Error(`${fieldPath} is required`);
|
|
1067
|
-
validateFieldName(value, fieldPath);
|
|
1068
|
-
}
|
|
1069
|
-
|
|
1070
|
-
function chartEncoding(chart, role) {
|
|
1071
|
-
return isPlainObject(chart.encodings) && isPlainObject(chart.encodings[role]) ? chart.encodings[role] : {};
|
|
1072
|
-
}
|
|
1073
|
-
|
|
1074
|
-
function chartEncodingField(chart, role) {
|
|
1075
|
-
const field = chartEncoding(chart, role).field;
|
|
1076
|
-
return typeof field === "string" && field.trim() ? field : null;
|
|
1077
|
-
}
|
|
1078
|
-
|
|
1079
|
-
function chartEncodingFields(chart, role) {
|
|
1080
|
-
const fields = chartEncoding(chart, role).fields;
|
|
1081
|
-
return Array.isArray(fields) ? fields.filter((field) => typeof field === "string" && field.trim()) : [];
|
|
1082
|
-
}
|
|
1083
|
-
|
|
1084
|
-
function hasChartEncodingSpec(chart) {
|
|
1085
|
-
return Boolean(
|
|
1086
|
-
isPlainObject(chart.encodings) &&
|
|
1087
|
-
chartEncodingField(chart, "x") &&
|
|
1088
|
-
(chartEncodingField(chart, "y") || chartEncodingFields(chart, "y").length),
|
|
1089
|
-
);
|
|
1090
|
-
}
|
|
1091
|
-
|
|
1092
|
-
function validateNoLegacyArtifactChartFields(chart, fieldPath) {
|
|
1093
|
-
for (const key of ["xField", "series"]) {
|
|
1094
|
-
if (chart[key] != null) {
|
|
1095
|
-
throw new Error(`${fieldPath}.${key} is not supported for artifact charts; use encodings`);
|
|
1096
|
-
}
|
|
1097
|
-
}
|
|
1098
|
-
}
|
|
1099
|
-
|
|
1100
|
-
function validateChartEncodingSpec(chart, fieldPath, { requireType = false } = {}) {
|
|
1101
|
-
validateNoLegacyArtifactChartFields(chart, fieldPath);
|
|
1102
|
-
if (!hasChartEncodingSpec(chart)) {
|
|
1103
|
-
throw new Error(`${fieldPath}.encodings.x.field and ${fieldPath}.encodings.y.field or ${fieldPath}.encodings.y.fields are required for artifact charts`);
|
|
1104
|
-
}
|
|
1105
|
-
if (requireType) requireIdentifier(chart.type, `${fieldPath}.type`);
|
|
1106
|
-
requireIdentifier(chart.id, `${fieldPath}.id`);
|
|
1107
|
-
requireIdentifier(chart.dataset, `${fieldPath}.dataset`);
|
|
1108
|
-
requireIdentifier(chartEncodingField(chart, "x"), `${fieldPath}.encodings.x.field`);
|
|
1109
|
-
const yField = chartEncodingField(chart, "y");
|
|
1110
|
-
const yFields = chartEncodingFields(chart, "y");
|
|
1111
|
-
if (yField) validateIdentifier(yField, `${fieldPath}.encodings.y.field`);
|
|
1112
|
-
if (yFields.length) yFields.forEach((field, index) => validateIdentifier(field, `${fieldPath}.encodings.y.fields[${index}]`));
|
|
1113
|
-
if (!yField && !yFields.length) {
|
|
1114
|
-
throw new Error(`${fieldPath}.encodings.y must declare field or fields`);
|
|
1115
|
-
}
|
|
1116
|
-
for (const role of ["color", "size", "facet", "label"]) {
|
|
1117
|
-
validateIdentifier(chartEncodingField(chart, role), `${fieldPath}.encodings.${role}.field`);
|
|
1118
|
-
}
|
|
1119
|
-
asList(chartEncoding(chart, "tooltip")).forEach((tooltip, index) => {
|
|
1120
|
-
if (!isPlainObject(tooltip)) throw new Error(`${fieldPath}.encodings.tooltip[${index}] must be an object`);
|
|
1121
|
-
validateIdentifier(tooltip.field, `${fieldPath}.encodings.tooltip[${index}].field`);
|
|
1122
|
-
});
|
|
1123
|
-
return true;
|
|
1124
|
-
}
|
|
1125
|
-
|
|
1126
|
-
function normalizeSourceList(value) {
|
|
1127
|
-
if (Array.isArray(value)) return value.filter(isPlainObject).map((source) => ({ ...source }));
|
|
1128
|
-
return [];
|
|
1129
|
-
}
|
|
1130
|
-
|
|
1131
|
-
function sourceKey(source) {
|
|
1132
|
-
if (!isPlainObject(source)) return null;
|
|
1133
|
-
for (const key of ["id", "path", "href"]) {
|
|
1134
|
-
if (typeof source[key] === "string" && source[key]) return source[key];
|
|
1135
|
-
}
|
|
1136
|
-
return null;
|
|
1137
|
-
}
|
|
1138
|
-
|
|
1139
|
-
function mergeArtifactSources(manifest, sources) {
|
|
1140
|
-
const outputByKey = new Map();
|
|
1141
|
-
const output = [];
|
|
1142
|
-
|
|
1143
|
-
function addSource(source) {
|
|
1144
|
-
if (!isPlainObject(source)) return;
|
|
1145
|
-
const key = sourceKey(source);
|
|
1146
|
-
if (!key) return;
|
|
1147
|
-
const existing = outputByKey.get(key);
|
|
1148
|
-
const candidate = { ...(existing || {}), ...source };
|
|
1149
|
-
if (existing) Object.assign(existing, candidate);
|
|
1150
|
-
else {
|
|
1151
|
-
outputByKey.set(key, candidate);
|
|
1152
|
-
output.push(candidate);
|
|
1153
|
-
}
|
|
1154
|
-
}
|
|
1155
|
-
|
|
1156
|
-
sources.forEach(addSource);
|
|
1157
|
-
asList(manifest.sources).forEach(addSource);
|
|
1158
|
-
return output;
|
|
1159
|
-
}
|
|
1160
|
-
|
|
1161
|
-
function declaredSourceKeys(manifest) {
|
|
1162
|
-
const keys = new Set();
|
|
1163
|
-
for (const source of asList(manifest.sources)) {
|
|
1164
|
-
if (!isPlainObject(source)) continue;
|
|
1165
|
-
for (const key of ["id", "path", "href"]) {
|
|
1166
|
-
if (typeof source[key] === "string" && source[key]) keys.add(source[key]);
|
|
1167
|
-
}
|
|
1168
|
-
}
|
|
1169
|
-
return keys;
|
|
1170
|
-
}
|
|
1171
|
-
|
|
1172
|
-
function validateManifestSources(manifest) {
|
|
1173
|
-
asList(manifest.sources).forEach((source, index) => {
|
|
1174
|
-
if (!isPlainObject(source)) throw new Error(`$.manifest.sources[${index}] must be an object`);
|
|
1175
|
-
if (source.id != null) validateIdentifier(source.id, `$.manifest.sources[${index}].id`);
|
|
1176
|
-
for (const key of ["label", "path", "href"]) {
|
|
1177
|
-
if (source[key] != null && typeof source[key] !== "string") {
|
|
1178
|
-
throw new Error(`$.manifest.sources[${index}].${key} must be a string`);
|
|
1179
|
-
}
|
|
1180
|
-
}
|
|
1181
|
-
});
|
|
1182
|
-
}
|
|
1183
|
-
|
|
1184
|
-
function stripLeadingSqlComments(text) {
|
|
1185
|
-
return String(text || "")
|
|
1186
|
-
.replace(/^\s*(?:--[^\n]*\n\s*)+/u, "")
|
|
1187
|
-
.replace(/^\s*(?:\/\*[\s\S]*?\*\/\s*)+/u, "")
|
|
1188
|
-
.trim();
|
|
1189
|
-
}
|
|
1190
|
-
|
|
1191
|
-
function looksLikeSqlQuery(text) {
|
|
1192
|
-
const stripped = stripLeadingSqlComments(text);
|
|
1193
|
-
if (!stripped) return false;
|
|
1194
|
-
return (
|
|
1195
|
-
/^(with|select|insert|create|replace|merge|update|delete|copy|explain)\b/iu.test(stripped) ||
|
|
1196
|
-
/\bselect\b[\s\S]+\bfrom\b/iu.test(stripped)
|
|
1197
|
-
);
|
|
1198
|
-
}
|
|
1199
|
-
|
|
1200
|
-
function firstSqlText(...values) {
|
|
1201
|
-
for (const value of values) {
|
|
1202
|
-
if (typeof value === "string" && looksLikeSqlQuery(value)) return value;
|
|
1203
|
-
}
|
|
1204
|
-
return null;
|
|
1205
|
-
}
|
|
1206
|
-
|
|
1207
|
-
function actualSqlTextFromSourceQuery(value) {
|
|
1208
|
-
const query = asObject(value);
|
|
1209
|
-
return firstSqlText(query.sql);
|
|
1210
|
-
}
|
|
1211
|
-
|
|
1212
|
-
function actualSqlTextFromSourceLike(value) {
|
|
1213
|
-
const source = asObject(value);
|
|
1214
|
-
return actualSqlTextFromSourceQuery(source.query);
|
|
1215
|
-
}
|
|
1216
|
-
|
|
1217
|
-
function hasSqlFileSource(value) {
|
|
1218
|
-
const source = asObject(value);
|
|
1219
|
-
return [source.path, source.href].some((item) => {
|
|
1220
|
-
if (typeof item !== "string") return false;
|
|
1221
|
-
const lower = item.toLowerCase();
|
|
1222
|
-
return lower.endsWith(".sql") || lower.includes(".sql?");
|
|
1223
|
-
});
|
|
1224
|
-
}
|
|
1225
|
-
|
|
1226
|
-
function validateActualSqlSource(value, fieldPath, { allowSqlFile = false } = {}) {
|
|
1227
|
-
const sourceQuery = asObject(asObject(value).query);
|
|
1228
|
-
const declaredQuery = sourceQuery.sql;
|
|
1229
|
-
const sqlText = actualSqlTextFromSourceLike(value);
|
|
1230
|
-
if (typeof sqlText === "string" && looksLikeSqlQuery(sqlText)) return;
|
|
1231
|
-
if (typeof declaredQuery === "string" && declaredQuery.trim()) {
|
|
1232
|
-
throw new Error(`${fieldPath} must include the actual SQL query text used to produce widget source data`);
|
|
1233
|
-
}
|
|
1234
|
-
if (allowSqlFile && hasSqlFileSource(value)) return;
|
|
1235
|
-
throw new Error(`${fieldPath} must include the actual SQL query text used to produce widget source data`);
|
|
1236
|
-
}
|
|
1237
|
-
|
|
1238
|
-
function sourceForArtifactItem(item, sources) {
|
|
1239
|
-
if (isPlainObject(item.source)) return item.source;
|
|
1240
|
-
const sourceId = typeof item.sourceId === "string" ? item.sourceId : null;
|
|
1241
|
-
if (!sourceId) return null;
|
|
1242
|
-
return sources.find((source) => isPlainObject(source) && source.id === sourceId) || null;
|
|
1243
|
-
}
|
|
1244
|
-
|
|
1245
|
-
const REPORT_BLOCK_TYPES = new Set([
|
|
1246
|
-
"markdown",
|
|
1247
|
-
"metric-strip",
|
|
1248
|
-
"chart",
|
|
1249
|
-
"table",
|
|
1250
|
-
"html",
|
|
1251
|
-
]);
|
|
1252
|
-
const MARKDOWN_REPORT_BLOCK_TYPES = new Set(["markdown"]);
|
|
1253
|
-
|
|
1254
|
-
function hasVisibleHtml(block) {
|
|
1255
|
-
return typeof block.body === "string" && block.body.trim();
|
|
1256
|
-
}
|
|
1257
|
-
|
|
1258
|
-
function hasVisibleReportText(block) {
|
|
1259
|
-
return typeof block.body === "string" && block.body.trim();
|
|
1260
|
-
}
|
|
1261
|
-
|
|
1262
|
-
function validateReportRenderableCard(card, fieldPath) {
|
|
1263
|
-
if (!isPlainObject(card)) throw new Error(`${fieldPath} must reference a manifest card object`);
|
|
1264
|
-
for (const key of ["id", "dataset"]) {
|
|
1265
|
-
requireIdentifier(card[key], `${fieldPath}.${key}`);
|
|
1266
|
-
}
|
|
1267
|
-
const metrics = asList(card.metrics);
|
|
1268
|
-
if (!metrics.length) throw new Error(`${fieldPath}.metrics must contain at least one metric object`);
|
|
1269
|
-
metrics.forEach((metric, metricIndex) => {
|
|
1270
|
-
if (!isPlainObject(metric)) throw new Error(`${fieldPath}.metrics[${metricIndex}] must be an object`);
|
|
1271
|
-
requireIdentifier(metric.label, `${fieldPath}.metrics[${metricIndex}].label`);
|
|
1272
|
-
requireIdentifier(metric.field, `${fieldPath}.metrics[${metricIndex}].field`);
|
|
1273
|
-
});
|
|
1274
|
-
}
|
|
1275
|
-
|
|
1276
|
-
function validateReportRenderableChart(chart, fieldPath) {
|
|
1277
|
-
if (!isPlainObject(chart)) throw new Error(`${fieldPath} must reference a manifest chart object`);
|
|
1278
|
-
validateChartEncodingSpec(chart, fieldPath, { requireType: true });
|
|
1279
|
-
}
|
|
1280
|
-
|
|
1281
|
-
function validateReportRenderableTable(table, fieldPath) {
|
|
1282
|
-
if (!isPlainObject(table)) throw new Error(`${fieldPath} must reference a manifest table object`);
|
|
1283
|
-
for (const key of ["id", "dataset"]) {
|
|
1284
|
-
requireIdentifier(table[key], `${fieldPath}.${key}`);
|
|
1285
|
-
}
|
|
1286
|
-
const columns = asList(table.columns);
|
|
1287
|
-
if (!columns.length) {
|
|
1288
|
-
throw new Error(`${fieldPath}.columns must contain at least one column object for artifact rendering`);
|
|
1289
|
-
}
|
|
1290
|
-
columns.forEach((column, columnIndex) => {
|
|
1291
|
-
if (!isPlainObject(column)) throw new Error(`${fieldPath}.columns[${columnIndex}] must be an object`);
|
|
1292
|
-
requireIdentifier(column.field, `${fieldPath}.columns[${columnIndex}].field`);
|
|
1293
|
-
});
|
|
1294
|
-
}
|
|
1295
|
-
|
|
1296
|
-
function validateArtifactBlockManifestShape(manifest, surface) {
|
|
1297
|
-
if (manifest.blocks != null && !Array.isArray(manifest.blocks)) {
|
|
1298
|
-
throw new Error("$.manifest.blocks must be an array of top-level artifact blocks");
|
|
1299
|
-
}
|
|
1300
|
-
|
|
1301
|
-
const blocks = asList(manifest.blocks);
|
|
1302
|
-
if (!blocks.length) {
|
|
1303
|
-
throw new Error(
|
|
1304
|
-
"$.manifest.blocks must contain top-level artifact blocks",
|
|
1305
|
-
);
|
|
1306
|
-
}
|
|
1307
|
-
|
|
1308
|
-
const cardsById = new Map(asList(manifest.cards).filter(isPlainObject).map((card) => [card.id, card]));
|
|
1309
|
-
const chartsById = new Map(asList(manifest.charts).filter(isPlainObject).map((chart) => [chart.id, chart]));
|
|
1310
|
-
const tablesById = new Map(asList(manifest.tables).filter(isPlainObject).map((table) => [table.id, table]));
|
|
1311
|
-
const blockIds = new Set();
|
|
1312
|
-
let renderableBlockCount = 0;
|
|
1313
|
-
let chartBlockCount = 0;
|
|
1314
|
-
|
|
1315
|
-
blocks.forEach((block, index) => {
|
|
1316
|
-
if (!isPlainObject(block)) throw new Error(`$.manifest.blocks[${index}] must be an object`);
|
|
1317
|
-
validateIdentifier(block.id, `$.manifest.blocks[${index}].id`);
|
|
1318
|
-
if (typeof block.id !== "string" || !block.id) {
|
|
1319
|
-
throw new Error(`$.manifest.blocks[${index}].id is required for artifact rendering`);
|
|
1320
|
-
}
|
|
1321
|
-
if (blockIds.has(block.id)) throw new Error(`$.manifest.blocks[${index}].id duplicates ${JSON.stringify(block.id)}`);
|
|
1322
|
-
blockIds.add(block.id);
|
|
1323
|
-
|
|
1324
|
-
if (typeof block.type !== "string" || !REPORT_BLOCK_TYPES.has(block.type)) {
|
|
1325
|
-
throw new Error(
|
|
1326
|
-
`$.manifest.blocks[${index}].type must be one of ${Array.from(REPORT_BLOCK_TYPES).join(", ")}`,
|
|
1327
|
-
);
|
|
1328
|
-
}
|
|
1329
|
-
if (MARKDOWN_REPORT_BLOCK_TYPES.has(block.type)) {
|
|
1330
|
-
for (const field of ["bodyMarkdown", "content", "markdown", "text"]) {
|
|
1331
|
-
if (block[field] != null) {
|
|
1332
|
-
throw new Error(`$.manifest.blocks[${index}] uses ${field}, but markdown blocks render body; use body instead`);
|
|
1333
|
-
}
|
|
1334
|
-
}
|
|
1335
|
-
if (block.title != null) {
|
|
1336
|
-
throw new Error(`$.manifest.blocks[${index}] uses title, but markdown blocks render body; include headings in body instead`);
|
|
1337
|
-
}
|
|
1338
|
-
if (block.html != null) {
|
|
1339
|
-
throw new Error(`$.manifest.blocks[${index}] uses html, but markdown blocks render body; use body instead`);
|
|
1340
|
-
}
|
|
1341
|
-
if (typeof block.body !== "string" || !block.body.trim()) {
|
|
1342
|
-
throw new Error(`$.manifest.blocks[${index}].body must be a non-empty string`);
|
|
1343
|
-
}
|
|
1344
|
-
}
|
|
1345
|
-
|
|
1346
|
-
if (MARKDOWN_REPORT_BLOCK_TYPES.has(block.type) && hasVisibleReportText(block)) renderableBlockCount += 1;
|
|
1347
|
-
if (block.type === "html") {
|
|
1348
|
-
if (block.height != null) {
|
|
1349
|
-
throw new Error(`$.manifest.blocks[${index}].height is not supported for html blocks; html blocks auto-size to content`);
|
|
1350
|
-
}
|
|
1351
|
-
for (const field of ["bodyMarkdown", "content", "html", "markdown", "text", "title"]) {
|
|
1352
|
-
if (block[field] != null) {
|
|
1353
|
-
throw new Error(`$.manifest.blocks[${index}] uses ${field}, but html blocks render body; use body instead`);
|
|
1354
|
-
}
|
|
1355
|
-
}
|
|
1356
|
-
if (!hasVisibleHtml(block)) {
|
|
1357
|
-
throw new Error(`$.manifest.blocks[${index}].body must be a non-empty HTML string`);
|
|
1358
|
-
}
|
|
1359
|
-
renderableBlockCount += 1;
|
|
1360
|
-
}
|
|
1361
|
-
if (block.type === "metric-strip") {
|
|
1362
|
-
const cardIds = asList(block.cardIds);
|
|
1363
|
-
if (!cardIds.length) {
|
|
1364
|
-
throw new Error(`$.manifest.blocks[${index}].cardIds must reference at least one manifest card`);
|
|
1365
|
-
}
|
|
1366
|
-
cardIds.forEach((cardId, cardIndex) => {
|
|
1367
|
-
validateIdentifier(cardId, `$.manifest.blocks[${index}].cardIds[${cardIndex}]`);
|
|
1368
|
-
if (!cardsById.has(cardId)) {
|
|
1369
|
-
throw new Error(`$.manifest.blocks[${index}].cardIds[${cardIndex}] does not match a manifest card`);
|
|
1370
|
-
}
|
|
1371
|
-
validateReportRenderableCard(cardsById.get(cardId), `$.manifest.cards[${JSON.stringify(cardId)}]`);
|
|
1372
|
-
});
|
|
1373
|
-
renderableBlockCount += 1;
|
|
1374
|
-
}
|
|
1375
|
-
if (block.type === "chart") {
|
|
1376
|
-
validateIdentifier(block.chartId, `$.manifest.blocks[${index}].chartId`);
|
|
1377
|
-
if (!chartsById.has(block.chartId)) throw new Error(`$.manifest.blocks[${index}].chartId does not match a manifest chart`);
|
|
1378
|
-
validateReportRenderableChart(chartsById.get(block.chartId), `$.manifest.charts[${JSON.stringify(block.chartId)}]`);
|
|
1379
|
-
renderableBlockCount += 1;
|
|
1380
|
-
chartBlockCount += 1;
|
|
1381
|
-
}
|
|
1382
|
-
if (block.type === "table") {
|
|
1383
|
-
validateIdentifier(block.tableId, `$.manifest.blocks[${index}].tableId`);
|
|
1384
|
-
if (!tablesById.has(block.tableId)) throw new Error(`$.manifest.blocks[${index}].tableId does not match a manifest table`);
|
|
1385
|
-
validateReportRenderableTable(tablesById.get(block.tableId), `$.manifest.tables[${JSON.stringify(block.tableId)}]`);
|
|
1386
|
-
renderableBlockCount += 1;
|
|
1387
|
-
}
|
|
1388
|
-
});
|
|
1389
|
-
|
|
1390
|
-
if (!renderableBlockCount) {
|
|
1391
|
-
throw new Error("$.manifest.blocks does not contain any renderable artifact content");
|
|
1392
|
-
}
|
|
1393
|
-
if (surface === "report" && !chartBlockCount) {
|
|
1394
|
-
throw new Error("$.manifest.blocks must include at least one chart block for report artifacts");
|
|
1395
|
-
}
|
|
1396
|
-
}
|
|
1397
|
-
|
|
1398
|
-
function validateArtifactManifest(manifest, surface) {
|
|
1399
|
-
if (!isPlainObject(manifest)) throw new Error("$.manifest must be an object");
|
|
1400
|
-
if (manifest.version !== 1) throw new Error("$.manifest.version must be 1");
|
|
1401
|
-
const manifestSurface = manifest.surface;
|
|
1402
|
-
if (manifestSurface != null && manifestSurface !== surface) {
|
|
1403
|
-
throw new Error("$.manifest.surface must match $.surface");
|
|
1404
|
-
}
|
|
1405
|
-
if (typeof manifest.title !== "string" || !manifest.title.trim()) {
|
|
1406
|
-
throw new Error("$.manifest.title is required for artifact rendering");
|
|
1407
|
-
}
|
|
1408
|
-
if (manifest.audience != null) {
|
|
1409
|
-
throw new Error("$.manifest.audience is not supported for artifact rendering");
|
|
1410
|
-
}
|
|
1411
|
-
if (manifest.freshness != null) {
|
|
1412
|
-
throw new Error("$.manifest.freshness is not supported for artifact rendering");
|
|
1413
|
-
}
|
|
1414
|
-
validateSafetyFlags(manifest, "$.manifest");
|
|
1415
|
-
validateManifestSources(manifest);
|
|
1416
|
-
|
|
1417
|
-
asList(manifest.cards).forEach((card, index) => {
|
|
1418
|
-
if (!isPlainObject(card)) throw new Error(`$.manifest.cards[${index}] must be an object`);
|
|
1419
|
-
for (const key of ["valueField", "format", "label", "title", "indicators", "deltaField", "deltaLabel"]) {
|
|
1420
|
-
if (card[key] != null) throw new Error(`$.manifest.cards[${index}].${key} is not supported; use metrics[]`);
|
|
1421
|
-
}
|
|
1422
|
-
for (const key of ["id", "dataset"]) {
|
|
1423
|
-
validateIdentifier(card[key], `$.manifest.cards[${index}].${key}`);
|
|
1424
|
-
}
|
|
1425
|
-
if (!Array.isArray(card.metrics) || !card.metrics.length) {
|
|
1426
|
-
throw new Error(`$.manifest.cards[${index}].metrics must contain at least one metric object`);
|
|
1427
|
-
}
|
|
1428
|
-
card.metrics.forEach((metric, metricIndex) => {
|
|
1429
|
-
if (!isPlainObject(metric)) {
|
|
1430
|
-
throw new Error(`$.manifest.cards[${index}].metrics[${metricIndex}] must be an object`);
|
|
1431
|
-
}
|
|
1432
|
-
validateIdentifier(metric.label, `$.manifest.cards[${index}].metrics[${metricIndex}].label`);
|
|
1433
|
-
validateIdentifier(metric.field, `$.manifest.cards[${index}].metrics[${metricIndex}].field`);
|
|
1434
|
-
validateIdentifier(metric.format, `$.manifest.cards[${index}].metrics[${metricIndex}].format`);
|
|
1435
|
-
if (metric.signed != null && typeof metric.signed !== "boolean") {
|
|
1436
|
-
throw new Error(`$.manifest.cards[${index}].metrics[${metricIndex}].signed must be a boolean`);
|
|
1437
|
-
}
|
|
1438
|
-
if (metric.movement != null) {
|
|
1439
|
-
throw new Error(`$.manifest.cards[${index}].metrics[${metricIndex}].movement is not supported; use signed`);
|
|
1440
|
-
}
|
|
1441
|
-
});
|
|
1442
|
-
});
|
|
1443
|
-
|
|
1444
|
-
asList(manifest.charts).forEach((chart, index) => {
|
|
1445
|
-
if (!isPlainObject(chart)) throw new Error(`$.manifest.charts[${index}] must be an object`);
|
|
1446
|
-
validateChartEncodingSpec(chart, `$.manifest.charts[${index}]`, { requireType: true });
|
|
1447
|
-
});
|
|
1448
|
-
|
|
1449
|
-
asList(manifest.tables).forEach((table, index) => {
|
|
1450
|
-
if (!isPlainObject(table)) throw new Error(`$.manifest.tables[${index}] must be an object`);
|
|
1451
|
-
for (const key of ["id", "dataset"]) validateIdentifier(table[key], `$.manifest.tables[${index}].${key}`);
|
|
1452
|
-
asList(table.columns).forEach((column, columnIndex) => {
|
|
1453
|
-
if (!isPlainObject(column)) {
|
|
1454
|
-
throw new Error(`$.manifest.tables[${index}].columns[${columnIndex}] must be an object`);
|
|
1455
|
-
}
|
|
1456
|
-
validateIdentifier(column.field, `$.manifest.tables[${index}].columns[${columnIndex}].field`);
|
|
1457
|
-
});
|
|
1458
|
-
});
|
|
1459
|
-
|
|
1460
|
-
validateArtifactBlockManifestShape(manifest, surface);
|
|
1461
|
-
}
|
|
1462
|
-
|
|
1463
|
-
function validateArtifactSnapshot(snapshot) {
|
|
1464
|
-
if (!isPlainObject(snapshot)) throw new Error("$.snapshot must be an object");
|
|
1465
|
-
if (snapshot.version !== 1) throw new Error("$.snapshot.version must be 1");
|
|
1466
|
-
const status = snapshot.status;
|
|
1467
|
-
if (status != null && !["ready", "partial", "blocked", "fixture"].includes(status)) {
|
|
1468
|
-
throw new Error("$.snapshot.status must be ready, partial, blocked, or fixture");
|
|
1469
|
-
}
|
|
1470
|
-
if (!isPlainObject(snapshot.datasets)) throw new Error("$.snapshot.datasets must be an object");
|
|
1471
|
-
if (snapshot.definitions != null) {
|
|
1472
|
-
throw new Error("$.snapshot.definitions is not supported for artifact rendering");
|
|
1473
|
-
}
|
|
1474
|
-
const accessIssues = asList(snapshot.accessIssues);
|
|
1475
|
-
if (accessIssues.length && status !== "partial" && status !== "blocked") {
|
|
1476
|
-
throw new Error(
|
|
1477
|
-
"$.snapshot.accessIssues is only allowed when $.snapshot.status is partial or blocked; use a markdown body block or manifest.sources for optional source limitations in ready artifacts",
|
|
1478
|
-
);
|
|
1479
|
-
}
|
|
1480
|
-
const datasets = Object.entries(snapshot.datasets);
|
|
1481
|
-
if (datasets.length > MAX_ARTIFACT_DATASETS) {
|
|
1482
|
-
throw new Error(`$.snapshot.datasets has ${datasets.length} datasets; maximum is ${MAX_ARTIFACT_DATASETS}`);
|
|
1483
|
-
}
|
|
1484
|
-
datasets.forEach(([datasetId, rows]) => {
|
|
1485
|
-
validateIdentifier(datasetId, `$.snapshot.datasets.${datasetId}`);
|
|
1486
|
-
if (!Array.isArray(rows)) {
|
|
1487
|
-
throw new Error(`$.snapshot.datasets.${datasetId} must be an array of row objects`);
|
|
1488
|
-
}
|
|
1489
|
-
validateRows(
|
|
1490
|
-
rows,
|
|
1491
|
-
`$.snapshot.datasets.${datasetId}`,
|
|
1492
|
-
MAX_ARTIFACT_ROWS_PER_DATASET,
|
|
1493
|
-
Number.POSITIVE_INFINITY,
|
|
1494
|
-
);
|
|
1495
|
-
});
|
|
1496
|
-
}
|
|
1497
|
-
|
|
1498
|
-
function normalizeArtifactSnapshot(snapshot) {
|
|
1499
|
-
const datasets = {};
|
|
1500
|
-
for (const [datasetId, value] of Object.entries(asObject(snapshot.datasets))) {
|
|
1501
|
-
const rows = isPlainObject(value) ? value.rows : value;
|
|
1502
|
-
datasets[datasetId] = asList(rows).filter(isPlainObject).map((row) => ({ ...row }));
|
|
1503
|
-
}
|
|
1504
|
-
return {
|
|
1505
|
-
...snapshot,
|
|
1506
|
-
datasets,
|
|
1507
|
-
};
|
|
1508
|
-
}
|
|
1509
|
-
|
|
1510
|
-
function normalizeArtifactManifest(manifest, surface) {
|
|
1511
|
-
const normalized = {
|
|
1512
|
-
...manifest,
|
|
1513
|
-
surface: manifest.surface ?? surface,
|
|
1514
|
-
};
|
|
1515
|
-
if (surface === "dashboard") {
|
|
1516
|
-
normalized.blocks = Array.isArray(manifest.blocks) ? manifest.blocks : [];
|
|
1517
|
-
}
|
|
1518
|
-
if (Array.isArray(manifest.blocks)) {
|
|
1519
|
-
normalized.blocks = manifest.blocks.map((block) => {
|
|
1520
|
-
if (!isPlainObject(block)) return block;
|
|
1521
|
-
return { ...block };
|
|
1522
|
-
});
|
|
1523
|
-
}
|
|
1524
|
-
return normalized;
|
|
1525
|
-
}
|
|
1526
|
-
|
|
1527
|
-
function validateArtifactSources(payload) {
|
|
1528
|
-
const declared = declaredSourceKeys(payload.manifest);
|
|
1529
|
-
let totalInlineChars = 0;
|
|
1530
|
-
payload.sources.forEach((source, index) => {
|
|
1531
|
-
if (!isPlainObject(source)) throw new Error(`$.sources[${index}] must be an object`);
|
|
1532
|
-
if (source.id != null) validateIdentifier(source.id, `$.sources[${index}].id`);
|
|
1533
|
-
const queryText = asObject(source.query).sql;
|
|
1534
|
-
const hasInlineQuery = typeof queryText === "string" && queryText.length > 0;
|
|
1535
|
-
if (!hasInlineQuery) return;
|
|
1536
|
-
const id = typeof source.id === "string" ? source.id : "";
|
|
1537
|
-
const pathValue = typeof source.path === "string" ? source.path : "";
|
|
1538
|
-
if (!declared.has(id) && !declared.has(pathValue)) {
|
|
1539
|
-
throw new Error(`$.sources[${index}].query.sql is only allowed for sources declared in manifest.sources`);
|
|
1540
|
-
}
|
|
1541
|
-
totalInlineChars += queryText.length;
|
|
1542
|
-
if (totalInlineChars > MAX_ARTIFACT_INLINE_SOURCE_CHARS) {
|
|
1543
|
-
throw new Error(
|
|
1544
|
-
`inline source text exceeds ${MAX_ARTIFACT_INLINE_SOURCE_CHARS} characters; pass a bounded source excerpt`,
|
|
1545
|
-
);
|
|
1546
|
-
}
|
|
1547
|
-
});
|
|
1548
|
-
}
|
|
1549
|
-
|
|
1550
|
-
function validateArtifactDatasetReferences(manifest, snapshot) {
|
|
1551
|
-
const datasets = new Set(Object.keys(snapshot.datasets || {}));
|
|
1552
|
-
const references = [
|
|
1553
|
-
...asList(manifest.cards).map((item) => item.dataset),
|
|
1554
|
-
...asList(manifest.charts).map((item) => item.dataset),
|
|
1555
|
-
...asList(manifest.tables).map((item) => item.dataset),
|
|
1556
|
-
].filter(Boolean);
|
|
1557
|
-
for (const dataset of references) {
|
|
1558
|
-
if (!datasets.has(dataset) && snapshot.status !== "blocked") {
|
|
1559
|
-
throw new Error(`$.snapshot.datasets is missing dataset ${JSON.stringify(dataset)}`);
|
|
1560
|
-
}
|
|
1561
|
-
}
|
|
1562
|
-
}
|
|
1563
|
-
|
|
1564
|
-
function validateArtifactChartDataCompatibility(manifest, snapshot) {
|
|
1565
|
-
const datasets = snapshot.datasets || {};
|
|
1566
|
-
asList(manifest.charts).forEach((chart, index) => {
|
|
1567
|
-
if (!isPlainObject(chart)) return;
|
|
1568
|
-
const rows = asList(datasets[chart.dataset]).filter(isPlainObject);
|
|
1569
|
-
if (!rows.length) return;
|
|
1570
|
-
if (hasChartEncodingSpec(chart)) {
|
|
1571
|
-
const yFields = [
|
|
1572
|
-
chartEncodingField(chart, "y"),
|
|
1573
|
-
...chartEncodingFields(chart, "y"),
|
|
1574
|
-
].filter(Boolean);
|
|
1575
|
-
yFields.forEach((field, fieldIndex) => {
|
|
1576
|
-
const hasNumericValue = rows.some((row) => numeric(row[field]) != null);
|
|
1577
|
-
if (!hasNumericValue) {
|
|
1578
|
-
throw new Error(
|
|
1579
|
-
`$.manifest.charts[${index}].encodings.y${fieldIndex ? ".fields" : ".field"} ${JSON.stringify(field)} ` +
|
|
1580
|
-
"must reference a numeric dataset field with at least one numeric value",
|
|
1581
|
-
);
|
|
1582
|
-
}
|
|
1583
|
-
});
|
|
1584
|
-
return;
|
|
1585
|
-
}
|
|
1586
|
-
asList(chart.series).forEach((item, seriesIndex) => {
|
|
1587
|
-
if (!isPlainObject(item) || typeof item.field !== "string") return;
|
|
1588
|
-
const hasNumericValue = rows.some((row) => numeric(row[item.field]) != null);
|
|
1589
|
-
if (!hasNumericValue) {
|
|
1590
|
-
throw new Error(
|
|
1591
|
-
`$.manifest.charts[${index}].series[${seriesIndex}].field ${JSON.stringify(item.field)} ` +
|
|
1592
|
-
"must reference a numeric dataset field with at least one numeric value",
|
|
1593
|
-
);
|
|
1594
|
-
}
|
|
1595
|
-
});
|
|
1596
|
-
});
|
|
1597
|
-
}
|
|
1598
|
-
|
|
1599
|
-
function validateArtifactSourceQueries(manifest, sources) {
|
|
1600
|
-
const chartBlocks = new Set(
|
|
1601
|
-
asList(manifest.blocks)
|
|
1602
|
-
.filter((block) => isPlainObject(block) && block.type === "chart" && typeof block.chartId === "string")
|
|
1603
|
-
.map((block) => block.chartId),
|
|
1604
|
-
);
|
|
1605
|
-
asList(manifest.charts).forEach((chart, index) => {
|
|
1606
|
-
if (!isPlainObject(chart)) return;
|
|
1607
|
-
if (chartBlocks.size && !chartBlocks.has(chart.id)) return;
|
|
1608
|
-
const chartSource = sourceForArtifactItem(chart, sources);
|
|
1609
|
-
validateActualSqlSource(chartSource, `$.manifest.charts[${index}].source`, {
|
|
1610
|
-
allowSqlFile: true,
|
|
1611
|
-
});
|
|
1612
|
-
});
|
|
1613
|
-
const tableBlocks = new Set(
|
|
1614
|
-
asList(manifest.blocks)
|
|
1615
|
-
.filter((block) => isPlainObject(block) && block.type === "table" && typeof block.tableId === "string")
|
|
1616
|
-
.map((block) => block.tableId),
|
|
1617
|
-
);
|
|
1618
|
-
asList(manifest.tables).forEach((table, index) => {
|
|
1619
|
-
if (!isPlainObject(table)) return;
|
|
1620
|
-
if (tableBlocks.size && !tableBlocks.has(table.id)) return;
|
|
1621
|
-
const tableSource = sourceForArtifactItem(table, sources);
|
|
1622
|
-
validateActualSqlSource(tableSource, `$.manifest.tables[${index}].source`, {
|
|
1623
|
-
allowSqlFile: true,
|
|
1624
|
-
});
|
|
1625
|
-
});
|
|
1626
|
-
}
|
|
1627
|
-
|
|
1628
|
-
function validateArtifactPayload(payload) {
|
|
1629
|
-
validateSafetyFlags(payload, "$");
|
|
1630
|
-
if (payload.surface !== "dashboard" && payload.surface !== "report") {
|
|
1631
|
-
throw new Error('surface must be "dashboard" or "report"');
|
|
1632
|
-
}
|
|
1633
|
-
validateArtifactManifest(payload.manifest, payload.surface);
|
|
1634
|
-
validateArtifactSnapshot(payload.snapshot);
|
|
1635
|
-
validateArtifactDatasetReferences(payload.manifest, payload.snapshot);
|
|
1636
|
-
validateArtifactChartDataCompatibility(payload.manifest, payload.snapshot);
|
|
1637
|
-
validateArtifactSources(payload);
|
|
1638
|
-
validateArtifactSourceQueries(payload.manifest, payload.sources);
|
|
1639
|
-
const encoded = JSON.stringify(payload);
|
|
1640
|
-
if (Buffer.byteLength(encoded, "utf8") > MAX_ARTIFACT_PAYLOAD_BYTES) {
|
|
1641
|
-
throw new Error(`artifact payload exceeds ${MAX_ARTIFACT_PAYLOAD_BYTES} bytes; pass a bounded snapshot`);
|
|
1642
|
-
}
|
|
1643
|
-
}
|
|
1644
|
-
|
|
1645
|
-
function buildArtifactPayload(args) {
|
|
1646
|
-
if (args.initial_view != null) {
|
|
1647
|
-
throw new Error("initial_view is not supported for artifact rendering; artifacts open fullscreen by default");
|
|
1648
|
-
}
|
|
1649
|
-
const surface = args.surface;
|
|
1650
|
-
const manifest = normalizeArtifactManifest(asObject(args.manifest), surface);
|
|
1651
|
-
const snapshot = asObject(args.snapshot);
|
|
1652
|
-
const sources = normalizeSourceList(args.sources);
|
|
1653
|
-
const packageInfo = asObject(args.package_info);
|
|
1654
|
-
const mergedSources = mergeArtifactSources(manifest, sources);
|
|
1655
|
-
return {
|
|
1656
|
-
ok: true,
|
|
1657
|
-
widget_type: "artifact",
|
|
1658
|
-
surface,
|
|
1659
|
-
manifest,
|
|
1660
|
-
snapshot,
|
|
1661
|
-
sources: mergedSources,
|
|
1662
|
-
package_info: Object.keys(packageInfo).length ? packageInfo : null,
|
|
1663
|
-
packageInfo: Object.keys(packageInfo).length ? packageInfo : null,
|
|
1664
|
-
};
|
|
1665
|
-
}
|
|
1666
|
-
|
|
1667
|
-
function validatedArtifactPayload(payload) {
|
|
1668
|
-
validateArtifactPayload(payload);
|
|
1669
|
-
return payload;
|
|
1670
|
-
}
|
|
1671
|
-
|
|
1672
|
-
function artifactId(payload) {
|
|
1673
|
-
return crypto.createHash("sha256").update(JSON.stringify(payload)).digest("hex");
|
|
1674
|
-
}
|
|
1675
|
-
|
|
1676
|
-
function safeHtmlJson(value) {
|
|
1677
|
-
return JSON.stringify(value).replace(/</g, "\\u003c").replace(/>/g, "\\u003e");
|
|
1678
|
-
}
|
|
1679
|
-
|
|
1680
|
-
function hostedArtifactHtml(html, payload) {
|
|
1681
|
-
const hosting = {
|
|
1682
|
-
mode: "site_creator",
|
|
1683
|
-
readOnly: true,
|
|
1684
|
-
controls: {
|
|
1685
|
-
edit: false,
|
|
1686
|
-
exportHostedLink: false,
|
|
1687
|
-
hostedLink: false,
|
|
1688
|
-
},
|
|
1689
|
-
};
|
|
1690
|
-
const script = [
|
|
1691
|
-
"<script>",
|
|
1692
|
-
`window.__DATASCIENCE_ARTIFACT_HOSTING__=${safeHtmlJson(hosting)};`,
|
|
1693
|
-
"window.openai=Object.assign({},window.openai,{",
|
|
1694
|
-
`toolOutput:${safeHtmlJson(payload)},`,
|
|
1695
|
-
`toolResponseMetadata:${safeHtmlJson(payload)},`,
|
|
1696
|
-
'availableDisplayModes:["inline","fullscreen"],',
|
|
1697
|
-
'displayMode:"fullscreen",',
|
|
1698
|
-
'hostContext:{availableDisplayModes:["inline","fullscreen"],displayMode:"fullscreen"}',
|
|
1699
|
-
"});",
|
|
1700
|
-
"</script>",
|
|
1701
|
-
].join("");
|
|
1702
|
-
if (/<head(\s[^>]*)?>/i.test(html)) {
|
|
1703
|
-
return html.replace(/<head(\s[^>]*)?>/i, (match) => `${match}\n${script}`);
|
|
1704
|
-
}
|
|
1705
|
-
return `${script}\n${html}`;
|
|
1706
|
-
}
|
|
1707
|
-
|
|
1708
|
-
function inlineSourceText(source) {
|
|
1709
|
-
if (!isPlainObject(source)) return null;
|
|
1710
|
-
const query = asObject(source.query);
|
|
1711
|
-
if (typeof query.sql === "string") return query.sql;
|
|
1712
|
-
if (typeof source.text === "string") return source.text;
|
|
1713
|
-
if (typeof source.source === "string") return source.source;
|
|
1714
|
-
return null;
|
|
1715
|
-
}
|
|
1716
|
-
|
|
1717
|
-
function sourceLookupEntries(payload) {
|
|
1718
|
-
const entries = [];
|
|
1719
|
-
const seen = new Set();
|
|
1720
|
-
for (const source of [...asList(payload.manifest?.sources), ...asList(payload.sources)]) {
|
|
1721
|
-
if (!isPlainObject(source)) continue;
|
|
1722
|
-
const text = inlineSourceText(source);
|
|
1723
|
-
if (text == null) continue;
|
|
1724
|
-
for (const key of [source.path, source.id, source.href]) {
|
|
1725
|
-
if (typeof key !== "string" || !key || seen.has(key)) continue;
|
|
1726
|
-
seen.add(key);
|
|
1727
|
-
entries.push([key, text]);
|
|
1728
|
-
}
|
|
1729
|
-
}
|
|
1730
|
-
return entries;
|
|
1731
|
-
}
|
|
1732
|
-
|
|
1733
|
-
function normalizeOutputDir(outputDir, id) {
|
|
1734
|
-
if (outputDir == null || outputDir === "") {
|
|
1735
|
-
return {
|
|
1736
|
-
outputDir: path.join(os.tmpdir(), `datascience-artifact-site-${id.slice(0, 12)}`),
|
|
1737
|
-
isDefault: true,
|
|
1738
|
-
};
|
|
1739
|
-
}
|
|
1740
|
-
if (typeof outputDir !== "string") {
|
|
1741
|
-
throw new Error("output_dir must be a string when provided");
|
|
1742
|
-
}
|
|
1743
|
-
return { outputDir: path.resolve(outputDir), isDefault: false };
|
|
1744
|
-
}
|
|
1745
|
-
|
|
1746
|
-
function ensureWritableOutputDir(outputDir, isDefault) {
|
|
1747
|
-
if (!fs.existsSync(outputDir)) {
|
|
1748
|
-
fs.mkdirSync(outputDir, { recursive: true });
|
|
1749
|
-
return;
|
|
1750
|
-
}
|
|
1751
|
-
const entries = fs.readdirSync(outputDir);
|
|
1752
|
-
if (!entries.length) return;
|
|
1753
|
-
if (isDefault && path.basename(outputDir).startsWith("datascience-artifact-site-")) {
|
|
1754
|
-
fs.rmSync(outputDir, { recursive: true, force: true });
|
|
1755
|
-
fs.mkdirSync(outputDir, { recursive: true });
|
|
1756
|
-
return;
|
|
1757
|
-
}
|
|
1758
|
-
throw new Error(`output_dir already exists and is not empty: ${outputDir}`);
|
|
1759
|
-
}
|
|
1760
|
-
|
|
1761
|
-
function writeJsonFile(filePath, value) {
|
|
1762
|
-
fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
|
1763
|
-
}
|
|
1764
|
-
|
|
1765
|
-
function workerSource({ artifactHtml, chartHtml, manifest, snapshot, packageInfo, sourceEntries }) {
|
|
1766
|
-
return `"use strict";
|
|
1767
|
-
|
|
1768
|
-
const INDEX_HTML = ${JSON.stringify(artifactHtml)};
|
|
1769
|
-
const CHART_WIDGET_HTML = ${JSON.stringify(chartHtml)};
|
|
1770
|
-
const MANIFEST = ${JSON.stringify(manifest)};
|
|
1771
|
-
const SNAPSHOT = ${JSON.stringify(snapshot)};
|
|
1772
|
-
const PACKAGE_INFO = ${JSON.stringify(packageInfo)};
|
|
1773
|
-
const SOURCE_TEXT = new Map(${JSON.stringify(sourceEntries)});
|
|
1774
|
-
|
|
1775
|
-
function jsonResponse(body, init = {}) {
|
|
1776
|
-
return new Response(JSON.stringify(body), {
|
|
1777
|
-
status: init.status || 200,
|
|
1778
|
-
headers: {
|
|
1779
|
-
"content-type": "application/json; charset=utf-8",
|
|
1780
|
-
"cache-control": "no-store",
|
|
1781
|
-
...(init.headers || {}),
|
|
1782
|
-
},
|
|
1783
|
-
});
|
|
1784
|
-
}
|
|
1785
|
-
|
|
1786
|
-
function textResponse(body, init = {}) {
|
|
1787
|
-
return new Response(String(body == null ? "" : body), {
|
|
1788
|
-
status: init.status || 200,
|
|
1789
|
-
headers: {
|
|
1790
|
-
"content-type": init.contentType || "text/plain; charset=utf-8",
|
|
1791
|
-
"cache-control": "no-store",
|
|
1792
|
-
...(init.headers || {}),
|
|
1793
|
-
},
|
|
1794
|
-
});
|
|
1795
|
-
}
|
|
1796
|
-
|
|
1797
|
-
function sourceTextFor(url) {
|
|
1798
|
-
const key =
|
|
1799
|
-
url.searchParams.get("path") ||
|
|
1800
|
-
url.searchParams.get("id") ||
|
|
1801
|
-
url.searchParams.get("source") ||
|
|
1802
|
-
url.searchParams.get("sourceId") ||
|
|
1803
|
-
"";
|
|
1804
|
-
return SOURCE_TEXT.get(key) || null;
|
|
1805
|
-
}
|
|
1806
|
-
|
|
1807
|
-
export default {
|
|
1808
|
-
async fetch(request) {
|
|
1809
|
-
const url = new URL(request.url);
|
|
1810
|
-
if (url.pathname === "/api/manifest") return jsonResponse(MANIFEST);
|
|
1811
|
-
if (url.pathname === "/api/snapshot") return jsonResponse(SNAPSHOT);
|
|
1812
|
-
if (url.pathname === "/api/package") return jsonResponse(PACKAGE_INFO);
|
|
1813
|
-
if (url.pathname === "/api/inline-chart-widget") {
|
|
1814
|
-
return textResponse(CHART_WIDGET_HTML, { contentType: "text/html; charset=utf-8" });
|
|
1815
|
-
}
|
|
1816
|
-
if (url.pathname === "/api/source-file" || url.pathname === "/api/source") {
|
|
1817
|
-
const text = sourceTextFor(url);
|
|
1818
|
-
if (text != null) return textResponse(text);
|
|
1819
|
-
return textResponse("Source text was not included in this hosted artifact.", { status: 404 });
|
|
1820
|
-
}
|
|
1821
|
-
if (url.pathname === "/" || url.pathname === "/index.html") {
|
|
1822
|
-
return textResponse(INDEX_HTML, { contentType: "text/html; charset=utf-8" });
|
|
1823
|
-
}
|
|
1824
|
-
return textResponse("Not found", { status: 404 });
|
|
1825
|
-
},
|
|
1826
|
-
};
|
|
1827
|
-
`;
|
|
1828
|
-
}
|
|
1829
|
-
|
|
1830
|
-
function createTarArchive(outputDir, archivePath) {
|
|
1831
|
-
const result = childProcess.spawnSync("tar", ["-C", outputDir, "-czf", archivePath, "dist"], {
|
|
1832
|
-
encoding: "utf8",
|
|
1833
|
-
});
|
|
1834
|
-
if (result.status !== 0) {
|
|
1835
|
-
throw new Error(`failed to create Site Creator archive: ${result.stderr || result.stdout || "tar failed"}`);
|
|
1836
|
-
}
|
|
1837
|
-
}
|
|
1838
|
-
|
|
1839
|
-
function exportDataScienceArtifactPackage(args) {
|
|
1840
|
-
const payload = validatedArtifactPayload(buildArtifactPayload(args));
|
|
1841
|
-
const id = artifactId(payload);
|
|
1842
|
-
const { outputDir, isDefault } = normalizeOutputDir(args.output_dir, id);
|
|
1843
|
-
ensureWritableOutputDir(outputDir, isDefault);
|
|
1844
|
-
|
|
1845
|
-
const distDir = path.join(outputDir, "dist");
|
|
1846
|
-
const serverDir = path.join(distDir, "server");
|
|
1847
|
-
const clientDir = path.join(distDir, "client");
|
|
1848
|
-
const dataDir = path.join(clientDir, "data");
|
|
1849
|
-
const metaDir = path.join(distDir, "_appgen_meta");
|
|
1850
|
-
fs.mkdirSync(serverDir, { recursive: true });
|
|
1851
|
-
fs.mkdirSync(dataDir, { recursive: true });
|
|
1852
|
-
fs.mkdirSync(metaDir, { recursive: true });
|
|
1853
|
-
|
|
1854
|
-
const previousPackageInfo = payload.package_info || payload.packageInfo || {};
|
|
1855
|
-
const previousControls = isPlainObject(previousPackageInfo.controls) ? previousPackageInfo.controls : {};
|
|
1856
|
-
const packageInfo = {
|
|
1857
|
-
...previousPackageInfo,
|
|
1858
|
-
artifactId: id,
|
|
1859
|
-
artifactRuntime: "datascience-artifact-widget.html",
|
|
1860
|
-
deliveryMode: "site_creator",
|
|
1861
|
-
exportedAt: new Date().toISOString(),
|
|
1862
|
-
hostedReadOnly: true,
|
|
1863
|
-
controls: {
|
|
1864
|
-
...previousControls,
|
|
1865
|
-
edit: false,
|
|
1866
|
-
exportHostedLink: false,
|
|
1867
|
-
hostedLink: false,
|
|
1868
|
-
},
|
|
1869
|
-
};
|
|
1870
|
-
const hostedPayload = {
|
|
1871
|
-
...payload,
|
|
1872
|
-
package_info: packageInfo,
|
|
1873
|
-
packageInfo,
|
|
1874
|
-
};
|
|
1875
|
-
const sourceEntries = sourceLookupEntries(hostedPayload);
|
|
1876
|
-
const artifactHtml = hostedArtifactHtml(readWidgetHtmlAsset("datascience-artifact-widget.html"), hostedPayload);
|
|
1877
|
-
const chartHtml = readWidgetHtmlAsset("datascience-chart-widget.html");
|
|
1878
|
-
const appgarden = {};
|
|
1879
|
-
if (typeof args.site_creator_project_id === "string" && args.site_creator_project_id) {
|
|
1880
|
-
appgarden.project_id = args.site_creator_project_id;
|
|
1881
|
-
}
|
|
1882
|
-
|
|
1883
|
-
writeJsonFile(path.join(dataDir, "manifest.json"), hostedPayload.manifest);
|
|
1884
|
-
writeJsonFile(path.join(dataDir, "snapshot.json"), hostedPayload.snapshot);
|
|
1885
|
-
writeJsonFile(path.join(dataDir, "package.json"), packageInfo);
|
|
1886
|
-
writeJsonFile(path.join(dataDir, "sources.json"), Object.fromEntries(sourceEntries));
|
|
1887
|
-
writeJsonFile(path.join(metaDir, "appgarden.json"), appgarden);
|
|
1888
|
-
fs.writeFileSync(path.join(clientDir, "index.html"), artifactHtml, "utf8");
|
|
1889
|
-
fs.writeFileSync(
|
|
1890
|
-
path.join(serverDir, "index.js"),
|
|
1891
|
-
workerSource({
|
|
1892
|
-
artifactHtml,
|
|
1893
|
-
chartHtml,
|
|
1894
|
-
manifest: hostedPayload.manifest,
|
|
1895
|
-
snapshot: hostedPayload.snapshot,
|
|
1896
|
-
packageInfo,
|
|
1897
|
-
sourceEntries,
|
|
1898
|
-
}),
|
|
1899
|
-
"utf8",
|
|
1900
|
-
);
|
|
1901
|
-
|
|
1902
|
-
const archivePath = `${outputDir}.tar.gz`;
|
|
1903
|
-
createTarArchive(outputDir, archivePath);
|
|
1904
|
-
return {
|
|
1905
|
-
ok: true,
|
|
1906
|
-
export_type: "site_creator_package",
|
|
1907
|
-
artifact_id: id,
|
|
1908
|
-
manifest_title: hostedPayload.manifest.title || null,
|
|
1909
|
-
surface: hostedPayload.surface,
|
|
1910
|
-
dataset_count: Object.keys(hostedPayload.snapshot.datasets || {}).length,
|
|
1911
|
-
source_count: sourceEntries.length,
|
|
1912
|
-
output_dir: outputDir,
|
|
1913
|
-
archive_path: archivePath,
|
|
1914
|
-
worker_entrypoint: path.join(serverDir, "index.js"),
|
|
1915
|
-
appgarden_path: path.join(metaDir, "appgarden.json"),
|
|
1916
|
-
access_default: null,
|
|
1917
|
-
access_requirement: "explicit-user-selection-before-deployment",
|
|
1918
|
-
routes: ["/", "/api/manifest", "/api/snapshot", "/api/package", "/api/source-file", "/api/inline-chart-widget"],
|
|
1919
|
-
message:
|
|
1920
|
-
"Exported the validated Data Analytics artifact with the canonical MCP app runtime for Site Creator deployment.",
|
|
1921
|
-
};
|
|
1922
|
-
}
|
|
1923
|
-
|
|
1924
|
-
function normalizeChartSource(args) {
|
|
1925
|
-
const source = asObject(args.source);
|
|
1926
|
-
if (!Object.keys(source).length) return null;
|
|
1927
|
-
const query = asObject(source.query);
|
|
1928
|
-
const normalized = {};
|
|
1929
|
-
for (const key of ["id", "label", "path", "href"]) {
|
|
1930
|
-
if (source[key] != null) normalized[key] = source[key];
|
|
1931
|
-
}
|
|
1932
|
-
if (Object.keys(query).length) {
|
|
1933
|
-
normalized.query = {};
|
|
1934
|
-
for (const key of [
|
|
1935
|
-
"engine",
|
|
1936
|
-
"id",
|
|
1937
|
-
"url",
|
|
1938
|
-
"sql",
|
|
1939
|
-
"description",
|
|
1940
|
-
"language",
|
|
1941
|
-
"executed_at",
|
|
1942
|
-
"tables_used",
|
|
1943
|
-
"filters",
|
|
1944
|
-
"metric_definitions",
|
|
1945
|
-
]) {
|
|
1946
|
-
if (query[key] != null) normalized.query[key] = query[key];
|
|
1947
|
-
}
|
|
1948
|
-
}
|
|
1949
|
-
return Object.keys(normalized).length ? normalized : null;
|
|
1950
|
-
}
|
|
1951
|
-
|
|
1952
|
-
function normalizeColumns(columns, rows) {
|
|
1953
|
-
const normalized = [];
|
|
1954
|
-
const seen = new Set();
|
|
1955
|
-
for (const column of asList(columns)) {
|
|
1956
|
-
if (!isPlainObject(column)) continue;
|
|
1957
|
-
const key = column.key;
|
|
1958
|
-
if (typeof key !== "string" || !key || seen.has(key)) continue;
|
|
1959
|
-
seen.add(key);
|
|
1960
|
-
normalized.push({ ...column });
|
|
1961
|
-
}
|
|
1962
|
-
if (normalized.length || !rows.length) return normalized;
|
|
1963
|
-
const inferredKeys = [];
|
|
1964
|
-
for (const row of rows) {
|
|
1965
|
-
for (const key of Object.keys(row)) {
|
|
1966
|
-
if (!inferredKeys.includes(key)) inferredKeys.push(key);
|
|
1967
|
-
}
|
|
1968
|
-
}
|
|
1969
|
-
return inferredKeys.map((key) => ({ key, label: titleCaseKey(key) }));
|
|
1970
|
-
}
|
|
1971
|
-
|
|
1972
|
-
function normalizeResultTable(value, { fallbackColumns = null, fallbackRows = null } = {}) {
|
|
1973
|
-
const table = asObject(value);
|
|
1974
|
-
const hasTable = Object.keys(table).length > 0;
|
|
1975
|
-
const rowsValue = hasTable ? table.rows : fallbackRows;
|
|
1976
|
-
const rows = asList(rowsValue).filter(isPlainObject).map((row) => ({ ...row }));
|
|
1977
|
-
const columns = normalizeColumns(hasTable ? table.columns : fallbackColumns, rows);
|
|
1978
|
-
if (!rows.length && !columns.length && !hasTable) return null;
|
|
1979
|
-
const rowCount = Number.isInteger(table.row_count) ? table.row_count : rows.length;
|
|
1980
|
-
return { ...table, columns, rows, row_count: rowCount, truncated: hasTable ? Boolean(table.truncated) : false };
|
|
1981
|
-
}
|
|
1982
|
-
|
|
1983
|
-
function columnKeys(resultTable) {
|
|
1984
|
-
const keys = new Set();
|
|
1985
|
-
for (const column of asList((resultTable || {}).columns)) {
|
|
1986
|
-
if (isPlainObject(column) && typeof column.key === "string" && column.key) keys.add(column.key);
|
|
1987
|
-
}
|
|
1988
|
-
for (const row of asList((resultTable || {}).rows)) {
|
|
1989
|
-
if (!isPlainObject(row)) continue;
|
|
1990
|
-
for (const key of Object.keys(row)) {
|
|
1991
|
-
if (key) keys.add(key);
|
|
1992
|
-
}
|
|
1993
|
-
}
|
|
1994
|
-
return keys;
|
|
1995
|
-
}
|
|
1996
|
-
|
|
1997
|
-
function columnType(resultTable, key) {
|
|
1998
|
-
if (!key) return null;
|
|
1999
|
-
for (const column of asList((resultTable || {}).columns)) {
|
|
2000
|
-
if (isPlainObject(column) && column.key === key) return typeof column.type === "string" ? column.type : null;
|
|
2001
|
-
}
|
|
2002
|
-
return null;
|
|
2003
|
-
}
|
|
2004
|
-
|
|
2005
|
-
function columnUnit(resultTable, key) {
|
|
2006
|
-
if (!key) return null;
|
|
2007
|
-
for (const column of asList((resultTable || {}).columns)) {
|
|
2008
|
-
if (isPlainObject(column) && column.key === key && typeof column.unit === "string") {
|
|
2009
|
-
const unit = column.unit.trim();
|
|
2010
|
-
return unit || null;
|
|
2011
|
-
}
|
|
2012
|
-
}
|
|
2013
|
-
return null;
|
|
2014
|
-
}
|
|
2015
|
-
|
|
2016
|
-
function columnValueFormat(resultTable, key) {
|
|
2017
|
-
if (!key) return null;
|
|
2018
|
-
for (const column of asList((resultTable || {}).columns)) {
|
|
2019
|
-
if (!isPlainObject(column) || column.key !== key) continue;
|
|
2020
|
-
if (["compact", "number", "percent", "currency"].includes(column.format)) {
|
|
2021
|
-
return column.format;
|
|
2022
|
-
}
|
|
2023
|
-
if (["number", "percent", "currency"].includes(column.type)) {
|
|
2024
|
-
return column.type;
|
|
2025
|
-
}
|
|
2026
|
-
return null;
|
|
2027
|
-
}
|
|
2028
|
-
return null;
|
|
2029
|
-
}
|
|
2030
|
-
|
|
2031
|
-
function columnLabel(resultTable, key) {
|
|
2032
|
-
if (key === MEASURE_NAMES_FIELD) return "Measure names";
|
|
2033
|
-
for (const column of asList((resultTable || {}).columns)) {
|
|
2034
|
-
if (isPlainObject(column) && column.key === key && typeof column.label === "string" && column.label) {
|
|
2035
|
-
return column.label;
|
|
2036
|
-
}
|
|
2037
|
-
}
|
|
2038
|
-
return titleCaseKey(key);
|
|
2039
|
-
}
|
|
2040
|
-
|
|
2041
|
-
function numeric(value) {
|
|
2042
|
-
if (typeof value === "boolean") return null;
|
|
2043
|
-
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
2044
|
-
if (typeof value === "string") {
|
|
2045
|
-
const parsed = Number(value.replace(/,/g, ""));
|
|
2046
|
-
return Number.isFinite(parsed) ? parsed : null;
|
|
2047
|
-
}
|
|
2048
|
-
return null;
|
|
2049
|
-
}
|
|
2050
|
-
|
|
2051
|
-
function numericColumnKeys(resultTable) {
|
|
2052
|
-
const rows = asList((resultTable || {}).rows).filter(isPlainObject);
|
|
2053
|
-
let keys = Array.from(columnKeys(resultTable));
|
|
2054
|
-
const declaredOrder = asList((resultTable || {}).columns)
|
|
2055
|
-
.filter((column) => isPlainObject(column) && typeof column.key === "string")
|
|
2056
|
-
.map((column) => column.key);
|
|
2057
|
-
if (declaredOrder.length) keys = declaredOrder.filter((key) => keys.includes(key));
|
|
2058
|
-
const output = [];
|
|
2059
|
-
for (const key of keys) {
|
|
2060
|
-
const declaredType = columnType(resultTable, key);
|
|
2061
|
-
if (["number", "percent", "currency"].includes(declaredType)) {
|
|
2062
|
-
output.push(key);
|
|
2063
|
-
continue;
|
|
2064
|
-
}
|
|
2065
|
-
if (["text", "date"].includes(declaredType)) continue;
|
|
2066
|
-
if (rows.some((row) => numeric(row[key]) !== null)) output.push(key);
|
|
2067
|
-
}
|
|
2068
|
-
return output;
|
|
2069
|
-
}
|
|
2070
|
-
|
|
2071
|
-
function isNumericColumn(resultTable, key) {
|
|
2072
|
-
return Boolean(key) && numericColumnKeys(resultTable).includes(key);
|
|
2073
|
-
}
|
|
2074
|
-
|
|
2075
|
-
function measureCompatibilityKey(resultTable, key) {
|
|
2076
|
-
if (!key) return null;
|
|
2077
|
-
const unit = columnUnit(resultTable, key);
|
|
2078
|
-
if (unit) return `unit:${unit.toLowerCase()}`;
|
|
2079
|
-
return columnType(resultTable, key) === "percent" ? "type:percent" : null;
|
|
2080
|
-
}
|
|
2081
|
-
|
|
2082
|
-
function multiMeasureSeriesEnabled(spec) {
|
|
2083
|
-
return asObject(spec && spec.settings).multi_measure_series === true;
|
|
2084
|
-
}
|
|
2085
|
-
|
|
2086
|
-
function usesMultiMeasureSeries(spec) {
|
|
2087
|
-
return multiMeasureSeriesEnabled(spec) || encodingField(spec, "color") === MEASURE_NAMES_FIELD;
|
|
2088
|
-
}
|
|
2089
|
-
|
|
2090
|
-
function compatibleMeasureKeys(resultTable, spec) {
|
|
2091
|
-
const xField = encodingField(spec, "x");
|
|
2092
|
-
const measureKeys = numericColumnKeys(resultTable).filter((key) => key !== xField);
|
|
2093
|
-
if (multiMeasureSeriesEnabled(spec)) return measureKeys;
|
|
2094
|
-
const yField = encodingField(spec, "y");
|
|
2095
|
-
const compatibilityKey = measureCompatibilityKey(resultTable, yField);
|
|
2096
|
-
if (!compatibilityKey) return [];
|
|
2097
|
-
return measureKeys.filter((key) => measureCompatibilityKey(resultTable, key) === compatibilityKey);
|
|
2098
|
-
}
|
|
2099
|
-
|
|
2100
|
-
function normalizeVisualizationTypeAndSettings(rawVisualizationType, rawSettings) {
|
|
2101
|
-
const visualizationType = rawVisualizationType;
|
|
2102
|
-
const raw = asObject(rawSettings);
|
|
2103
|
-
const settings = {};
|
|
2104
|
-
if (raw.show_points != null) settings.show_points = raw.show_points;
|
|
2105
|
-
if (raw.multi_measure_series != null) settings.multi_measure_series = raw.multi_measure_series;
|
|
2106
|
-
if (visualizationType === "bar") {
|
|
2107
|
-
settings.orientation = firstValue(raw.orientation, "vertical");
|
|
2108
|
-
settings.group_mode = firstValue(raw.group_mode, "single");
|
|
2109
|
-
}
|
|
2110
|
-
return {
|
|
2111
|
-
visualizationType,
|
|
2112
|
-
settings: Object.keys(settings).length ? settings : undefined,
|
|
2113
|
-
};
|
|
2114
|
-
}
|
|
2115
|
-
|
|
2116
|
-
function validateEnumSetting(settings, key, acceptedValues) {
|
|
2117
|
-
const value = settings[key];
|
|
2118
|
-
if (value == null || acceptedValues.includes(value)) return;
|
|
2119
|
-
const publicKey = key === "group_mode" ? "grouping" : key === "show_points" ? "points" : key;
|
|
2120
|
-
throw new Error(`chart.options.${publicKey} must be one of ${acceptedValues.join(", ")}`);
|
|
2121
|
-
}
|
|
2122
|
-
|
|
2123
|
-
function validateBooleanSetting(settings, key) {
|
|
2124
|
-
const value = settings[key];
|
|
2125
|
-
if (value == null || typeof value === "boolean") return;
|
|
2126
|
-
throw new Error(`chart.options.${key} must be true or false`);
|
|
2127
|
-
}
|
|
2128
|
-
|
|
2129
|
-
function normalizeHeatmapEncodingsForWidget(resultTable, encodings) {
|
|
2130
|
-
const x = asObject(encodings.x);
|
|
2131
|
-
const y = asObject(encodings.y);
|
|
2132
|
-
const color = asObject(encodings.color);
|
|
2133
|
-
if (!resultTable || !y.field || !color.field) {
|
|
2134
|
-
return { x, y, color };
|
|
2135
|
-
}
|
|
2136
|
-
const yIsNumeric = isNumericColumn(resultTable, y.field);
|
|
2137
|
-
const colorIsNumeric = isNumericColumn(resultTable, color.field);
|
|
2138
|
-
if (!yIsNumeric && colorIsNumeric) {
|
|
2139
|
-
return { x, y: color, color: y };
|
|
2140
|
-
}
|
|
2141
|
-
return { x, y, color };
|
|
2142
|
-
}
|
|
2143
|
-
|
|
2144
|
-
function validatePercentStackedBarShape(resultTable, spec) {
|
|
2145
|
-
if (!resultTable) return;
|
|
2146
|
-
const rows = asList(resultTable.rows).filter(isPlainObject);
|
|
2147
|
-
if (!rows.length) return;
|
|
2148
|
-
const xField = encodingField(spec, "x");
|
|
2149
|
-
const yField = encodingField(spec, "y");
|
|
2150
|
-
if (!yField) return;
|
|
2151
|
-
const totals = new Map();
|
|
2152
|
-
let numericCount = 0;
|
|
2153
|
-
for (const row of rows) {
|
|
2154
|
-
const yValue = numeric(row[yField]);
|
|
2155
|
-
if (yValue === null) continue;
|
|
2156
|
-
if (yValue < 0) {
|
|
2157
|
-
throw new Error(
|
|
2158
|
-
"chart.options.grouping stacked100 requires non-negative y values so percentages have a valid denominator",
|
|
2159
|
-
);
|
|
2160
|
-
}
|
|
2161
|
-
numericCount += 1;
|
|
2162
|
-
const category = xField ? String(row[xField]) : "__all__";
|
|
2163
|
-
totals.set(category, (totals.get(category) || 0) + yValue);
|
|
2164
|
-
}
|
|
2165
|
-
if (!numericCount) return;
|
|
2166
|
-
if ([...totals.values()].some((total) => total <= 0)) {
|
|
2167
|
-
throw new Error(
|
|
2168
|
-
"chart.options.grouping stacked100 requires a positive denominator for every stacked category",
|
|
2169
|
-
);
|
|
2170
|
-
}
|
|
2171
|
-
}
|
|
2172
|
-
|
|
2173
|
-
function validateVisualizationSettings(spec, resultTable) {
|
|
2174
|
-
const settings = asObject(spec.settings);
|
|
2175
|
-
validateEnumSetting(settings, "orientation", BAR_ORIENTATIONS);
|
|
2176
|
-
validateEnumSetting(settings, "group_mode", BAR_GROUP_MODES);
|
|
2177
|
-
validateEnumSetting(settings, "show_points", TREND_POINT_MODES);
|
|
2178
|
-
validateBooleanSetting(settings, "multi_measure_series");
|
|
2179
|
-
if (spec.visualization_type !== "bar") return;
|
|
2180
|
-
const groupMode = settings.group_mode || "single";
|
|
2181
|
-
if (["grouped", "stacked", "stacked100"].includes(groupMode) && !encodingField(spec, "color")) {
|
|
2182
|
-
throw new Error(
|
|
2183
|
-
`chart.options.grouping ${groupMode} requires chart.fields.color.field`,
|
|
2184
|
-
);
|
|
2185
|
-
}
|
|
2186
|
-
if (groupMode === "stacked100") validatePercentStackedBarShape(resultTable, spec);
|
|
2187
|
-
}
|
|
2188
|
-
|
|
2189
|
-
function isHorizontalBarType(visualizationType, settings = {}) {
|
|
2190
|
-
if (visualizationType !== "bar") return false;
|
|
2191
|
-
return asObject(settings).orientation === "horizontal";
|
|
2192
|
-
}
|
|
2193
|
-
|
|
2194
|
-
function chartQualityWarnings(spec, resultTable) {
|
|
2195
|
-
const warnings = [];
|
|
2196
|
-
const visualizationType = spec.visualization_type;
|
|
2197
|
-
const subtitle = String(asObject(spec.presentation).subtitle || "").trim();
|
|
2198
|
-
if (subtitleLooksLikeSourceOrQueryMetadata(subtitle)) {
|
|
2199
|
-
warnings.push(
|
|
2200
|
-
"Chart subtitle looks like source/query metadata; use source.query/source metadata for provenance and make the subtitle a reader-facing insight.",
|
|
2201
|
-
);
|
|
2202
|
-
}
|
|
2203
|
-
if (
|
|
2204
|
-
visualizationType === "leaderboard" &&
|
|
2205
|
-
String(encodingField(spec, "x") || "").toLowerCase().includes("metric")
|
|
2206
|
-
) {
|
|
2207
|
-
warnings.push("Leaderboard over metric definitions is usually better as KPI cards or a table.");
|
|
2208
|
-
}
|
|
2209
|
-
if (resultTable && resultTable.truncated) {
|
|
2210
|
-
warnings.push("Chart uses a sampled result table; verify omitted rows do not change the claim.");
|
|
2211
|
-
}
|
|
2212
|
-
const xField = encodingField(spec, "x");
|
|
2213
|
-
const distinctCategories = distinctResultValues(resultTable, xField);
|
|
2214
|
-
if (visualizationType === "bar" && distinctCategories > COMPACT_BAR_CATEGORY_WARNING_THRESHOLD) {
|
|
2215
|
-
warnings.push(
|
|
2216
|
-
`Compact bar chart has ${distinctCategories} categories; consider a reviewed top-N slice.`,
|
|
2217
|
-
);
|
|
2218
|
-
}
|
|
2219
|
-
if (visualizationType === "pie" && distinctCategories > COMPACT_PIE_SEGMENT_WARNING_THRESHOLD) {
|
|
2220
|
-
warnings.push(
|
|
2221
|
-
`Compact pie chart has ${distinctCategories} slices; consider a reviewed aggregation.`,
|
|
2222
|
-
);
|
|
2223
|
-
}
|
|
2224
|
-
return warnings;
|
|
2225
|
-
}
|
|
2226
|
-
|
|
2227
|
-
function subtitleLooksLikeSourceOrQueryMetadata(subtitle) {
|
|
2228
|
-
if (!subtitle) return false;
|
|
2229
|
-
const lower = subtitle.toLowerCase();
|
|
2230
|
-
if (/\b(source|query|sql|select|where|join|dashboard|notebook|partition)\b/.test(lower)) {
|
|
2231
|
-
return true;
|
|
2232
|
-
}
|
|
2233
|
-
if (/\b(metric|table|warehouse|schema|dataset|view|cube|source id|query id)\s*:/.test(lower)) {
|
|
2234
|
-
return true;
|
|
2235
|
-
}
|
|
2236
|
-
if (/\b[a-z][a-z0-9_]*\.[a-z][a-z0-9_]*\.[a-z][a-z0-9_]*\b/.test(lower)) {
|
|
2237
|
-
return true;
|
|
2238
|
-
}
|
|
2239
|
-
if (/\b[a-z][a-z0-9]*_[a-z0-9_]*\b/.test(subtitle) && /\b(metric|field|column|source)\b/.test(lower)) {
|
|
2240
|
-
return true;
|
|
2241
|
-
}
|
|
2242
|
-
return false;
|
|
2243
|
-
}
|
|
2244
|
-
|
|
2245
|
-
function distinctResultValues(resultTable, field) {
|
|
2246
|
-
if (!resultTable || !field) return 0;
|
|
2247
|
-
const values = new Set();
|
|
2248
|
-
for (const row of asList(resultTable.rows).filter(isPlainObject)) {
|
|
2249
|
-
if (row[field] != null) values.add(String(row[field]));
|
|
2250
|
-
}
|
|
2251
|
-
return values.size;
|
|
2252
|
-
}
|
|
2253
|
-
|
|
2254
|
-
function looksLikeIdentifierField(key) {
|
|
2255
|
-
return /(^|_)(id|uuid|url|email)$/i.test(String(key || ""));
|
|
2256
|
-
}
|
|
2257
|
-
|
|
2258
|
-
function scatterPointLabelField(resultTable, spec) {
|
|
2259
|
-
if (!resultTable || spec.visualization_type !== "scatter") return null;
|
|
2260
|
-
const explicitLabelField = encodingField(spec, "label");
|
|
2261
|
-
if (explicitLabelField) return explicitLabelField;
|
|
2262
|
-
const xField = encodingField(spec, "x");
|
|
2263
|
-
const yField = encodingField(spec, "y");
|
|
2264
|
-
const sizeField = encodingField(spec, "size");
|
|
2265
|
-
const colorField = encodingField(spec, "color");
|
|
2266
|
-
const excluded = new Set([xField, yField, sizeField, colorField].filter(Boolean));
|
|
2267
|
-
const declaredOrder = asList(resultTable.columns)
|
|
2268
|
-
.filter((column) => isPlainObject(column) && typeof column.key === "string")
|
|
2269
|
-
.map((column) => column.key);
|
|
2270
|
-
const keys = declaredOrder.length ? declaredOrder : Array.from(columnKeys(resultTable));
|
|
2271
|
-
const candidates = [];
|
|
2272
|
-
for (const key of keys) {
|
|
2273
|
-
if (!key || excluded.has(key) || looksLikeIdentifierField(key)) continue;
|
|
2274
|
-
if (isNumericColumn(resultTable, key)) continue;
|
|
2275
|
-
if (columnType(resultTable, key) === "date") continue;
|
|
2276
|
-
const distinct = distinctResultValues(resultTable, key);
|
|
2277
|
-
if (distinct >= 2) candidates.push(key);
|
|
2278
|
-
}
|
|
2279
|
-
return candidates.length === 1 ? candidates[0] : null;
|
|
2280
|
-
}
|
|
2281
|
-
|
|
2282
|
-
function chartInputFromLegacyVisualizationSpec(args) {
|
|
2283
|
-
const spec = asObject(args.visualization_spec);
|
|
2284
|
-
const settings = asObject(spec.settings);
|
|
2285
|
-
const encodings = asObject(spec.encodings);
|
|
2286
|
-
return {
|
|
2287
|
-
type: spec.visualization_type,
|
|
2288
|
-
fields: {
|
|
2289
|
-
...(encodings.x ? { x: encodings.x } : {}),
|
|
2290
|
-
...(encodings.y ? { y: encodings.y } : {}),
|
|
2291
|
-
...(encodings.size ? { size: encodings.size } : {}),
|
|
2292
|
-
...(encodings.color ? { color: encodings.color } : {}),
|
|
2293
|
-
...(encodings.label ? { label: encodings.label } : {}),
|
|
2294
|
-
},
|
|
2295
|
-
options: {
|
|
2296
|
-
...(settings.orientation != null ? { orientation: settings.orientation } : {}),
|
|
2297
|
-
...(settings.group_mode != null ? { grouping: settings.group_mode } : {}),
|
|
2298
|
-
...(settings.show_points != null ? { points: settings.show_points } : {}),
|
|
2299
|
-
...(settings.multi_measure_series != null
|
|
2300
|
-
? { multi_measure_series: settings.multi_measure_series }
|
|
2301
|
-
: {}),
|
|
2302
|
-
},
|
|
2303
|
-
};
|
|
2304
|
-
}
|
|
2305
|
-
|
|
2306
|
-
function displayInputFromLegacyVisualizationSpec(args) {
|
|
2307
|
-
const presentation = asObject(asObject(args.visualization_spec).presentation);
|
|
2308
|
-
return {
|
|
2309
|
-
...(firstValue(presentation.unit, args.unit) != null
|
|
2310
|
-
? { unit: firstValue(presentation.unit, args.unit) }
|
|
2311
|
-
: {}),
|
|
2312
|
-
...(firstValue(presentation.baseline, args.baseline) != null
|
|
2313
|
-
? { baseline: firstValue(presentation.baseline, args.baseline) }
|
|
2314
|
-
: {}),
|
|
2315
|
-
...(firstValue(presentation.x_axis_title, presentation.x_label, args.x_axis_title, args.x_label) != null
|
|
2316
|
-
? {
|
|
2317
|
-
x_axis_title: firstValue(
|
|
2318
|
-
presentation.x_axis_title,
|
|
2319
|
-
presentation.x_label,
|
|
2320
|
-
args.x_axis_title,
|
|
2321
|
-
args.x_label,
|
|
2322
|
-
),
|
|
2323
|
-
}
|
|
2324
|
-
: {}),
|
|
2325
|
-
...(firstValue(presentation.y_axis_title, presentation.y_label, args.y_axis_title, args.y_label) != null
|
|
2326
|
-
? {
|
|
2327
|
-
y_axis_title: firstValue(
|
|
2328
|
-
presentation.y_axis_title,
|
|
2329
|
-
presentation.y_label,
|
|
2330
|
-
args.y_axis_title,
|
|
2331
|
-
args.y_label,
|
|
2332
|
-
),
|
|
2333
|
-
}
|
|
2334
|
-
: {}),
|
|
2335
|
-
...(firstValue(presentation.show_controls, args.show_controls) != null
|
|
2336
|
-
? { controls: firstValue(presentation.show_controls, args.show_controls) }
|
|
2337
|
-
: {}),
|
|
2338
|
-
};
|
|
2339
|
-
}
|
|
2340
|
-
|
|
2341
|
-
function normalizeChartInput(args) {
|
|
2342
|
-
const chart = Object.keys(asObject(args.chart)).length
|
|
2343
|
-
? asObject(args.chart)
|
|
2344
|
-
: chartInputFromLegacyVisualizationSpec(args);
|
|
2345
|
-
const display = Object.keys(asObject(args.display)).length
|
|
2346
|
-
? asObject(args.display)
|
|
2347
|
-
: displayInputFromLegacyVisualizationSpec(args);
|
|
2348
|
-
return {
|
|
2349
|
-
source: normalizeChartSource(args),
|
|
2350
|
-
table: normalizeResultTable(firstValue(args.table, args.result_table)),
|
|
2351
|
-
chart,
|
|
2352
|
-
display,
|
|
2353
|
-
};
|
|
2354
|
-
}
|
|
2355
|
-
|
|
2356
|
-
function visualizationArgsFromChartInput(args, chartInput) {
|
|
2357
|
-
const chart = asObject(chartInput.chart);
|
|
2358
|
-
const fields = asObject(chart.fields);
|
|
2359
|
-
const options = asObject(chart.options);
|
|
2360
|
-
const display = asObject(chartInput.display);
|
|
2361
|
-
return {
|
|
2362
|
-
...args,
|
|
2363
|
-
visualization_spec: {
|
|
2364
|
-
visualization_type: chart.type,
|
|
2365
|
-
encodings: {
|
|
2366
|
-
...(fields.x ? { x: fields.x } : {}),
|
|
2367
|
-
...(fields.y ? { y: fields.y } : {}),
|
|
2368
|
-
...(fields.size ? { size: fields.size } : {}),
|
|
2369
|
-
...(fields.color ? { color: fields.color } : {}),
|
|
2370
|
-
...(fields.lineStyle ? { lineStyle: fields.lineStyle } : {}),
|
|
2371
|
-
...(fields.label ? { label: fields.label } : {}),
|
|
2372
|
-
},
|
|
2373
|
-
settings: {
|
|
2374
|
-
...(options.orientation != null ? { orientation: options.orientation } : {}),
|
|
2375
|
-
...(options.grouping != null ? { group_mode: options.grouping } : {}),
|
|
2376
|
-
...(options.points != null ? { show_points: options.points } : {}),
|
|
2377
|
-
...(options.multi_measure_series != null
|
|
2378
|
-
? { multi_measure_series: options.multi_measure_series }
|
|
2379
|
-
: {}),
|
|
2380
|
-
},
|
|
2381
|
-
presentation: {
|
|
2382
|
-
title: args.title,
|
|
2383
|
-
subtitle: args.subtitle,
|
|
2384
|
-
...(display.unit != null ? { unit: display.unit } : {}),
|
|
2385
|
-
...(display.baseline != null ? { baseline: display.baseline } : {}),
|
|
2386
|
-
...(display.x_axis_title != null ? { x_axis_title: display.x_axis_title } : {}),
|
|
2387
|
-
...(display.y_axis_title != null ? { y_axis_title: display.y_axis_title } : {}),
|
|
2388
|
-
...(display.controls != null ? { show_controls: display.controls } : {}),
|
|
2389
|
-
},
|
|
2390
|
-
},
|
|
2391
|
-
show_controls: display.controls,
|
|
2392
|
-
};
|
|
2393
|
-
}
|
|
2394
|
-
|
|
2395
|
-
function chartOutputFromVisualizationSpec(spec) {
|
|
2396
|
-
const encodings = asObject(spec.encodings);
|
|
2397
|
-
const settings = asObject(spec.settings);
|
|
2398
|
-
return {
|
|
2399
|
-
type: spec.visualization_type,
|
|
2400
|
-
fields: {
|
|
2401
|
-
...(encodings.x ? { x: encodings.x } : {}),
|
|
2402
|
-
...(encodings.y ? { y: encodings.y } : {}),
|
|
2403
|
-
...(encodings.size ? { size: encodings.size } : {}),
|
|
2404
|
-
...(encodings.color ? { color: encodings.color } : {}),
|
|
2405
|
-
...(encodings.lineStyle ? { lineStyle: encodings.lineStyle } : {}),
|
|
2406
|
-
...(encodings.label ? { label: encodings.label } : {}),
|
|
2407
|
-
},
|
|
2408
|
-
...(Object.keys(settings).length
|
|
2409
|
-
? {
|
|
2410
|
-
options: {
|
|
2411
|
-
...(settings.orientation != null ? { orientation: settings.orientation } : {}),
|
|
2412
|
-
...(settings.group_mode != null ? { grouping: settings.group_mode } : {}),
|
|
2413
|
-
...(settings.show_points != null ? { points: settings.show_points } : {}),
|
|
2414
|
-
...(settings.multi_measure_series != null
|
|
2415
|
-
? { multi_measure_series: settings.multi_measure_series }
|
|
2416
|
-
: {}),
|
|
2417
|
-
},
|
|
2418
|
-
}
|
|
2419
|
-
: {}),
|
|
2420
|
-
};
|
|
2421
|
-
}
|
|
2422
|
-
|
|
2423
|
-
function displayOutputFromVisualizationSpec(spec) {
|
|
2424
|
-
const presentation = asObject(spec.presentation);
|
|
2425
|
-
const display = {
|
|
2426
|
-
...(presentation.unit != null ? { unit: presentation.unit } : {}),
|
|
2427
|
-
...(presentation.baseline != null ? { baseline: presentation.baseline } : {}),
|
|
2428
|
-
...(presentation.x_axis_title != null ? { x_axis_title: presentation.x_axis_title } : {}),
|
|
2429
|
-
...(presentation.y_axis_title != null ? { y_axis_title: presentation.y_axis_title } : {}),
|
|
2430
|
-
...(presentation.show_controls != null ? { controls: presentation.show_controls } : {}),
|
|
2431
|
-
};
|
|
2432
|
-
return Object.keys(display).length ? display : null;
|
|
2433
|
-
}
|
|
2434
|
-
|
|
2435
|
-
function normalizeVisualizationSpec(args, resultTable) {
|
|
2436
|
-
const spec = asObject(args.visualization_spec);
|
|
2437
|
-
const {
|
|
2438
|
-
intent: _intent,
|
|
2439
|
-
question: _question,
|
|
2440
|
-
rationale: _rationale,
|
|
2441
|
-
version: _version,
|
|
2442
|
-
comparisonContext: _comparisonContext,
|
|
2443
|
-
sort: _sort,
|
|
2444
|
-
...specWithoutRemovedMetadata
|
|
2445
|
-
} = spec;
|
|
2446
|
-
const encodings = asObject(spec.encodings);
|
|
2447
|
-
const presentation = asObject(spec.presentation);
|
|
2448
|
-
const requestedVisualizationType = spec.visualization_type;
|
|
2449
|
-
if (requestedVisualizationType == null) {
|
|
2450
|
-
throw new Error("chart.type is required");
|
|
2451
|
-
}
|
|
2452
|
-
if (!CANONICAL_CHART_TYPE_SET.has(requestedVisualizationType)) {
|
|
2453
|
-
throw new Error(`chart.type must be one of ${CANONICAL_CHART_TYPES.join(", ")}`);
|
|
2454
|
-
}
|
|
2455
|
-
const normalizedType = normalizeVisualizationTypeAndSettings(requestedVisualizationType, spec.settings);
|
|
2456
|
-
const visualizationType = normalizedType.visualizationType;
|
|
2457
|
-
const settings = normalizedType.settings;
|
|
2458
|
-
let x = asObject(encodings.x);
|
|
2459
|
-
let y = asObject(encodings.y);
|
|
2460
|
-
const size = asObject(encodings.size);
|
|
2461
|
-
let color = asObject(encodings.color);
|
|
2462
|
-
const lineStyle = asObject(encodings.lineStyle);
|
|
2463
|
-
const label = asObject(encodings.label);
|
|
2464
|
-
if (visualizationType === "heatmap") {
|
|
2465
|
-
({ x, y, color } = normalizeHeatmapEncodingsForWidget(resultTable, { x, y, color }));
|
|
2466
|
-
}
|
|
2467
|
-
const requestedXLabel = firstValue(presentation.x_axis_title, presentation.x_label);
|
|
2468
|
-
const requestedYLabel = firstValue(presentation.y_axis_title, presentation.y_label);
|
|
2469
|
-
if (typeof x.field !== "string" || !x.field) {
|
|
2470
|
-
throw new Error("chart.fields.x.field is required");
|
|
2471
|
-
}
|
|
2472
|
-
if (typeof y.field !== "string" || !y.field) {
|
|
2473
|
-
throw new Error("chart.fields.y.field is required");
|
|
2474
|
-
}
|
|
2475
|
-
const resolvedEncodings = { x, y };
|
|
2476
|
-
if (size.field) resolvedEncodings.size = size;
|
|
2477
|
-
if (color.field) resolvedEncodings.color = color;
|
|
2478
|
-
if (lineStyle.field) resolvedEncodings.lineStyle = lineStyle;
|
|
2479
|
-
if (label.field) resolvedEncodings.label = label;
|
|
2480
|
-
const resolvedPresentation = {
|
|
2481
|
-
title: firstValue(presentation.title, args.title),
|
|
2482
|
-
subtitle: firstValue(presentation.subtitle, args.subtitle),
|
|
2483
|
-
x_label: requestedXLabel,
|
|
2484
|
-
y_label: requestedYLabel,
|
|
2485
|
-
unit: firstValue(presentation.unit, y.unit, columnUnit(resultTable, y.field)),
|
|
2486
|
-
baseline: presentation.baseline,
|
|
2487
|
-
show_controls: firstValue(presentation.show_controls, args.show_controls),
|
|
2488
|
-
};
|
|
2489
|
-
const normalizedSpec = {
|
|
2490
|
-
...specWithoutRemovedMetadata,
|
|
2491
|
-
visualization_type: visualizationType,
|
|
2492
|
-
settings,
|
|
2493
|
-
encodings: resolvedEncodings,
|
|
2494
|
-
presentation: {
|
|
2495
|
-
...resolvedPresentation,
|
|
2496
|
-
x_axis_title: resolvedPresentation.x_label,
|
|
2497
|
-
y_axis_title: resolvedPresentation.y_label,
|
|
2498
|
-
},
|
|
2499
|
-
};
|
|
2500
|
-
validateVisualizationSettings(normalizedSpec, resultTable);
|
|
2501
|
-
return normalizedSpec;
|
|
2502
|
-
}
|
|
2503
|
-
|
|
2504
|
-
function encodingField(spec, role) {
|
|
2505
|
-
const field = asObject(asObject((spec || {}).encodings)[role]).field;
|
|
2506
|
-
return typeof field === "string" && field ? field : null;
|
|
2507
|
-
}
|
|
2508
|
-
|
|
2509
|
-
function chartEncodingForField(spec, resultTable, role, fallbackField = null) {
|
|
2510
|
-
const source = asObject(asObject((spec || {}).encodings)[role]);
|
|
2511
|
-
const field = typeof fallbackField === "string" && fallbackField ? fallbackField : encodingField(spec, role);
|
|
2512
|
-
if (!field) return null;
|
|
2513
|
-
const encoding = { ...source, field };
|
|
2514
|
-
if (!encoding.label) encoding.label = columnLabel(resultTable, field);
|
|
2515
|
-
if (!encoding.format) {
|
|
2516
|
-
const format = columnValueFormat(resultTable, field);
|
|
2517
|
-
if (format) encoding.format = format;
|
|
2518
|
-
}
|
|
2519
|
-
if (!encoding.unit) {
|
|
2520
|
-
const unit = columnUnit(resultTable, field);
|
|
2521
|
-
if (unit) encoding.unit = unit;
|
|
2522
|
-
}
|
|
2523
|
-
if (!encoding.type) {
|
|
2524
|
-
const type = columnType(resultTable, field);
|
|
2525
|
-
if (type === "date") encoding.type = "temporal";
|
|
2526
|
-
else if (type === "text") encoding.type = "nominal";
|
|
2527
|
-
else if (type === "percent" || type === "currency" || type === "number") encoding.type = "quantitative";
|
|
2528
|
-
}
|
|
2529
|
-
return encoding;
|
|
2530
|
-
}
|
|
2531
|
-
|
|
2532
|
-
function encodingValue(spec, role, key) {
|
|
2533
|
-
return asObject(asObject((spec || {}).encodings)[role])[key];
|
|
2534
|
-
}
|
|
2535
|
-
|
|
2536
|
-
function validateVisualizationFields(resultTable, spec) {
|
|
2537
|
-
if (!resultTable) return;
|
|
2538
|
-
const keys = columnKeys(resultTable);
|
|
2539
|
-
if (!keys.size) return;
|
|
2540
|
-
const visualizationType = spec.visualization_type;
|
|
2541
|
-
const heatmapColorField = visualizationType === "heatmap" ? encodingField(spec, "color") : null;
|
|
2542
|
-
if (visualizationType === "heatmap" && !heatmapColorField) {
|
|
2543
|
-
throw new Error("chart.fields.color.field is required for heatmap charts and represents the Y-axis category");
|
|
2544
|
-
}
|
|
2545
|
-
for (const role of ["x", "y", "size", "color", "lineStyle", "label"]) {
|
|
2546
|
-
const field = encodingField(spec, role);
|
|
2547
|
-
if (role === "color" && field === MEASURE_NAMES_FIELD) {
|
|
2548
|
-
if (compatibleMeasureKeys(resultTable, spec).length > 1) continue;
|
|
2549
|
-
throw new Error(
|
|
2550
|
-
"chart.fields.color.field \"__measure_names__\" requires at least two compatible measures or chart.options.multi_measure_series=true",
|
|
2551
|
-
);
|
|
2552
|
-
}
|
|
2553
|
-
if (field && !keys.has(field)) {
|
|
2554
|
-
throw new Error(
|
|
2555
|
-
`chart.fields.${role}.field ${JSON.stringify(field)} is not present in table`,
|
|
2556
|
-
);
|
|
2557
|
-
}
|
|
2558
|
-
if (
|
|
2559
|
-
role === "y" &&
|
|
2560
|
-
field &&
|
|
2561
|
-
encodingValue(spec, "y", "aggregate") !== "count" &&
|
|
2562
|
-
!isNumericColumn(resultTable, field)
|
|
2563
|
-
) {
|
|
2564
|
-
const message =
|
|
2565
|
-
visualizationType === "heatmap"
|
|
2566
|
-
? "chart.fields.y.field must reference the numeric heatmap cell value; use chart.fields.color.field for the Y-axis category"
|
|
2567
|
-
: "chart.fields.y.field must reference a numeric table column";
|
|
2568
|
-
throw new Error(message);
|
|
2569
|
-
}
|
|
2570
|
-
if (role === "size" && field && spec.visualization_type !== "scatter") {
|
|
2571
|
-
throw new Error("chart.fields.size.field is only supported for scatter charts");
|
|
2572
|
-
}
|
|
2573
|
-
if (role === "size" && field && !isNumericColumn(resultTable, field)) {
|
|
2574
|
-
throw new Error("chart.fields.size.field must reference a numeric table column");
|
|
2575
|
-
}
|
|
2576
|
-
}
|
|
2577
|
-
const lineStyleField = encodingField(spec, "lineStyle");
|
|
2578
|
-
if (lineStyleField) {
|
|
2579
|
-
if (!["line", "area", "stackedArea", "sparkline"].includes(visualizationType)) {
|
|
2580
|
-
throw new Error("chart.fields.lineStyle.field is only supported for line, area, stackedArea, and sparkline charts");
|
|
2581
|
-
}
|
|
2582
|
-
for (const row of asList(resultTable.rows).filter(isPlainObject)) {
|
|
2583
|
-
const rawStyle = row[lineStyleField];
|
|
2584
|
-
if (rawStyle == null || rawStyle === "") continue;
|
|
2585
|
-
const style = String(rawStyle);
|
|
2586
|
-
if (!SERIES_LINE_STYLES.includes(style)) {
|
|
2587
|
-
throw new Error(
|
|
2588
|
-
`chart.fields.lineStyle.field values must be one of ${SERIES_LINE_STYLES.join(", ")}`,
|
|
2589
|
-
);
|
|
2590
|
-
}
|
|
2591
|
-
}
|
|
2592
|
-
}
|
|
2593
|
-
}
|
|
2594
|
-
|
|
2595
|
-
function deriveChartPoints(resultTable, spec) {
|
|
2596
|
-
if (!resultTable) return [];
|
|
2597
|
-
const rows = asList(resultTable.rows).filter(isPlainObject);
|
|
2598
|
-
if (!rows.length) return [];
|
|
2599
|
-
const xField = encodingField(spec, "x");
|
|
2600
|
-
const yField = encodingField(spec, "y");
|
|
2601
|
-
const sizeField = encodingField(spec, "size");
|
|
2602
|
-
const colorField = encodingField(spec, "color");
|
|
2603
|
-
const yAggregate = String(encodingValue(spec, "y", "aggregate") || "sum").toLowerCase();
|
|
2604
|
-
const visualizationType = String(spec.visualization_type);
|
|
2605
|
-
const output = [];
|
|
2606
|
-
for (const row of rows) {
|
|
2607
|
-
const xValue = xField ? row[xField] : null;
|
|
2608
|
-
if (usesMultiMeasureSeries(spec)) {
|
|
2609
|
-
for (const measureKey of compatibleMeasureKeys(resultTable, spec)) {
|
|
2610
|
-
const yValue = numeric(row[measureKey]);
|
|
2611
|
-
if (yValue === null) continue;
|
|
2612
|
-
output.push({
|
|
2613
|
-
...row,
|
|
2614
|
-
x: xValue,
|
|
2615
|
-
y: yValue,
|
|
2616
|
-
size: sizeField ? numeric(row[sizeField]) : null,
|
|
2617
|
-
series: columnLabel(resultTable, measureKey),
|
|
2618
|
-
});
|
|
2619
|
-
}
|
|
2620
|
-
continue;
|
|
2621
|
-
}
|
|
2622
|
-
let yValue;
|
|
2623
|
-
if (yAggregate === "count") {
|
|
2624
|
-
yValue = 1;
|
|
2625
|
-
} else {
|
|
2626
|
-
let sourceY = yField ? row[yField] : null;
|
|
2627
|
-
if (sourceY == null && visualizationType === "histogram") sourceY = xValue;
|
|
2628
|
-
yValue = numeric(sourceY);
|
|
2629
|
-
}
|
|
2630
|
-
if (yValue === null) continue;
|
|
2631
|
-
const seriesValue = colorField ? row[colorField] : null;
|
|
2632
|
-
const seriesLabel = colorField
|
|
2633
|
-
? firstValue(seriesValue, "Value")
|
|
2634
|
-
: firstValue(presentationValue(spec, {}, "y_label"), columnLabel(resultTable, yField), "Value");
|
|
2635
|
-
output.push({
|
|
2636
|
-
...row,
|
|
2637
|
-
x: xValue,
|
|
2638
|
-
y: yValue,
|
|
2639
|
-
size: sizeField ? numeric(row[sizeField]) : null,
|
|
2640
|
-
series: String(seriesLabel),
|
|
2641
|
-
});
|
|
2642
|
-
}
|
|
2643
|
-
return output;
|
|
2644
|
-
}
|
|
2645
|
-
|
|
2646
|
-
function normalizeTablePayload(args) {
|
|
2647
|
-
const source = normalizeChartSource(args);
|
|
2648
|
-
validateActualSqlSource(source, "$.source.query.sql");
|
|
2649
|
-
const resultTable = normalizeResultTable(args.result_table, {
|
|
2650
|
-
fallbackColumns: args.columns,
|
|
2651
|
-
fallbackRows: args.rows,
|
|
2652
|
-
});
|
|
2653
|
-
const rows = asList((resultTable || {}).rows);
|
|
2654
|
-
const viewConfig = asObject(args.view_config);
|
|
2655
|
-
const maxRows = firstValue(args.max_rows, viewConfig.max_rows, 50);
|
|
2656
|
-
return {
|
|
2657
|
-
ok: true,
|
|
2658
|
-
widget_type: "table",
|
|
2659
|
-
title: String(firstValue(args.title, "Query preview")),
|
|
2660
|
-
subtitle: args.subtitle,
|
|
2661
|
-
source,
|
|
2662
|
-
result_table: resultTable,
|
|
2663
|
-
columns: (resultTable || {}).columns || [],
|
|
2664
|
-
rows,
|
|
2665
|
-
metrics: args.metrics || [],
|
|
2666
|
-
notes: args.notes || [],
|
|
2667
|
-
max_rows: Number.parseInt(maxRows || 50, 10),
|
|
2668
|
-
};
|
|
2669
|
-
}
|
|
2670
|
-
|
|
2671
|
-
function presentationValue(spec, args, key) {
|
|
2672
|
-
return firstValue(asObject(spec.presentation)[key], args[key]);
|
|
2673
|
-
}
|
|
2674
|
-
|
|
2675
|
-
function axisTitleValue(spec, args, axis) {
|
|
2676
|
-
return firstValue(
|
|
2677
|
-
asObject(spec.presentation)[`${axis}_axis_title`],
|
|
2678
|
-
asObject(spec.presentation)[`${axis}_label`],
|
|
2679
|
-
args[`${axis}_axis_title`],
|
|
2680
|
-
args[`${axis}_label`],
|
|
2681
|
-
);
|
|
2682
|
-
}
|
|
2683
|
-
|
|
2684
|
-
function axisTitleForField(spec, args, resultTable, axis, field) {
|
|
2685
|
-
const title = axisTitleValue(spec, args, axis);
|
|
2686
|
-
return title == null || title === "" ? undefined : String(title);
|
|
2687
|
-
}
|
|
2688
|
-
|
|
2689
|
-
function chartSpecFromVisualizationSpec(spec, args, resultTable, id = "chart") {
|
|
2690
|
-
const xField = encodingField(spec, "x") || "x";
|
|
2691
|
-
const yField = encodingField(spec, "y") || "y";
|
|
2692
|
-
const sizeField = encodingField(spec, "size");
|
|
2693
|
-
const colorField = encodingField(spec, "color");
|
|
2694
|
-
const lineStyleField = encodingField(spec, "lineStyle");
|
|
2695
|
-
const xAxisTitle = axisTitleForField(spec, args, resultTable, "x", xField);
|
|
2696
|
-
const yAxisTitle = axisTitleForField(spec, args, resultTable, "y", yField);
|
|
2697
|
-
const horizontal = isHorizontalBarType(spec.visualization_type, spec.settings);
|
|
2698
|
-
const encodings = {};
|
|
2699
|
-
encodings.x = chartEncodingForField(spec, resultTable, "x", xField);
|
|
2700
|
-
if (usesMultiMeasureSeries(spec)) {
|
|
2701
|
-
const fields = compatibleMeasureKeys(resultTable, spec);
|
|
2702
|
-
encodings.y = {
|
|
2703
|
-
...asObject(asObject((spec || {}).encodings).y),
|
|
2704
|
-
fields,
|
|
2705
|
-
label: presentationValue(spec, {}, "y_label") || "Value",
|
|
2706
|
-
type: "quantitative",
|
|
2707
|
-
};
|
|
2708
|
-
delete encodings.y.field;
|
|
2709
|
-
} else {
|
|
2710
|
-
encodings.y = chartEncodingForField(spec, resultTable, "y", yField);
|
|
2711
|
-
}
|
|
2712
|
-
if (colorField) {
|
|
2713
|
-
encodings.color = chartEncodingForField(spec, resultTable, "color", colorField);
|
|
2714
|
-
}
|
|
2715
|
-
if (lineStyleField) {
|
|
2716
|
-
encodings.lineStyle = chartEncodingForField(spec, resultTable, "lineStyle", lineStyleField);
|
|
2717
|
-
}
|
|
2718
|
-
if (spec.visualization_type === "scatter" && sizeField) {
|
|
2719
|
-
encodings.size = chartEncodingForField(spec, resultTable, "size", sizeField);
|
|
2720
|
-
}
|
|
2721
|
-
const labelField = scatterPointLabelField(resultTable, spec);
|
|
2722
|
-
if (spec.visualization_type === "scatter" && labelField && asList((resultTable || {}).rows).some((row) => isPlainObject(row) && row[labelField] != null)) {
|
|
2723
|
-
encodings.label = chartEncodingForField(spec, resultTable, "label", labelField);
|
|
2724
|
-
}
|
|
2725
|
-
return {
|
|
2726
|
-
id: String(firstValue(args.id, id)),
|
|
2727
|
-
title: String(presentationValue(spec, args, "title") || args.title || "Data Analytics chart"),
|
|
2728
|
-
subtitle: presentationValue(spec, args, "subtitle"),
|
|
2729
|
-
type: spec.visualization_type,
|
|
2730
|
-
dataset: String(firstValue(args.id, args.label, "default")),
|
|
2731
|
-
...(Object.keys(encodings).length ? { encodings } : {}),
|
|
2732
|
-
xAxisTitle: horizontal ? yAxisTitle : xAxisTitle,
|
|
2733
|
-
yAxisTitle: horizontal ? xAxisTitle : yAxisTitle,
|
|
2734
|
-
unit: presentationValue(spec, args, "unit"),
|
|
2735
|
-
valueFormat: columnValueFormat(resultTable, yField) || undefined,
|
|
2736
|
-
settings: spec.settings,
|
|
2737
|
-
surface: {
|
|
2738
|
-
surface: "compact",
|
|
2739
|
-
showControls: Boolean(presentationValue(spec, args, "show_controls")),
|
|
2740
|
-
},
|
|
2741
|
-
};
|
|
2742
|
-
}
|
|
2743
|
-
|
|
2744
|
-
function normalizeChartPayload(args) {
|
|
2745
|
-
const chartInput = normalizeChartInput(args);
|
|
2746
|
-
validateActualSqlSource(chartInput.source, "$.source.query.sql");
|
|
2747
|
-
const resultTable = chartInput.table;
|
|
2748
|
-
const normalizedArgs = visualizationArgsFromChartInput(args, chartInput);
|
|
2749
|
-
const visualizationSpec = normalizeVisualizationSpec(normalizedArgs, resultTable);
|
|
2750
|
-
validateVisualizationFields(resultTable, visualizationSpec);
|
|
2751
|
-
|
|
2752
|
-
const data = deriveChartPoints(resultTable, visualizationSpec);
|
|
2753
|
-
const chartSpec = chartSpecFromVisualizationSpec(visualizationSpec, normalizedArgs, resultTable, "default");
|
|
2754
|
-
const qualityWarnings = chartQualityWarnings(visualizationSpec, resultTable);
|
|
2755
|
-
if (!asList((resultTable || {}).rows).length) {
|
|
2756
|
-
throw new Error("chart widgets require table.rows");
|
|
2757
|
-
}
|
|
2758
|
-
|
|
2759
|
-
return {
|
|
2760
|
-
ok: true,
|
|
2761
|
-
widget_type: "chart",
|
|
2762
|
-
title: String(presentationValue(visualizationSpec, normalizedArgs, "title") || args.title),
|
|
2763
|
-
subtitle: presentationValue(visualizationSpec, normalizedArgs, "subtitle"),
|
|
2764
|
-
source: chartInput.source,
|
|
2765
|
-
table: resultTable,
|
|
2766
|
-
chart: chartOutputFromVisualizationSpec(visualizationSpec),
|
|
2767
|
-
display: displayOutputFromVisualizationSpec(visualizationSpec),
|
|
2768
|
-
quality_warnings: qualityWarnings,
|
|
2769
|
-
chart_spec: chartSpec,
|
|
2770
|
-
visualization_type: visualizationSpec.visualization_type,
|
|
2771
|
-
show_controls: presentationValue(visualizationSpec, normalizedArgs, "show_controls"),
|
|
2772
|
-
data,
|
|
2773
|
-
unit: presentationValue(visualizationSpec, normalizedArgs, "unit"),
|
|
2774
|
-
baseline: presentationValue(visualizationSpec, normalizedArgs, "baseline"),
|
|
2775
|
-
};
|
|
2776
|
-
}
|
|
2777
|
-
|
|
2778
|
-
function callTool(name, inputArgs = {}) {
|
|
2779
|
-
const args = isPlainObject(inputArgs) ? { ...inputArgs } : {};
|
|
2780
|
-
const toolName = name;
|
|
2781
|
-
|
|
2782
|
-
if (toolName === TOOL_NAMES.validateArtifact) {
|
|
2783
|
-
const payload = validatedArtifactPayload(buildArtifactPayload(args));
|
|
2784
|
-
return {
|
|
2785
|
-
ok: true,
|
|
2786
|
-
validation_type: "artifact",
|
|
2787
|
-
surface: payload.surface,
|
|
2788
|
-
manifest_title: payload.manifest.title || null,
|
|
2789
|
-
dataset_count: Object.keys(payload.snapshot.datasets || {}).length,
|
|
2790
|
-
source_count: asList(payload.sources).length,
|
|
2791
|
-
snapshot_status: payload.snapshot.status || null,
|
|
2792
|
-
message: "Artifact payload is valid. It is safe to call render_artifact once.",
|
|
2793
|
-
artifact_payload: payload,
|
|
2794
|
-
};
|
|
2795
|
-
}
|
|
2796
|
-
if (toolName === TOOL_NAMES.renderArtifact) {
|
|
2797
|
-
return validatedArtifactPayload(buildArtifactPayload(args));
|
|
2798
|
-
}
|
|
2799
|
-
if (toolName === TOOL_NAMES.exportArtifactPackage) {
|
|
2800
|
-
return exportDataScienceArtifactPackage(args);
|
|
2801
|
-
}
|
|
2802
|
-
if (toolName === TOOL_NAMES.renderChart) {
|
|
2803
|
-
return validatedWidgetPayload(normalizeChartPayload({ ...args, widget_type: "chart" }));
|
|
2804
|
-
}
|
|
2805
|
-
if (toolName === TOOL_NAMES.renderTable) {
|
|
2806
|
-
return validatedWidgetPayload(normalizeTablePayload({ ...args, widget_type: "table" }));
|
|
2807
|
-
}
|
|
2808
|
-
|
|
2809
|
-
throw new Error(`unknown Data Analytics widget tool: ${name}`);
|
|
2810
|
-
}
|
|
2811
|
-
|
|
2812
|
-
function widgetUriForPayload(payload) {
|
|
2813
|
-
if (payload.widget_type === "artifact") return ARTIFACT_WIDGET_URI;
|
|
2814
|
-
if (payload.widget_type === "table") return TABLE_WIDGET_URI;
|
|
2815
|
-
if (payload.widget_type === "chart") return CHART_WIDGET_URI;
|
|
2816
|
-
return null;
|
|
2817
|
-
}
|
|
2818
|
-
|
|
2819
|
-
function toolResult(payload, toolName) {
|
|
2820
|
-
const result = {
|
|
2821
|
-
content: [{ type: "text", text: JSON.stringify(payload) }],
|
|
2822
|
-
structuredContent: payload,
|
|
2823
|
-
isError: false,
|
|
2824
|
-
};
|
|
2825
|
-
const widgetUri = widgetUriForPayload(payload) || WIDGET_TOOL_URIS[toolName];
|
|
2826
|
-
if (widgetUri) result._meta = toolUiMeta(widgetUri, toolName);
|
|
2827
|
-
return result;
|
|
2828
|
-
}
|
|
2829
|
-
|
|
2830
|
-
function toolError(message) {
|
|
2831
|
-
const payload = { ok: false, error: message };
|
|
2832
|
-
return {
|
|
2833
|
-
content: [{ type: "text", text: JSON.stringify(payload) }],
|
|
2834
|
-
structuredContent: payload,
|
|
2835
|
-
isError: true,
|
|
2836
|
-
};
|
|
2837
|
-
}
|
|
2838
|
-
|
|
2839
|
-
function rpcResponse(id, result) {
|
|
2840
|
-
return { jsonrpc: "2.0", id, result };
|
|
2841
|
-
}
|
|
2842
|
-
|
|
2843
|
-
function rpcError(id, code, message) {
|
|
2844
|
-
return { jsonrpc: "2.0", id, error: { code, message } };
|
|
2845
|
-
}
|
|
2846
|
-
|
|
2847
|
-
async function handleRpc(message) {
|
|
2848
|
-
if (!isPlainObject(message)) return rpcError(null, -32600, "Invalid Request");
|
|
2849
|
-
const messageId = message.id;
|
|
2850
|
-
const method = message.method;
|
|
2851
|
-
const params = isPlainObject(message.params) ? message.params : {};
|
|
2852
|
-
if (typeof method !== "string") return messageId != null ? rpcError(messageId, -32600, "Invalid Request") : null;
|
|
2853
|
-
if (method.startsWith("notifications/") || method === "$/cancelRequest") return null;
|
|
2854
|
-
try {
|
|
2855
|
-
if (method === "initialize") {
|
|
2856
|
-
return rpcResponse(messageId, {
|
|
2857
|
-
protocolVersion: params.protocolVersion || "2024-11-05",
|
|
2858
|
-
capabilities: {
|
|
2859
|
-
tools: { listChanged: false },
|
|
2860
|
-
resources: { subscribe: false, listChanged: false },
|
|
2861
|
-
},
|
|
2862
|
-
serverInfo: {
|
|
2863
|
-
name: SERVER_NAME,
|
|
2864
|
-
title: "Data Analytics Widgets",
|
|
2865
|
-
version: SERVER_VERSION,
|
|
2866
|
-
description: "Render Data Analytics charts, tables, dashboards, and report artifacts.",
|
|
2867
|
-
icons: DATA_ANALYTICS_ICONS,
|
|
2868
|
-
},
|
|
2869
|
-
instructions: SERVER_INSTRUCTIONS,
|
|
2870
|
-
});
|
|
2871
|
-
}
|
|
2872
|
-
if (method === "ping") return rpcResponse(messageId, {});
|
|
2873
|
-
if (method === "tools/list") return rpcResponse(messageId, { tools: toolDefinitions() });
|
|
2874
|
-
if (method === "tools/call") {
|
|
2875
|
-
const name = params.name;
|
|
2876
|
-
if (typeof name !== "string") return rpcError(messageId, -32602, "tools/call requires a tool name");
|
|
2877
|
-
const args = params.arguments || {};
|
|
2878
|
-
if (!isPlainObject(args)) return rpcError(messageId, -32602, "tools/call arguments must be an object");
|
|
2879
|
-
try {
|
|
2880
|
-
return rpcResponse(messageId, toolResult(callTool(name, args), name));
|
|
2881
|
-
} catch (error) {
|
|
2882
|
-
return rpcResponse(messageId, toolError(error && error.message ? error.message : String(error)));
|
|
2883
|
-
}
|
|
2884
|
-
}
|
|
2885
|
-
if (method === "resources/list") return rpcResponse(messageId, { resources: resources() });
|
|
2886
|
-
if (method === "resources/read") {
|
|
2887
|
-
const uri = params.uri;
|
|
2888
|
-
if (typeof uri !== "string") return rpcError(messageId, -32602, "resources/read requires a resource uri");
|
|
2889
|
-
if (!knownResourceUri(uri)) {
|
|
2890
|
-
return rpcError(messageId, -32602, `unknown Data Analytics widget resource: ${uri}`);
|
|
2891
|
-
}
|
|
2892
|
-
return rpcResponse(messageId, {
|
|
2893
|
-
contents: [
|
|
2894
|
-
{
|
|
2895
|
-
uri,
|
|
2896
|
-
mimeType: WIDGET_MIME_TYPE,
|
|
2897
|
-
text: resourceText(uri),
|
|
2898
|
-
_meta: widgetResourceMeta(uri),
|
|
2899
|
-
},
|
|
2900
|
-
],
|
|
2901
|
-
});
|
|
2902
|
-
}
|
|
2903
|
-
if (method === "resources/templates/list") return rpcResponse(messageId, { resourceTemplates: [] });
|
|
2904
|
-
if (method === "prompts/list") return rpcResponse(messageId, { prompts: [] });
|
|
2905
|
-
} catch (error) {
|
|
2906
|
-
return rpcError(messageId, -32000, error && error.message ? error.message : String(error));
|
|
2907
|
-
}
|
|
2908
|
-
return rpcError(messageId, -32601, `Method not found: ${method}`);
|
|
2909
|
-
}
|
|
2910
|
-
|
|
2911
|
-
function writeRpc(message) {
|
|
2912
|
-
process.stdout.write(`${JSON.stringify(message)}\n`);
|
|
2913
|
-
}
|
|
2914
|
-
|
|
2915
|
-
function runStdio() {
|
|
2916
|
-
const rl = readline.createInterface({ input: process.stdin });
|
|
2917
|
-
rl.on("line", async (line) => {
|
|
2918
|
-
const trimmed = line.trim();
|
|
2919
|
-
if (!trimmed) return;
|
|
2920
|
-
let decoded;
|
|
2921
|
-
try {
|
|
2922
|
-
decoded = JSON.parse(trimmed);
|
|
2923
|
-
} catch (error) {
|
|
2924
|
-
writeRpc(rpcError(null, -32700, `Parse error: ${error.message}`));
|
|
2925
|
-
return;
|
|
2926
|
-
}
|
|
2927
|
-
if (Array.isArray(decoded)) {
|
|
2928
|
-
const responses = [];
|
|
2929
|
-
for (const request of decoded) {
|
|
2930
|
-
const response = await handleRpc(request);
|
|
2931
|
-
if (response) responses.push(response);
|
|
2932
|
-
}
|
|
2933
|
-
if (responses.length) writeRpc(responses);
|
|
2934
|
-
return;
|
|
2935
|
-
}
|
|
2936
|
-
const response = await handleRpc(decoded);
|
|
2937
|
-
if (response) writeRpc(response);
|
|
2938
|
-
});
|
|
2939
|
-
}
|
|
2940
|
-
|
|
2941
|
-
module.exports = {
|
|
2942
|
-
SERVER_NAME,
|
|
2943
|
-
SERVER_VERSION,
|
|
2944
|
-
ARTIFACT_WIDGET_URI,
|
|
2945
|
-
TABLE_WIDGET_URI,
|
|
2946
|
-
CHART_WIDGET_URI,
|
|
2947
|
-
SERVER_INSTRUCTIONS,
|
|
2948
|
-
DATA_ANALYTICS_ICONS,
|
|
2949
|
-
MEASURE_NAMES_FIELD,
|
|
2950
|
-
MAX_WIDGET_ROWS,
|
|
2951
|
-
MAX_ARTIFACT_DATASETS,
|
|
2952
|
-
MAX_ARTIFACT_ROWS_PER_DATASET,
|
|
2953
|
-
MAX_ARTIFACT_PAYLOAD_BYTES,
|
|
2954
|
-
MAX_ARTIFACT_INLINE_SOURCE_CHARS,
|
|
2955
|
-
toolDefinitions,
|
|
2956
|
-
resources,
|
|
2957
|
-
callTool,
|
|
2958
|
-
exportDataScienceArtifactPackage,
|
|
2959
|
-
handleRpc,
|
|
2960
|
-
};
|
|
2961
|
-
|
|
2962
|
-
if (require.main === module) {
|
|
2963
|
-
runStdio();
|
|
2964
|
-
}
|