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,1500 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import { readFileSync } from "node:fs";
|
|
3
|
-
import { createRequire } from "node:module";
|
|
4
|
-
import { test } from "node:test";
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
const require = createRequire(import.meta.url);
|
|
8
|
-
const server = require("../mcp/server.cjs");
|
|
9
|
-
|
|
10
|
-
function sourceQueryForTest() {
|
|
11
|
-
return {
|
|
12
|
-
query: {
|
|
13
|
-
engine: "trino",
|
|
14
|
-
sql: "SELECT category, value FROM warehouse.chart_source",
|
|
15
|
-
description: "Loads category/value rows for chart tests.",
|
|
16
|
-
id: "test-query",
|
|
17
|
-
},
|
|
18
|
-
};
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function queryTablePayload() {
|
|
22
|
-
return {
|
|
23
|
-
title: "ARR trend",
|
|
24
|
-
source: {
|
|
25
|
-
query: {
|
|
26
|
-
engine: "databricks",
|
|
27
|
-
sql: "SELECT reporting_date, arr_b FROM gtm.example",
|
|
28
|
-
description: "Loads ARR by reporting date for widget tests.",
|
|
29
|
-
id: "query-123",
|
|
30
|
-
executed_at: "2026-05-01T00:00:00Z",
|
|
31
|
-
},
|
|
32
|
-
},
|
|
33
|
-
table: {
|
|
34
|
-
columns: [
|
|
35
|
-
{ key: "reporting_date", label: "Reporting date", type: "date" },
|
|
36
|
-
{ key: "arr_b", label: "ARR", type: "number", unit: "$B" },
|
|
37
|
-
],
|
|
38
|
-
rows: [
|
|
39
|
-
{ reporting_date: "2026-03-31", arr_b: 2.73 },
|
|
40
|
-
{ reporting_date: "2026-04-30", arr_b: 2.9 },
|
|
41
|
-
],
|
|
42
|
-
row_count: 2,
|
|
43
|
-
truncated: false,
|
|
44
|
-
},
|
|
45
|
-
chart: {
|
|
46
|
-
type: "line",
|
|
47
|
-
fields: {
|
|
48
|
-
x: { field: "reporting_date", type: "temporal", time_unit: "month" },
|
|
49
|
-
y: { field: "arr_b", type: "quantitative", aggregate: "sum", unit: "$B" },
|
|
50
|
-
},
|
|
51
|
-
},
|
|
52
|
-
display: {
|
|
53
|
-
baseline: 2.8,
|
|
54
|
-
unit: "$B",
|
|
55
|
-
controls: true,
|
|
56
|
-
},
|
|
57
|
-
};
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function artifactPayload(surface = "dashboard") {
|
|
61
|
-
const manifest = {
|
|
62
|
-
version: 1,
|
|
63
|
-
surface,
|
|
64
|
-
title: "Revenue momentum",
|
|
65
|
-
generatedAt: "2026-05-07T00:00:00Z",
|
|
66
|
-
cards: [
|
|
67
|
-
{
|
|
68
|
-
id: "revenue_card",
|
|
69
|
-
dataset: "weekly_revenue",
|
|
70
|
-
metrics: [{ label: "Revenue", field: "revenue_m", format: "currency" }],
|
|
71
|
-
},
|
|
72
|
-
],
|
|
73
|
-
charts: [
|
|
74
|
-
{
|
|
75
|
-
id: "revenue_chart",
|
|
76
|
-
title: "Revenue by segment",
|
|
77
|
-
type: "bar",
|
|
78
|
-
dataset: "weekly_revenue",
|
|
79
|
-
sourceId: "weekly_revenue_sql",
|
|
80
|
-
encodings: {
|
|
81
|
-
x: { field: "segment", type: "nominal" },
|
|
82
|
-
y: { field: "revenue_m", type: "quantitative" },
|
|
83
|
-
},
|
|
84
|
-
},
|
|
85
|
-
],
|
|
86
|
-
tables: [
|
|
87
|
-
{
|
|
88
|
-
id: "revenue_table",
|
|
89
|
-
title: "Revenue rows",
|
|
90
|
-
dataset: "weekly_revenue",
|
|
91
|
-
sourceId: "weekly_revenue_sql",
|
|
92
|
-
columns: [
|
|
93
|
-
{ field: "segment", label: "Segment" },
|
|
94
|
-
{ field: "revenue_m", label: "Revenue", format: "currency" },
|
|
95
|
-
],
|
|
96
|
-
},
|
|
97
|
-
],
|
|
98
|
-
sources: [{ id: "weekly_revenue_sql", label: "Revenue SQL", path: "queries/revenue.sql" }],
|
|
99
|
-
};
|
|
100
|
-
if (surface === "report") {
|
|
101
|
-
manifest.blocks = [
|
|
102
|
-
{
|
|
103
|
-
id: "summary_text",
|
|
104
|
-
type: "markdown",
|
|
105
|
-
body: "Revenue is concentrated in Beta.",
|
|
106
|
-
},
|
|
107
|
-
{
|
|
108
|
-
id: "revenue_chart_block",
|
|
109
|
-
type: "chart",
|
|
110
|
-
chartId: "revenue_chart",
|
|
111
|
-
},
|
|
112
|
-
{
|
|
113
|
-
id: "revenue_table_block",
|
|
114
|
-
type: "table",
|
|
115
|
-
tableId: "revenue_table",
|
|
116
|
-
},
|
|
117
|
-
];
|
|
118
|
-
} else {
|
|
119
|
-
manifest.blocks = [
|
|
120
|
-
{
|
|
121
|
-
id: "revenue_metrics",
|
|
122
|
-
type: "metric-strip",
|
|
123
|
-
cardIds: ["revenue_card"],
|
|
124
|
-
},
|
|
125
|
-
{
|
|
126
|
-
id: "revenue_chart_block",
|
|
127
|
-
type: "chart",
|
|
128
|
-
chartId: "revenue_chart",
|
|
129
|
-
},
|
|
130
|
-
{
|
|
131
|
-
id: "revenue_table_block",
|
|
132
|
-
type: "table",
|
|
133
|
-
tableId: "revenue_table",
|
|
134
|
-
},
|
|
135
|
-
];
|
|
136
|
-
}
|
|
137
|
-
return {
|
|
138
|
-
surface,
|
|
139
|
-
manifest,
|
|
140
|
-
snapshot: {
|
|
141
|
-
version: 1,
|
|
142
|
-
generatedAt: "2026-05-07T00:00:00Z",
|
|
143
|
-
status: "ready",
|
|
144
|
-
datasets: {
|
|
145
|
-
weekly_revenue: [
|
|
146
|
-
{ segment: "Alpha", revenue_m: 12 },
|
|
147
|
-
{ segment: "Beta", revenue_m: 18 },
|
|
148
|
-
],
|
|
149
|
-
},
|
|
150
|
-
},
|
|
151
|
-
sources: [
|
|
152
|
-
{
|
|
153
|
-
id: "weekly_revenue_sql",
|
|
154
|
-
query: {
|
|
155
|
-
engine: "trino",
|
|
156
|
-
sql: "SELECT segment, revenue_m FROM warehouse.weekly_revenue",
|
|
157
|
-
description: "Loads weekly revenue by segment for artifact tests.",
|
|
158
|
-
},
|
|
159
|
-
},
|
|
160
|
-
],
|
|
161
|
-
};
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
async function widgetResourceHtml(uri) {
|
|
165
|
-
const response = await server.handleRpc({
|
|
166
|
-
jsonrpc: "2.0",
|
|
167
|
-
id: 1,
|
|
168
|
-
method: "resources/read",
|
|
169
|
-
params: { uri },
|
|
170
|
-
});
|
|
171
|
-
return response.result.contents[0].text;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
test("MCP widget resources serve bundled apps, not local development redirects", async () => {
|
|
175
|
-
const chartHtml = await widgetResourceHtml(server.CHART_WIDGET_URI);
|
|
176
|
-
const tableHtml = await widgetResourceHtml(server.TABLE_WIDGET_URI);
|
|
177
|
-
const artifactHtml = await widgetResourceHtml(server.ARTIFACT_WIDGET_URI);
|
|
178
|
-
|
|
179
|
-
for (const html of [chartHtml, tableHtml, artifactHtml]) {
|
|
180
|
-
assert.doesNotMatch(html, /Redirecting to the local widget source/);
|
|
181
|
-
assert.doesNotMatch(html, /window\.location\.replace\(target\)/);
|
|
182
|
-
}
|
|
183
|
-
assert.match(chartHtml, /@modelcontextprotocol\/ext-apps/);
|
|
184
|
-
assert.match(tableHtml, /@modelcontextprotocol\/ext-apps/);
|
|
185
|
-
assert.match(artifactHtml, /Data Analytics Artifact App/);
|
|
186
|
-
});
|
|
187
|
-
|
|
188
|
-
test("JavaScript MCP server renders hosted artifact payloads", () => {
|
|
189
|
-
const payload = server.callTool("render_artifact", artifactPayload("report"));
|
|
190
|
-
|
|
191
|
-
assert.equal(payload.widget_type, "artifact");
|
|
192
|
-
assert.equal(payload.surface, "report");
|
|
193
|
-
assert.equal(payload.manifest.title, "Revenue momentum");
|
|
194
|
-
assert.equal(payload.snapshot.datasets.weekly_revenue[1].segment, "Beta");
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
test("JavaScript MCP server exposes only canonical tool names", () => {
|
|
198
|
-
const names = server.toolDefinitions().map((tool) => tool.name);
|
|
199
|
-
assert.deepEqual(names, [
|
|
200
|
-
"validate_artifact",
|
|
201
|
-
"render_artifact",
|
|
202
|
-
"export_artifact_package",
|
|
203
|
-
"render_chart",
|
|
204
|
-
"render_table",
|
|
205
|
-
]);
|
|
206
|
-
|
|
207
|
-
assert.throws(
|
|
208
|
-
() => server.callTool("render_datascience_chart", queryTablePayload()),
|
|
209
|
-
/unknown Data Analytics widget tool: render_datascience_chart/,
|
|
210
|
-
);
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
test("JavaScript MCP server advertises Data Analytics icons on server info", async () => {
|
|
214
|
-
assert.equal(server.DATA_ANALYTICS_ICONS[0].mimeType, "image/svg+xml");
|
|
215
|
-
assert.deepEqual(server.DATA_ANALYTICS_ICONS[0].sizes, ["24x24"]);
|
|
216
|
-
assert.match(server.DATA_ANALYTICS_ICONS[0].src, /^data:image\/svg\+xml;base64,/);
|
|
217
|
-
assert.equal(server.DATA_ANALYTICS_ICONS[1].mimeType, "image/png");
|
|
218
|
-
assert.deepEqual(server.DATA_ANALYTICS_ICONS[1].sizes, ["360x360"]);
|
|
219
|
-
assert.match(server.DATA_ANALYTICS_ICONS[1].src, /^data:image\/png;base64,/);
|
|
220
|
-
|
|
221
|
-
for (const tool of server.toolDefinitions()) {
|
|
222
|
-
assert.equal(tool.icons, undefined);
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
const response = await server.handleRpc({
|
|
226
|
-
jsonrpc: "2.0",
|
|
227
|
-
id: 1,
|
|
228
|
-
method: "initialize",
|
|
229
|
-
params: {},
|
|
230
|
-
});
|
|
231
|
-
assert.equal(response.result.serverInfo.title, "Data Analytics Widgets");
|
|
232
|
-
assert.equal(
|
|
233
|
-
response.result.serverInfo.description,
|
|
234
|
-
"Render Data Analytics charts, tables, dashboards, and report artifacts.",
|
|
235
|
-
);
|
|
236
|
-
assert.deepEqual(response.result.serverInfo.icons, server.DATA_ANALYTICS_ICONS);
|
|
237
|
-
});
|
|
238
|
-
|
|
239
|
-
test("JavaScript MCP server validates artifact payloads without widget metadata", async () => {
|
|
240
|
-
const payload = server.callTool("validate_artifact", artifactPayload("report"));
|
|
241
|
-
|
|
242
|
-
assert.equal(payload.ok, true);
|
|
243
|
-
assert.equal(payload.validation_type, "artifact");
|
|
244
|
-
assert.equal(payload.surface, "report");
|
|
245
|
-
assert.equal(payload.manifest_title, "Revenue momentum");
|
|
246
|
-
assert.equal(payload.dataset_count, 1);
|
|
247
|
-
assert.equal(payload.artifact_payload.widget_type, "artifact");
|
|
248
|
-
|
|
249
|
-
const validateTool = server
|
|
250
|
-
.toolDefinitions()
|
|
251
|
-
.find((tool) => tool.name === "validate_artifact");
|
|
252
|
-
assert.ok(validateTool);
|
|
253
|
-
assert.equal(validateTool._meta, undefined);
|
|
254
|
-
|
|
255
|
-
const rpcPayload = await server.handleRpc({
|
|
256
|
-
jsonrpc: "2.0",
|
|
257
|
-
id: 1,
|
|
258
|
-
method: "tools/call",
|
|
259
|
-
params: { name: "validate_artifact", arguments: artifactPayload("report") },
|
|
260
|
-
});
|
|
261
|
-
assert.equal(rpcPayload.result.structuredContent.ok, true);
|
|
262
|
-
assert.equal(rpcPayload.result._meta, undefined);
|
|
263
|
-
});
|
|
264
|
-
|
|
265
|
-
test("JavaScript MCP server rejects artifact chart y encodings without numeric values", () => {
|
|
266
|
-
const args = artifactPayload("report");
|
|
267
|
-
args.manifest.charts[0] = {
|
|
268
|
-
...args.manifest.charts[0],
|
|
269
|
-
title: "Product-line share by country",
|
|
270
|
-
dataset: "product_line_share",
|
|
271
|
-
encodings: {
|
|
272
|
-
x: { field: "country_name", type: "nominal" },
|
|
273
|
-
y: { field: "product_line", type: "nominal" },
|
|
274
|
-
},
|
|
275
|
-
};
|
|
276
|
-
args.snapshot.datasets.product_line_share = [
|
|
277
|
-
{ country_name: "India", product_line: "Consumer", share_pct: 71 },
|
|
278
|
-
{ country_name: "India", product_line: "API", share_pct: 18 },
|
|
279
|
-
];
|
|
280
|
-
|
|
281
|
-
assert.throws(
|
|
282
|
-
() => server.callTool("validate_artifact", args),
|
|
283
|
-
/manifest\.charts\[0\]\.encodings\.y\.field "product_line" must reference a numeric dataset field/,
|
|
284
|
-
);
|
|
285
|
-
});
|
|
286
|
-
|
|
287
|
-
test("JavaScript MCP server rejects legacy artifact chart fields", () => {
|
|
288
|
-
const args = artifactPayload("report");
|
|
289
|
-
args.manifest.charts[0].xField = "segment";
|
|
290
|
-
|
|
291
|
-
assert.throws(
|
|
292
|
-
() => server.callTool("validate_artifact", args),
|
|
293
|
-
/manifest\.charts\[0\]\.xField is not supported for artifact charts; use encodings/,
|
|
294
|
-
);
|
|
295
|
-
});
|
|
296
|
-
|
|
297
|
-
test("JavaScript MCP server rejects table-shaped artifact snapshot datasets", () => {
|
|
298
|
-
const args = artifactPayload("report");
|
|
299
|
-
args.snapshot.datasets.weekly_revenue = {
|
|
300
|
-
columns: [
|
|
301
|
-
{ key: "segment", label: "Segment" },
|
|
302
|
-
{ key: "revenue_m", label: "Revenue", type: "currency" },
|
|
303
|
-
],
|
|
304
|
-
rows: args.snapshot.datasets.weekly_revenue,
|
|
305
|
-
};
|
|
306
|
-
|
|
307
|
-
assert.throws(
|
|
308
|
-
() => server.callTool("render_artifact", args),
|
|
309
|
-
/snapshot\.datasets\.weekly_revenue must be an array of row objects/,
|
|
310
|
-
);
|
|
311
|
-
});
|
|
312
|
-
|
|
313
|
-
test("JavaScript MCP server rejects key aliases for report table columns", () => {
|
|
314
|
-
const args = artifactPayload("report");
|
|
315
|
-
args.manifest.tables[0].columns = [
|
|
316
|
-
{ key: "segment", label: "Segment" },
|
|
317
|
-
{ key: "revenue_m", label: "Revenue", type: "currency" },
|
|
318
|
-
];
|
|
319
|
-
|
|
320
|
-
assert.throws(
|
|
321
|
-
() => server.callTool("render_artifact", args),
|
|
322
|
-
/manifest\.tables\["revenue_table"\]\.columns\[0\]\.field/,
|
|
323
|
-
);
|
|
324
|
-
});
|
|
325
|
-
|
|
326
|
-
test("JavaScript MCP server accepts body strings in report markdown blocks", () => {
|
|
327
|
-
const args = artifactPayload("report");
|
|
328
|
-
args.manifest.blocks[0] = {
|
|
329
|
-
id: "summary_text",
|
|
330
|
-
type: "markdown",
|
|
331
|
-
body: "## Summary\n\n- **Beta** leads\n- [Source](https://example.com)",
|
|
332
|
-
};
|
|
333
|
-
|
|
334
|
-
const payload = server.callTool("render_artifact", args);
|
|
335
|
-
|
|
336
|
-
assert.equal(payload.manifest.blocks[0].body, "## Summary\n\n- **Beta** leads\n- [Source](https://example.com)");
|
|
337
|
-
});
|
|
338
|
-
|
|
339
|
-
test("JavaScript MCP server rejects legacy narrative fields and removed block types", () => {
|
|
340
|
-
const args = artifactPayload("report");
|
|
341
|
-
args.manifest.blocks[0].markdown = "Legacy markdown field";
|
|
342
|
-
delete args.manifest.blocks[0].body;
|
|
343
|
-
assert.throws(
|
|
344
|
-
() => server.callTool("render_artifact", args),
|
|
345
|
-
/uses markdown, but markdown blocks render body/,
|
|
346
|
-
);
|
|
347
|
-
|
|
348
|
-
const titled = artifactPayload("report");
|
|
349
|
-
titled.manifest.blocks[0].title = "Legacy title";
|
|
350
|
-
titled.manifest.blocks[0].content = "Legacy content";
|
|
351
|
-
delete titled.manifest.blocks[0].body;
|
|
352
|
-
assert.throws(
|
|
353
|
-
() => server.callTool("render_artifact", titled),
|
|
354
|
-
/uses content, but markdown blocks render body/,
|
|
355
|
-
);
|
|
356
|
-
|
|
357
|
-
const bodyArray = artifactPayload("report");
|
|
358
|
-
bodyArray.manifest.blocks[0].body = ["Legacy array body"];
|
|
359
|
-
assert.throws(
|
|
360
|
-
() => server.callTool("render_artifact", bodyArray),
|
|
361
|
-
/body must be a non-empty string/,
|
|
362
|
-
);
|
|
363
|
-
|
|
364
|
-
const textType = artifactPayload("report");
|
|
365
|
-
textType.manifest.blocks[0].type = "text";
|
|
366
|
-
assert.throws(
|
|
367
|
-
() => server.callTool("render_artifact", textType),
|
|
368
|
-
/type must be one of markdown, metric-strip, chart, table, html/,
|
|
369
|
-
);
|
|
370
|
-
|
|
371
|
-
const removedType = artifactPayload("report");
|
|
372
|
-
removedType.manifest.blocks.push({
|
|
373
|
-
id: "next_steps",
|
|
374
|
-
type: "recommendation",
|
|
375
|
-
body: "Review account drivers.",
|
|
376
|
-
});
|
|
377
|
-
assert.throws(
|
|
378
|
-
() => server.callTool("render_artifact", removedType),
|
|
379
|
-
/type must be one of markdown, metric-strip, chart, table, html/,
|
|
380
|
-
);
|
|
381
|
-
});
|
|
382
|
-
|
|
383
|
-
test("JavaScript MCP server preserves inline ordered-list markers in report body", () => {
|
|
384
|
-
const args = artifactPayload("report");
|
|
385
|
-
args.manifest.blocks[0] = {
|
|
386
|
-
id: "summary_text",
|
|
387
|
-
type: "markdown",
|
|
388
|
-
body: "1. Review account drivers 2. Ask account teams for context 3. Monitor next week",
|
|
389
|
-
};
|
|
390
|
-
args.manifest.blocks.push({
|
|
391
|
-
id: "questions_text",
|
|
392
|
-
type: "markdown",
|
|
393
|
-
body: "1. Is the gain durable? 2. Which migrations explain it?",
|
|
394
|
-
});
|
|
395
|
-
|
|
396
|
-
const payload = server.callTool("render_artifact", args);
|
|
397
|
-
|
|
398
|
-
assert.equal(
|
|
399
|
-
payload.manifest.blocks[0].body,
|
|
400
|
-
"1. Review account drivers 2. Ask account teams for context 3. Monitor next week",
|
|
401
|
-
);
|
|
402
|
-
assert.equal(
|
|
403
|
-
payload.manifest.blocks[3].body,
|
|
404
|
-
"1. Is the gain durable? 2. Which migrations explain it?",
|
|
405
|
-
);
|
|
406
|
-
});
|
|
407
|
-
|
|
408
|
-
test("JavaScript MCP server leaves percent chart scale to the manifest data contract", () => {
|
|
409
|
-
const args = artifactPayload("report");
|
|
410
|
-
args.manifest.charts[0].valueFormat = "percent";
|
|
411
|
-
args.manifest.charts[0].encodings.y = { field: "share", type: "quantitative" };
|
|
412
|
-
args.snapshot.datasets.weekly_revenue = [
|
|
413
|
-
{ segment: "Alpha", share: 2.0 },
|
|
414
|
-
{ segment: "Beta", share: 0.851 },
|
|
415
|
-
];
|
|
416
|
-
|
|
417
|
-
assert.equal(server.callTool("validate_artifact", args).ok, true);
|
|
418
|
-
});
|
|
419
|
-
|
|
420
|
-
test("JavaScript MCP server requires explicit artifact chart source links", () => {
|
|
421
|
-
const args = artifactPayload("report");
|
|
422
|
-
delete args.manifest.charts[0].sourceId;
|
|
423
|
-
|
|
424
|
-
assert.throws(
|
|
425
|
-
() => server.callTool("validate_artifact", args),
|
|
426
|
-
/actual SQL query text/,
|
|
427
|
-
);
|
|
428
|
-
});
|
|
429
|
-
|
|
430
|
-
test("JavaScript MCP server accepts custom HTML artifact blocks", () => {
|
|
431
|
-
const args = artifactPayload("report");
|
|
432
|
-
args.manifest.blocks.push({
|
|
433
|
-
id: "custom_html",
|
|
434
|
-
type: "html",
|
|
435
|
-
body: '<section style="font: 14px sans-serif"><strong>Custom HTML</strong><div data-value="42"></div></section>',
|
|
436
|
-
});
|
|
437
|
-
|
|
438
|
-
const payload = server.callTool("render_artifact", args);
|
|
439
|
-
|
|
440
|
-
assert.equal(payload.manifest.blocks[3].type, "html");
|
|
441
|
-
assert.match(payload.manifest.blocks[3].body, /Custom HTML/);
|
|
442
|
-
|
|
443
|
-
const htmlField = artifactPayload("report");
|
|
444
|
-
htmlField.manifest.blocks.push({
|
|
445
|
-
id: "custom_html",
|
|
446
|
-
type: "html",
|
|
447
|
-
html: "<strong>Custom HTML</strong>",
|
|
448
|
-
});
|
|
449
|
-
assert.throws(
|
|
450
|
-
() => server.callTool("render_artifact", htmlField),
|
|
451
|
-
/uses html, but html blocks render body/,
|
|
452
|
-
);
|
|
453
|
-
|
|
454
|
-
const fixedHeight = artifactPayload("report");
|
|
455
|
-
fixedHeight.manifest.blocks.push({
|
|
456
|
-
id: "custom_html",
|
|
457
|
-
type: "html",
|
|
458
|
-
body: "<strong>Custom HTML</strong>",
|
|
459
|
-
height: 240,
|
|
460
|
-
});
|
|
461
|
-
assert.throws(
|
|
462
|
-
() => server.callTool("render_artifact", fixedHeight),
|
|
463
|
-
/height is not supported for html blocks/,
|
|
464
|
-
);
|
|
465
|
-
|
|
466
|
-
const missingHtml = artifactPayload("report");
|
|
467
|
-
missingHtml.manifest.blocks.push({
|
|
468
|
-
id: "custom_html",
|
|
469
|
-
type: "html",
|
|
470
|
-
});
|
|
471
|
-
|
|
472
|
-
assert.throws(
|
|
473
|
-
() => server.callTool("validate_artifact", missingHtml),
|
|
474
|
-
/manifest\.blocks\[3\]\.body must be a non-empty HTML string/,
|
|
475
|
-
);
|
|
476
|
-
});
|
|
477
|
-
|
|
478
|
-
test("JavaScript MCP server accepts additional ordered report blocks", () => {
|
|
479
|
-
const args = artifactPayload("report");
|
|
480
|
-
args.manifest.blocks.push({
|
|
481
|
-
id: "questions_text",
|
|
482
|
-
type: "markdown",
|
|
483
|
-
body: "- Is the revenue shift durable?",
|
|
484
|
-
});
|
|
485
|
-
|
|
486
|
-
const payload = server.callTool("render_artifact", args);
|
|
487
|
-
|
|
488
|
-
assert.equal(payload.manifest.blocks[3].id, "questions_text");
|
|
489
|
-
});
|
|
490
|
-
|
|
491
|
-
test("JavaScript MCP server validates metric-strip blocks", () => {
|
|
492
|
-
const args = artifactPayload("report");
|
|
493
|
-
args.manifest.blocks.unshift({
|
|
494
|
-
id: "headline_metrics",
|
|
495
|
-
type: "metric-strip",
|
|
496
|
-
cardIds: ["revenue_card"],
|
|
497
|
-
});
|
|
498
|
-
|
|
499
|
-
const payload = server.callTool("validate_artifact", args);
|
|
500
|
-
assert.equal(payload.ok, true);
|
|
501
|
-
|
|
502
|
-
const missingCard = artifactPayload("report");
|
|
503
|
-
missingCard.manifest.blocks.unshift({
|
|
504
|
-
id: "headline_metrics",
|
|
505
|
-
type: "metric-strip",
|
|
506
|
-
cardIds: ["missing_card"],
|
|
507
|
-
});
|
|
508
|
-
assert.throws(
|
|
509
|
-
() => server.callTool("validate_artifact", missingCard),
|
|
510
|
-
/cardIds\[0\] does not match a manifest card/,
|
|
511
|
-
);
|
|
512
|
-
});
|
|
513
|
-
|
|
514
|
-
test("funnel labels use an explicit high-contrast fill", () => {
|
|
515
|
-
const chartTokens = readFileSync(
|
|
516
|
-
new URL("../src/analytics-app/charting/chart-tokens.css", import.meta.url),
|
|
517
|
-
"utf8",
|
|
518
|
-
);
|
|
519
|
-
const appCss = readFileSync(new URL("../src/analytics-app/styles.css", import.meta.url), "utf8");
|
|
520
|
-
|
|
521
|
-
assert.match(chartTokens, /--ds-chart-funnel-label: #ffffff;/);
|
|
522
|
-
for (const css of [chartTokens, appCss]) {
|
|
523
|
-
const funnelLabelBlock = css.split(".funnel-label", 2)[1].split("}", 1)[0];
|
|
524
|
-
assert.match(funnelLabelBlock, /fill: var\(--ds-chart-funnel-label, #ffffff\);/);
|
|
525
|
-
assert.doesNotMatch(funnelLabelBlock, /mix-blend-mode/);
|
|
526
|
-
assert.doesNotMatch(funnelLabelBlock, /fill: var\(--ds-surface/);
|
|
527
|
-
}
|
|
528
|
-
});
|
|
529
|
-
|
|
530
|
-
test("bar charts render direct value labels without hover", () => {
|
|
531
|
-
const renderer = readFileSync(
|
|
532
|
-
new URL("../src/analytics-app/charting/ChartRenderer.tsx", import.meta.url),
|
|
533
|
-
"utf8",
|
|
534
|
-
);
|
|
535
|
-
const chartTokens = readFileSync(
|
|
536
|
-
new URL("../src/analytics-app/charting/chart-tokens.css", import.meta.url),
|
|
537
|
-
"utf8",
|
|
538
|
-
);
|
|
539
|
-
|
|
540
|
-
assert.match(renderer, /function shouldShowBarValueLabels/);
|
|
541
|
-
assert.match(renderer, /function visibleBarValueLabelSides/);
|
|
542
|
-
assert.match(renderer, /const showBarValueLabels = shouldShowBarValueLabels/);
|
|
543
|
-
assert.match(renderer, /barValueLabelSides\.hasNegative/);
|
|
544
|
-
assert.match(renderer, /barValueLabelSides\.hasNonNegative/);
|
|
545
|
-
assert.match(renderer, /<LabelList/);
|
|
546
|
-
assert.match(renderer, /renderBarValueLabel/);
|
|
547
|
-
assert.match(chartTokens, /\.chart-bar-value-label/);
|
|
548
|
-
});
|
|
549
|
-
|
|
550
|
-
test("signed vertical bar labels are positioned outside negative bars", () => {
|
|
551
|
-
const renderer = readFileSync(
|
|
552
|
-
new URL("../src/analytics-app/charting/ChartRenderer.tsx", import.meta.url),
|
|
553
|
-
"utf8",
|
|
554
|
-
);
|
|
555
|
-
const labelRenderer = renderer
|
|
556
|
-
.split("function renderBarValueLabel", 2)[1]
|
|
557
|
-
.split("function categoryBarTooltipColor", 1)[0];
|
|
558
|
-
|
|
559
|
-
assert.match(labelRenderer, /const top = Math\.min\(numericY, numericY \+ numericHeight\);/);
|
|
560
|
-
assert.match(labelRenderer, /const bottom = Math\.max\(numericY, numericY \+ numericHeight\);/);
|
|
561
|
-
assert.match(labelRenderer, /y=\{isNegative \? bottom \+ BAR_NEGATIVE_VALUE_LABEL_OFFSET : top - BAR_VALUE_LABEL_OFFSET\}/);
|
|
562
|
-
});
|
|
563
|
-
|
|
564
|
-
test("signed vertical bar labels reserve compact axis clearance", () => {
|
|
565
|
-
const labelRenderer = readFileSync(
|
|
566
|
-
new URL("../src/analytics-app/charting/ChartRenderer.tsx", import.meta.url),
|
|
567
|
-
"utf8",
|
|
568
|
-
);
|
|
569
|
-
assert.match(labelRenderer, /const BAR_NEGATIVE_VALUE_LABEL_AXIS_GUTTER = 40;/);
|
|
570
|
-
});
|
|
571
|
-
|
|
572
|
-
test("encoded funnel charts keep a single series even when color is present", () => {
|
|
573
|
-
const helpers = readFileSync(
|
|
574
|
-
new URL("../src/analytics-app/charting/chart-app-helpers.tsx", import.meta.url),
|
|
575
|
-
"utf8",
|
|
576
|
-
);
|
|
577
|
-
|
|
578
|
-
assert.match(helpers, /if \(chart\.type === "funnel" \|\| !colorField \|\| shouldKeepSignedBarAsSingleSeries/);
|
|
579
|
-
});
|
|
580
|
-
|
|
581
|
-
test("encoded grouped scatter rows preserve size and label fields", () => {
|
|
582
|
-
const helpers = readFileSync(
|
|
583
|
-
new URL("../src/analytics-app/charting/chart-app-helpers.tsx", import.meta.url),
|
|
584
|
-
"utf8",
|
|
585
|
-
);
|
|
586
|
-
const groupedSeriesBlock = helpers
|
|
587
|
-
.split("const seriesFields = new Map", 2)[1]
|
|
588
|
-
.split("const rowsByX = new Map", 1)[0];
|
|
589
|
-
|
|
590
|
-
assert.match(groupedSeriesBlock, /if \(chart\.type === "scatter"\)/);
|
|
591
|
-
assert.match(groupedSeriesBlock, /rows: rows\.map/);
|
|
592
|
-
assert.match(groupedSeriesBlock, /\{ \.\.\.row, \[seriesField\]: row\[yField\] \}/);
|
|
593
|
-
});
|
|
594
|
-
|
|
595
|
-
test("encoded grouped scatter renders each bubble from the encoded y field", () => {
|
|
596
|
-
const renderer = readFileSync(
|
|
597
|
-
new URL("../src/analytics-app/charting/ChartRenderer.tsx", import.meta.url),
|
|
598
|
-
"utf8",
|
|
599
|
-
);
|
|
600
|
-
const scatterBlock = renderer
|
|
601
|
-
.split(') : chart.type === "scatter" ? (', 2)[1]
|
|
602
|
-
.split(') : chart.type === "area" || chart.type === "stackedArea" ?', 1)[0];
|
|
603
|
-
|
|
604
|
-
assert.match(renderer, /const scatterYField = chart\.type === "scatter" \? chartEncodingField\(chart, "y"\) : undefined;/);
|
|
605
|
-
assert.match(scatterBlock, /renderYAxis\(scatterYField \?\? firstSeries\.field\)/);
|
|
606
|
-
assert.match(scatterBlock, /const seriesRows = rows\.filter\(\(row\) => asNumber\(row\[series\.field\]\) != null\);/);
|
|
607
|
-
assert.match(scatterBlock, /const scatterDataKey = scatterYField && seriesRows\.some\(\(row\) => asNumber\(row\[scatterYField\]\) != null\)/);
|
|
608
|
-
assert.match(scatterBlock, /data=\{seriesRows\}/);
|
|
609
|
-
assert.match(scatterBlock, /dataKey=\{scatterDataKey\}/);
|
|
610
|
-
});
|
|
611
|
-
|
|
612
|
-
test("legend buttons stay transparent at rest while exposing hover and focus states", () => {
|
|
613
|
-
const chartTokens = readFileSync(
|
|
614
|
-
new URL("../src/analytics-app/charting/chart-tokens.css", import.meta.url),
|
|
615
|
-
"utf8",
|
|
616
|
-
);
|
|
617
|
-
|
|
618
|
-
assert.match(chartTokens, /\.chart-legend-button:hover/);
|
|
619
|
-
assert.match(chartTokens, /\.chart-legend-button:focus-visible/);
|
|
620
|
-
assert.match(chartTokens, /\.chart-legend-button \{[\s\S]*border: 0;[\s\S]*background: transparent;/);
|
|
621
|
-
assert.match(chartTokens, /\.chart-legend-button:hover \{[\s\S]*color: var\(--ds-chart-text\);/);
|
|
622
|
-
assert.match(chartTokens, /box-shadow: 0 0 0 3px/);
|
|
623
|
-
});
|
|
624
|
-
|
|
625
|
-
test("source data modal tables fill the available modal width", () => {
|
|
626
|
-
const app = readFileSync(new URL("../src/analytics-app/App.tsx", import.meta.url), "utf8");
|
|
627
|
-
const css = readFileSync(new URL("../src/analytics-app/styles.css", import.meta.url), "utf8");
|
|
628
|
-
|
|
629
|
-
assert.match(app, /className="source-data-table"/);
|
|
630
|
-
assert.match(css, /\.source-data-table \{/);
|
|
631
|
-
assert.match(css, /\.source-data-table \.table-scroll-content \{/);
|
|
632
|
-
assert.match(css, /width: 100%;/);
|
|
633
|
-
assert.match(css, /min-width: 100%;/);
|
|
634
|
-
});
|
|
635
|
-
|
|
636
|
-
test("funnel shapes use Recharts label view box geometry", () => {
|
|
637
|
-
const renderer = readFileSync(
|
|
638
|
-
new URL("../src/analytics-app/charting/ChartRenderer.tsx", import.meta.url),
|
|
639
|
-
"utf8",
|
|
640
|
-
);
|
|
641
|
-
const shapeRenderer = renderer
|
|
642
|
-
.split("function renderRoundedFunnelShape", 2)[1]
|
|
643
|
-
.split("function renderFunnelCenterLabel", 1)[0];
|
|
644
|
-
|
|
645
|
-
assert.match(shapeRenderer, /labelViewBox\?\.x \?\? x/);
|
|
646
|
-
assert.match(shapeRenderer, /labelViewBox\?\.y \?\? y/);
|
|
647
|
-
assert.match(shapeRenderer, /labelViewBox\?\.upperWidth \?\? upperWidth/);
|
|
648
|
-
assert.match(shapeRenderer, /labelViewBox\?\.lowerWidth \?\? lowerWidth/);
|
|
649
|
-
assert.match(shapeRenderer, /labelViewBox\?\.height \?\? height/);
|
|
650
|
-
});
|
|
651
|
-
|
|
652
|
-
test("category x-axis labels wrap when crowded and rotate only when requested", () => {
|
|
653
|
-
const renderer = readFileSync(
|
|
654
|
-
new URL("../src/analytics-app/charting/ChartRenderer.tsx", import.meta.url),
|
|
655
|
-
"utf8",
|
|
656
|
-
);
|
|
657
|
-
const rotationPolicy = renderer
|
|
658
|
-
.split("function shouldRotateCategoryXAxisLabels", 2)[1]
|
|
659
|
-
.split("function shouldWrapCategoryXAxisLabels", 1)[0];
|
|
660
|
-
const wrappingHeuristic = renderer
|
|
661
|
-
.split("function shouldWrapCategoryXAxisLabels", 2)[1]
|
|
662
|
-
.split("function getFunnelStageColor", 1)[0];
|
|
663
|
-
|
|
664
|
-
assert.match(rotationPolicy, /categoryLabelPolicy === "rotate"/);
|
|
665
|
-
assert.match(wrappingHeuristic, /availableWidth: number/);
|
|
666
|
-
assert.match(wrappingHeuristic, /labelPolicy === "wrap"/);
|
|
667
|
-
assert.match(wrappingHeuristic, /longestLabelWidth <= labelSlotWidth\) return false/);
|
|
668
|
-
assert.match(wrappingHeuristic, /if \(maxLength >= CATEGORY_X_AXIS_LONG_LABEL_LENGTH\) return true/);
|
|
669
|
-
assert.match(renderer, /function WrappedCategoryXAxisTick/);
|
|
670
|
-
});
|
|
671
|
-
|
|
672
|
-
test("numeric axis ticks omit repeated units while axis titles carry unit context", () => {
|
|
673
|
-
const renderer = readFileSync(
|
|
674
|
-
new URL("../src/analytics-app/charting/ChartRenderer.tsx", import.meta.url),
|
|
675
|
-
"utf8",
|
|
676
|
-
);
|
|
677
|
-
const helpers = readFileSync(
|
|
678
|
-
new URL("../src/analytics-app/charting/chart-app-helpers.tsx", import.meta.url),
|
|
679
|
-
"utf8",
|
|
680
|
-
);
|
|
681
|
-
const transforms = readFileSync(
|
|
682
|
-
new URL("../src/analytics-app/charting/chart-transforms.ts", import.meta.url),
|
|
683
|
-
"utf8",
|
|
684
|
-
);
|
|
685
|
-
const numericTick = renderer
|
|
686
|
-
.split("function NumericYAxisTick", 2)[1]
|
|
687
|
-
.split("function XAxisEndpointTick", 1)[0];
|
|
688
|
-
const horizontalBar = renderer
|
|
689
|
-
.split('horizontal && !stacked && (chart.type === "bar" || chart.type === "horizontalBar") ? (', 2)[1]
|
|
690
|
-
.split(") : horizontal && stacked && (", 1)[0];
|
|
691
|
-
|
|
692
|
-
assert.match(numericTick, /formatValue\(payload\?\.value, valueFormat\)/);
|
|
693
|
-
assert.doesNotMatch(numericTick, /\bunit\b/);
|
|
694
|
-
assert.doesNotMatch(renderer, /tickFormatter=\{\(value\) => formatValue\(value, [^}\n]*chart\.unit/);
|
|
695
|
-
assert.match(transforms, /labels: ticks\.map\(\(value\) => formatValue\(value, chart\.valueFormat\)\)/);
|
|
696
|
-
assert.match(helpers, /function chartEncodingAxisTitle/);
|
|
697
|
-
assert.match(helpers, /return `\$\{label\} \(\$\{unit\}\)`/);
|
|
698
|
-
assert.match(horizontalBar, /<XAxis[^>]*label=\{horizontalValueAxisLabel\}/);
|
|
699
|
-
assert.match(horizontalBar, /<YAxis[^>]*label=\{horizontalCategoryAxisLabel\}/);
|
|
700
|
-
assert.doesNotMatch(horizontalBar, /label=\{yAxisLabel\}/);
|
|
701
|
-
assert.doesNotMatch(horizontalBar, /label=\{xAxisLabel\}/);
|
|
702
|
-
});
|
|
703
|
-
|
|
704
|
-
test("the top bar uses the main page background surface", () => {
|
|
705
|
-
const styles = readFileSync(new URL("../src/analytics-app/styles.css", import.meta.url), "utf8");
|
|
706
|
-
const topBar = styles
|
|
707
|
-
.split(".analytics-top-bar {", 2)[1]
|
|
708
|
-
.split("}", 1)[0];
|
|
709
|
-
|
|
710
|
-
assert.match(topBar, /background: var\(--ds-bg\);/);
|
|
711
|
-
assert.doesNotMatch(topBar, /background: var\(--ds-surface\);/);
|
|
712
|
-
});
|
|
713
|
-
|
|
714
|
-
test("artifact table movement cells render explicit positive and negative signs", () => {
|
|
715
|
-
const app = readFileSync(new URL("../src/analytics-app/App.tsx", import.meta.url), "utf8");
|
|
716
|
-
const tableFormatter = app
|
|
717
|
-
.split("function formatTableCellValue", 2)[1]
|
|
718
|
-
.split("function normalizedTableTextLength", 1)[0];
|
|
719
|
-
|
|
720
|
-
assert.match(tableFormatter, /tableColumnLooksLikeMovement\(column\)/);
|
|
721
|
-
assert.match(tableFormatter, /numeric == null \|\| numeric === 0/);
|
|
722
|
-
assert.match(tableFormatter, /return `\$\{numeric > 0 \? "\+" : ""\}\$\{rendered\}`/);
|
|
723
|
-
assert.match(app, /tableCellMovementClass\(column, value\)/);
|
|
724
|
-
});
|
|
725
|
-
|
|
726
|
-
test("artifact card chart type menu is limited by data shape", () => {
|
|
727
|
-
const compatibility = readFileSync(
|
|
728
|
-
new URL("../src/analytics-app/charting/chart-compatibility.ts", import.meta.url),
|
|
729
|
-
"utf8",
|
|
730
|
-
);
|
|
731
|
-
const helpers = readFileSync(
|
|
732
|
-
new URL("../src/analytics-app/charting/chart-app-helpers.tsx", import.meta.url),
|
|
733
|
-
"utf8",
|
|
734
|
-
);
|
|
735
|
-
const app = readFileSync(new URL("../src/analytics-app/App.tsx", import.meta.url), "utf8");
|
|
736
|
-
|
|
737
|
-
assert.match(compatibility, /export function compatibleChartTypesForDataShape/);
|
|
738
|
-
assert.match(compatibility, /const FUNNEL_SHAPE_TYPES: ChartType\[\] = \["bar", "funnel"\]/);
|
|
739
|
-
assert.match(compatibility, /types: \["bar", "leaderboard"\]/);
|
|
740
|
-
assert.match(compatibility, /const HIDDEN_BAR_VARIANT_TYPE_LIST: ChartType\[\] = \[/);
|
|
741
|
-
assert.match(compatibility, /if \(chart\.type === "funnel" \|\| chart\.intent === "funnel"\) return FUNNEL_SHAPE_TYPES;/);
|
|
742
|
-
assert.match(helpers, /export function compatibleChartTypesForArtifactCard/);
|
|
743
|
-
assert.equal((app.match(/compatibleChartTypesForArtifactCard\(overriddenChart, chartRows\)/g) || []).length, 2);
|
|
744
|
-
assert.match(app, /applyChartSpecOverride\(chart, chartSpecOverrides\[chart\.id\]\)/);
|
|
745
|
-
assert.match(app, /const chartModalTypeOptions = chartModalBaseChart\s+\?\s+compatibleChartTypesFor\(chartModalBaseChart, chartModalRows\)/);
|
|
746
|
-
});
|
|
747
|
-
|
|
748
|
-
test("artifact data source dialogs use sectioned shared table content", () => {
|
|
749
|
-
const app = readFileSync(new URL("../src/analytics-app/App.tsx", import.meta.url), "utf8");
|
|
750
|
-
const styles = readFileSync(new URL("../src/analytics-app/styles.css", import.meta.url), "utf8");
|
|
751
|
-
|
|
752
|
-
assert.match(app, /<h3>Details<\/h3>/);
|
|
753
|
-
assert.match(app, /<h3>Source query<\/h3>/);
|
|
754
|
-
assert.match(app, /<h3>Data table<\/h3>/);
|
|
755
|
-
assert.match(app, /function SourceDataTable/);
|
|
756
|
-
assert.match(app, /className="source-data-table"/);
|
|
757
|
-
assert.match(app, /<TableContent allowColumnResize=\{false\} columnWidths=/);
|
|
758
|
-
assert.doesNotMatch(app, /function SourcePreviewRows/);
|
|
759
|
-
assert.match(app, /className="modal-close-button"/);
|
|
760
|
-
assert.match(app, /useModalScrollLock\(true\)/);
|
|
761
|
-
assert.match(styles, /\.native-modal\.source-modal/);
|
|
762
|
-
assert.match(styles, /\.source-query \{[\s\S]*?overflow-x: hidden;[\s\S]*?white-space: pre-wrap;/);
|
|
763
|
-
});
|
|
764
|
-
|
|
765
|
-
test("JavaScript MCP server rejects ready artifacts with access issues", () => {
|
|
766
|
-
const args = artifactPayload("report");
|
|
767
|
-
args.snapshot.accessIssues = [
|
|
768
|
-
{
|
|
769
|
-
id: "optional_join_denied",
|
|
770
|
-
sourceId: "exploratory_join",
|
|
771
|
-
message: "Optional use-case join was not readable.",
|
|
772
|
-
},
|
|
773
|
-
];
|
|
774
|
-
|
|
775
|
-
assert.throws(
|
|
776
|
-
() => server.callTool("render_artifact", args),
|
|
777
|
-
/\$\.snapshot\.accessIssues is only allowed when \$\.snapshot\.status is partial or blocked/,
|
|
778
|
-
);
|
|
779
|
-
});
|
|
780
|
-
|
|
781
|
-
test("JavaScript MCP server rejects malformed report block shapes before rendering", () => {
|
|
782
|
-
const missingTitle = artifactPayload("report");
|
|
783
|
-
delete missingTitle.manifest.title;
|
|
784
|
-
assert.throws(
|
|
785
|
-
() => server.callTool("render_artifact", missingTitle),
|
|
786
|
-
/\$\.manifest\.title is required for artifact rendering/,
|
|
787
|
-
);
|
|
788
|
-
|
|
789
|
-
const blankTitle = artifactPayload("report");
|
|
790
|
-
blankTitle.manifest.title = " ";
|
|
791
|
-
assert.throws(
|
|
792
|
-
() => server.callTool("validate_artifact", blankTitle),
|
|
793
|
-
/\$\.manifest\.title is required for artifact rendering/,
|
|
794
|
-
);
|
|
795
|
-
|
|
796
|
-
const withAudience = artifactPayload("report");
|
|
797
|
-
withAudience.manifest.audience = "product stakeholders";
|
|
798
|
-
assert.throws(
|
|
799
|
-
() => server.callTool("validate_artifact", withAudience),
|
|
800
|
-
/\$\.manifest\.audience is not supported/,
|
|
801
|
-
);
|
|
802
|
-
|
|
803
|
-
const withFreshness = artifactPayload("report");
|
|
804
|
-
withFreshness.manifest.freshness = { snapshotPath: "data/snapshot.json" };
|
|
805
|
-
assert.throws(
|
|
806
|
-
() => server.callTool("validate_artifact", withFreshness),
|
|
807
|
-
/\$\.manifest\.freshness is not supported/,
|
|
808
|
-
);
|
|
809
|
-
|
|
810
|
-
const withLegacyMetricDelta = artifactPayload("report");
|
|
811
|
-
withLegacyMetricDelta.manifest.cards[0].deltaField = "wow";
|
|
812
|
-
assert.throws(
|
|
813
|
-
() => server.callTool("validate_artifact", withLegacyMetricDelta),
|
|
814
|
-
/\$\.manifest\.cards\[0\]\.deltaField is not supported; use metrics\[\]/,
|
|
815
|
-
);
|
|
816
|
-
|
|
817
|
-
const withInitialView = artifactPayload("report");
|
|
818
|
-
withInitialView.initial_view = { displayMode: "inline" };
|
|
819
|
-
assert.throws(
|
|
820
|
-
() => server.callTool("render_artifact", withInitialView),
|
|
821
|
-
/initial_view is not supported/,
|
|
822
|
-
);
|
|
823
|
-
|
|
824
|
-
const withDefinitions = artifactPayload("report");
|
|
825
|
-
withDefinitions.snapshot.definitions = [{ metric: "Revenue", definition: "Booked usage." }];
|
|
826
|
-
assert.throws(
|
|
827
|
-
() => server.callTool("validate_artifact", withDefinitions),
|
|
828
|
-
/\$\.snapshot\.definitions is not supported/,
|
|
829
|
-
);
|
|
830
|
-
|
|
831
|
-
const noBlocks = artifactPayload("report");
|
|
832
|
-
delete noBlocks.manifest.blocks;
|
|
833
|
-
assert.throws(
|
|
834
|
-
() => server.callTool("render_artifact", noBlocks),
|
|
835
|
-
/must contain top-level artifact blocks/,
|
|
836
|
-
);
|
|
837
|
-
});
|
|
838
|
-
|
|
839
|
-
test("JavaScript MCP server allows a visible report heading that matches the report title", () => {
|
|
840
|
-
const args = artifactPayload("report");
|
|
841
|
-
args.manifest.blocks[0].body = "# Revenue momentum";
|
|
842
|
-
|
|
843
|
-
assert.equal(server.callTool("validate_artifact", args).ok, true);
|
|
844
|
-
});
|
|
845
|
-
|
|
846
|
-
test("JavaScript MCP server requires canonical dashboard blocks", () => {
|
|
847
|
-
const noBlocks = artifactPayload("dashboard");
|
|
848
|
-
delete noBlocks.manifest.blocks;
|
|
849
|
-
assert.throws(
|
|
850
|
-
() => server.callTool("render_artifact", noBlocks),
|
|
851
|
-
/must contain top-level artifact blocks/,
|
|
852
|
-
);
|
|
853
|
-
|
|
854
|
-
});
|
|
855
|
-
|
|
856
|
-
test("JavaScript MCP server requires reports to include a chart visualization", () => {
|
|
857
|
-
const args = artifactPayload("report");
|
|
858
|
-
args.manifest.blocks = args.manifest.blocks.filter((block) => block.type !== "chart");
|
|
859
|
-
|
|
860
|
-
assert.throws(
|
|
861
|
-
() => server.callTool("validate_artifact", args),
|
|
862
|
-
/must include at least one chart block for report artifacts/,
|
|
863
|
-
);
|
|
864
|
-
assert.throws(
|
|
865
|
-
() => server.callTool("render_artifact", args),
|
|
866
|
-
/must include at least one chart block for report artifacts/,
|
|
867
|
-
);
|
|
868
|
-
});
|
|
869
|
-
|
|
870
|
-
test("JavaScript MCP server advertises artifact handoff before fallback widgets", () => {
|
|
871
|
-
const artifactTool = server
|
|
872
|
-
.toolDefinitions()
|
|
873
|
-
.find((tool) => tool.name === "render_artifact");
|
|
874
|
-
const validateTool = server
|
|
875
|
-
.toolDefinitions()
|
|
876
|
-
.find((tool) => tool.name === "validate_artifact");
|
|
877
|
-
|
|
878
|
-
assert.ok(artifactTool);
|
|
879
|
-
assert.ok(validateTool);
|
|
880
|
-
assert.match(server.SERVER_INSTRUCTIONS, /^Before rendering a report or dashboard artifact/);
|
|
881
|
-
assert.match(server.SERVER_INSTRUCTIONS, /manifest\.blocks/);
|
|
882
|
-
assert.match(server.SERVER_INSTRUCTIONS, /first markdown block whose body is a # heading matching manifest\.title/);
|
|
883
|
-
assert.match(validateTool.description, /without rendering a hosted widget/);
|
|
884
|
-
assert.equal(validateTool._meta, undefined);
|
|
885
|
-
assert.match(artifactTool.description, /dashboard or report artifact/);
|
|
886
|
-
assert.match(artifactTool.description, /Call validate_artifact first/);
|
|
887
|
-
assert.match(artifactTool.inputSchema.properties.manifest.description, /top-level manifest\.blocks/);
|
|
888
|
-
assert.match(artifactTool.inputSchema.properties.manifest.description, /first content heading/);
|
|
889
|
-
assert.match(artifactTool.inputSchema.properties.manifest.properties.blocks.description, /type "html"/);
|
|
890
|
-
assert.match(artifactTool.inputSchema.properties.manifest.properties.title.description, /first content heading/);
|
|
891
|
-
const manifestSchema = artifactTool.inputSchema.properties.manifest;
|
|
892
|
-
const cardSchema = manifestSchema.properties.cards.items;
|
|
893
|
-
assert.ok(cardSchema.required.includes("metrics"));
|
|
894
|
-
assert.equal(cardSchema.properties.valueField, undefined);
|
|
895
|
-
assert.equal(cardSchema.properties.metrics.items.properties.field.type, "string");
|
|
896
|
-
|
|
897
|
-
const tableColumnSchema = manifestSchema.properties.tables.items.properties.columns.items;
|
|
898
|
-
assert.ok(tableColumnSchema.required.includes("field"));
|
|
899
|
-
assert.equal(tableColumnSchema.properties.key, undefined);
|
|
900
|
-
|
|
901
|
-
const blockSchema = manifestSchema.properties.blocks.items;
|
|
902
|
-
assert.ok(blockSchema.required.includes("id"));
|
|
903
|
-
assert.equal(blockSchema.properties.markdown, undefined);
|
|
904
|
-
assert.deepEqual(artifactTool._meta.ui, {
|
|
905
|
-
resourceUri: server.ARTIFACT_WIDGET_URI,
|
|
906
|
-
visibility: ["model"],
|
|
907
|
-
});
|
|
908
|
-
});
|
|
909
|
-
|
|
910
|
-
test("JavaScript MCP server rejects artifact source and safety violations", () => {
|
|
911
|
-
const args = artifactPayload();
|
|
912
|
-
args.sources[0].id = "not_declared";
|
|
913
|
-
assert.throws(
|
|
914
|
-
() => server.callTool("render_artifact", args),
|
|
915
|
-
/declared in manifest\.sources/,
|
|
916
|
-
);
|
|
917
|
-
|
|
918
|
-
const unsafe = artifactPayload();
|
|
919
|
-
unsafe.snapshot.datasets.weekly_revenue[0].customer_email = "person@example.com";
|
|
920
|
-
assert.throws(
|
|
921
|
-
() => server.callTool("render_artifact", unsafe),
|
|
922
|
-
/looks unsafe/,
|
|
923
|
-
);
|
|
924
|
-
});
|
|
925
|
-
|
|
926
|
-
test("JavaScript MCP server allows reviewed customer dimensions in artifacts", () => {
|
|
927
|
-
const args = artifactPayload();
|
|
928
|
-
args.contains_sensitive_data = true;
|
|
929
|
-
args.snapshot.datasets.weekly_revenue[0].customer_name = "Acme Corp";
|
|
930
|
-
|
|
931
|
-
const payload = server.callTool("render_artifact", args);
|
|
932
|
-
|
|
933
|
-
assert.equal(payload.widget_type, "artifact");
|
|
934
|
-
assert.equal(payload.snapshot.datasets.weekly_revenue[0].customer_name, "Acme Corp");
|
|
935
|
-
});
|
|
936
|
-
|
|
937
|
-
test("JavaScript MCP server renders query-shaped chart payloads", () => {
|
|
938
|
-
const payload = server.callTool("render_chart", queryTablePayload());
|
|
939
|
-
|
|
940
|
-
assert.equal(payload.widget_type, "chart");
|
|
941
|
-
assert.equal(payload.source.query.id, "query-123");
|
|
942
|
-
assert.equal(payload.chart.type, "line");
|
|
943
|
-
assert.equal(payload.chart_spec.type, "line");
|
|
944
|
-
assert.equal(payload.chart_spec.encodings.x.field, "reporting_date");
|
|
945
|
-
assert.equal(payload.chart_spec.encodings.y.field, "arr_b");
|
|
946
|
-
assert.equal("xField" in payload.chart_spec, false);
|
|
947
|
-
assert.equal("series" in payload.chart_spec, false);
|
|
948
|
-
assert.equal(payload.chart_spec.xAxisTitle, undefined);
|
|
949
|
-
assert.equal(payload.chart_spec.yAxisTitle, undefined);
|
|
950
|
-
assert.equal(payload.chart_spec.unit, "$B");
|
|
951
|
-
assert.equal("referenceLines" in payload.chart_spec, false);
|
|
952
|
-
assert.equal("chart_rows" in payload, false);
|
|
953
|
-
assert.equal("x_field" in payload, false);
|
|
954
|
-
assert.equal("y_field" in payload, false);
|
|
955
|
-
assert.equal("x_axis_title" in payload, false);
|
|
956
|
-
assert.equal("y_axis_title" in payload, false);
|
|
957
|
-
assert.equal("time_unit" in payload, false);
|
|
958
|
-
assert.deepEqual(payload.data, [
|
|
959
|
-
{
|
|
960
|
-
reporting_date: "2026-03-31",
|
|
961
|
-
arr_b: 2.73,
|
|
962
|
-
x: "2026-03-31",
|
|
963
|
-
y: 2.73,
|
|
964
|
-
size: null,
|
|
965
|
-
series: "ARR",
|
|
966
|
-
},
|
|
967
|
-
{
|
|
968
|
-
reporting_date: "2026-04-30",
|
|
969
|
-
arr_b: 2.9,
|
|
970
|
-
x: "2026-04-30",
|
|
971
|
-
y: 2.9,
|
|
972
|
-
size: null,
|
|
973
|
-
series: "ARR",
|
|
974
|
-
},
|
|
975
|
-
]);
|
|
976
|
-
});
|
|
977
|
-
|
|
978
|
-
test("JavaScript MCP server requires model-provided chart type and fields", () => {
|
|
979
|
-
const missingType = queryTablePayload();
|
|
980
|
-
delete missingType.chart.type;
|
|
981
|
-
assert.throws(() => server.callTool("render_chart", missingType), /chart\.type is required/);
|
|
982
|
-
|
|
983
|
-
const missingX = queryTablePayload();
|
|
984
|
-
delete missingX.chart.fields.x;
|
|
985
|
-
assert.throws(() => server.callTool("render_chart", missingX), /chart\.fields\.x\.field is required/);
|
|
986
|
-
|
|
987
|
-
const missingY = queryTablePayload();
|
|
988
|
-
delete missingY.chart.fields.y;
|
|
989
|
-
assert.throws(() => server.callTool("render_chart", missingY), /chart\.fields\.y\.field is required/);
|
|
990
|
-
});
|
|
991
|
-
|
|
992
|
-
test("JavaScript MCP server accepts custom chart axis titles", () => {
|
|
993
|
-
const args = queryTablePayload();
|
|
994
|
-
args.display.x_axis_title = "Fiscal month";
|
|
995
|
-
args.display.y_axis_title = "ARR, billions";
|
|
996
|
-
|
|
997
|
-
const payload = server.callTool("render_chart", args);
|
|
998
|
-
|
|
999
|
-
assert.equal(payload.display.x_axis_title, "Fiscal month");
|
|
1000
|
-
assert.equal(payload.display.y_axis_title, "ARR, billions");
|
|
1001
|
-
assert.equal(payload.chart_spec.xAxisTitle, "Fiscal month");
|
|
1002
|
-
assert.equal(payload.chart_spec.yAxisTitle, "ARR, billions");
|
|
1003
|
-
assert.equal("x_label" in payload, false);
|
|
1004
|
-
assert.equal("y_label" in payload, false);
|
|
1005
|
-
});
|
|
1006
|
-
|
|
1007
|
-
test("JavaScript MCP server normalizes heatmap encodings to widget field roles", () => {
|
|
1008
|
-
const payload = server.callTool("render_chart", {
|
|
1009
|
-
title: "Heatmap",
|
|
1010
|
-
source: {
|
|
1011
|
-
query: {
|
|
1012
|
-
engine: "trino",
|
|
1013
|
-
sql: "SELECT day_name, segment, score FROM warehouse.heatmap_source",
|
|
1014
|
-
id: "heatmap-query",
|
|
1015
|
-
},
|
|
1016
|
-
},
|
|
1017
|
-
table: {
|
|
1018
|
-
columns: [
|
|
1019
|
-
{ key: "day_name", label: "Day", type: "text" },
|
|
1020
|
-
{ key: "segment", label: "Segment", type: "text" },
|
|
1021
|
-
{ key: "score", label: "Score", type: "number" },
|
|
1022
|
-
],
|
|
1023
|
-
rows: [
|
|
1024
|
-
{ day_name: "Mon", segment: "Self-serve", score: 18 },
|
|
1025
|
-
{ day_name: "Mon", segment: "Enterprise", score: 26 },
|
|
1026
|
-
{ day_name: "Tue", segment: "Self-serve", score: 21 },
|
|
1027
|
-
{ day_name: "Tue", segment: "Enterprise", score: 31 },
|
|
1028
|
-
],
|
|
1029
|
-
},
|
|
1030
|
-
chart: {
|
|
1031
|
-
type: "heatmap",
|
|
1032
|
-
fields: {
|
|
1033
|
-
x: { field: "day_name", type: "ordinal" },
|
|
1034
|
-
y: { field: "segment", type: "nominal" },
|
|
1035
|
-
color: { field: "score", type: "quantitative" },
|
|
1036
|
-
},
|
|
1037
|
-
},
|
|
1038
|
-
});
|
|
1039
|
-
|
|
1040
|
-
assert.equal(payload.chart.fields.y.field, "score");
|
|
1041
|
-
assert.equal(payload.chart.fields.color.field, "segment");
|
|
1042
|
-
assert.deepEqual(payload.data.map((point) => [point.x, point.y, point.series]), [
|
|
1043
|
-
["Mon", 18, "Self-serve"],
|
|
1044
|
-
["Mon", 26, "Enterprise"],
|
|
1045
|
-
["Tue", 21, "Self-serve"],
|
|
1046
|
-
["Tue", 31, "Enterprise"],
|
|
1047
|
-
]);
|
|
1048
|
-
});
|
|
1049
|
-
|
|
1050
|
-
test("JavaScript MCP server reports heatmap field roles when value encoding is not numeric", () => {
|
|
1051
|
-
assert.throws(() => server.callTool("render_chart", {
|
|
1052
|
-
title: "Heatmap",
|
|
1053
|
-
source: {
|
|
1054
|
-
query: {
|
|
1055
|
-
engine: "trino",
|
|
1056
|
-
sql: "SELECT day_name, segment, score_label FROM warehouse.heatmap_source",
|
|
1057
|
-
id: "heatmap-query",
|
|
1058
|
-
},
|
|
1059
|
-
},
|
|
1060
|
-
table: {
|
|
1061
|
-
columns: [
|
|
1062
|
-
{ key: "day_name", label: "Day", type: "text" },
|
|
1063
|
-
{ key: "segment", label: "Segment", type: "text" },
|
|
1064
|
-
{ key: "score_label", label: "Score label", type: "text" },
|
|
1065
|
-
],
|
|
1066
|
-
rows: [
|
|
1067
|
-
{ day_name: "Mon", segment: "Self-serve", score_label: "Low" },
|
|
1068
|
-
{ day_name: "Mon", segment: "Enterprise", score_label: "High" },
|
|
1069
|
-
],
|
|
1070
|
-
},
|
|
1071
|
-
chart: {
|
|
1072
|
-
type: "heatmap",
|
|
1073
|
-
fields: {
|
|
1074
|
-
x: { field: "day_name", type: "ordinal" },
|
|
1075
|
-
y: { field: "score_label", type: "text" },
|
|
1076
|
-
color: { field: "segment", type: "nominal" },
|
|
1077
|
-
},
|
|
1078
|
-
},
|
|
1079
|
-
}), /numeric heatmap cell value/);
|
|
1080
|
-
});
|
|
1081
|
-
|
|
1082
|
-
test("JavaScript MCP server keeps scatter size on the source field", () => {
|
|
1083
|
-
const payload = server.callTool("render_chart", {
|
|
1084
|
-
title: "ARR efficiency by segment",
|
|
1085
|
-
source: sourceQueryForTest(),
|
|
1086
|
-
table: {
|
|
1087
|
-
columns: [
|
|
1088
|
-
{ key: "conversion_rate", label: "Conversion", type: "percent" },
|
|
1089
|
-
{ key: "arr_usd", label: "ARR", type: "currency", unit: "USD" },
|
|
1090
|
-
{ key: "active_accounts", label: "Active Accounts", type: "number" },
|
|
1091
|
-
{ key: "segment", label: "Segment", type: "text" },
|
|
1092
|
-
],
|
|
1093
|
-
rows: [
|
|
1094
|
-
{ conversion_rate: 0.24, arr_usd: 1445000, active_accounts: 95, segment: "Enterprise" },
|
|
1095
|
-
{ conversion_rate: 0.20, arr_usd: 846000, active_accounts: 161, segment: "Mid-Market" },
|
|
1096
|
-
],
|
|
1097
|
-
},
|
|
1098
|
-
chart: {
|
|
1099
|
-
type: "scatter",
|
|
1100
|
-
fields: {
|
|
1101
|
-
x: { field: "conversion_rate", type: "quantitative" },
|
|
1102
|
-
y: { field: "arr_usd", type: "quantitative" },
|
|
1103
|
-
size: { field: "active_accounts", type: "quantitative" },
|
|
1104
|
-
color: { field: "segment", type: "nominal" },
|
|
1105
|
-
},
|
|
1106
|
-
},
|
|
1107
|
-
});
|
|
1108
|
-
|
|
1109
|
-
assert.equal(payload.chart_spec.encodings.size.field, "active_accounts");
|
|
1110
|
-
assert.equal(payload.chart_spec.encodings.size.label, "Active Accounts");
|
|
1111
|
-
assert.equal(payload.chart_spec.encodings.size.format, "number");
|
|
1112
|
-
assert.equal(payload.chart_spec.encodings.x.field, "conversion_rate");
|
|
1113
|
-
assert.equal(payload.chart_spec.encodings.y.field, "arr_usd");
|
|
1114
|
-
assert.equal(payload.chart_spec.encodings.color.field, "segment");
|
|
1115
|
-
assert.equal("chart_rows" in payload, false);
|
|
1116
|
-
assert.deepEqual(payload.table.rows.map((row) => row.active_accounts), [95, 161]);
|
|
1117
|
-
assert.equal(payload.table.rows.some((row) => "__pointSize" in row), false);
|
|
1118
|
-
});
|
|
1119
|
-
|
|
1120
|
-
test("JavaScript MCP server accepts non-scatter label fields as optional display metadata", () => {
|
|
1121
|
-
const payload = server.callTool("render_chart", {
|
|
1122
|
-
title: "Leaderboard",
|
|
1123
|
-
source: sourceQueryForTest(),
|
|
1124
|
-
table: {
|
|
1125
|
-
columns: [
|
|
1126
|
-
{ key: "rank_order", label: "Rank", type: "number" },
|
|
1127
|
-
{ key: "team", label: "Team", type: "text" },
|
|
1128
|
-
{ key: "score", label: "Score", type: "number" },
|
|
1129
|
-
],
|
|
1130
|
-
rows: [
|
|
1131
|
-
{ rank_order: 1, team: "Team Atlas", score: 980 },
|
|
1132
|
-
{ rank_order: 2, team: "Team Beacon", score: 870 },
|
|
1133
|
-
],
|
|
1134
|
-
},
|
|
1135
|
-
chart: {
|
|
1136
|
-
type: "leaderboard",
|
|
1137
|
-
fields: {
|
|
1138
|
-
x: { field: "team", type: "nominal" },
|
|
1139
|
-
y: { field: "score", type: "quantitative" },
|
|
1140
|
-
label: { field: "rank_order", type: "quantitative" },
|
|
1141
|
-
},
|
|
1142
|
-
},
|
|
1143
|
-
});
|
|
1144
|
-
|
|
1145
|
-
assert.equal(payload.ok, true);
|
|
1146
|
-
assert.equal(payload.chart.fields.label.field, "rank_order");
|
|
1147
|
-
});
|
|
1148
|
-
|
|
1149
|
-
test("JavaScript MCP server exposes chart v1 settings in the chart schema", () => {
|
|
1150
|
-
const chartTool = server.toolDefinitions().find((tool) => tool.name === "render_chart");
|
|
1151
|
-
const properties = chartTool.inputSchema.properties;
|
|
1152
|
-
const chart = chartTool.inputSchema.properties.chart.properties;
|
|
1153
|
-
const options = chart.options.properties;
|
|
1154
|
-
const display = chartTool.inputSchema.properties.display.properties;
|
|
1155
|
-
const tableColumn = properties.table.properties.columns.items.properties;
|
|
1156
|
-
|
|
1157
|
-
assert.match(properties.subtitle.description, /insight or takeaway/);
|
|
1158
|
-
assert.match(properties.subtitle.description, /source\.query/);
|
|
1159
|
-
assert.deepEqual(options.orientation.enum, ["vertical", "horizontal", null]);
|
|
1160
|
-
assert.deepEqual(options.grouping.enum, ["single", "grouped", "stacked", "stacked100", null]);
|
|
1161
|
-
assert.ok(options.points);
|
|
1162
|
-
assert.equal("zero_line" in options, false);
|
|
1163
|
-
assert.equal("limit" in options, false);
|
|
1164
|
-
assert.equal("show_values" in options, false);
|
|
1165
|
-
assert.equal("category_label_policy" in options, false);
|
|
1166
|
-
assert.equal("sort" in options, false);
|
|
1167
|
-
assert.equal("show_latest_value" in options, false);
|
|
1168
|
-
assert.equal("bins" in options, false);
|
|
1169
|
-
assert.equal("max_segments" in options, false);
|
|
1170
|
-
assert.equal("other_threshold" in options, false);
|
|
1171
|
-
assert.equal("show_percent" in options, false);
|
|
1172
|
-
assert.equal("legend" in display, false);
|
|
1173
|
-
assert.equal("view_mode" in display, false);
|
|
1174
|
-
assert.equal("labels" in display, false);
|
|
1175
|
-
assert.equal("number_format" in display, false);
|
|
1176
|
-
assert.deepEqual(tableColumn.format.enum, ["compact", "number", "percent", "currency", null]);
|
|
1177
|
-
assert.equal("version" in chart, false);
|
|
1178
|
-
assert.equal("comparisonContext" in chart, false);
|
|
1179
|
-
assert.equal("notes" in properties, false);
|
|
1180
|
-
assert.equal("datasets" in properties, false);
|
|
1181
|
-
assert.equal("selected_dataset" in properties, false);
|
|
1182
|
-
assert.equal("query_binding" in properties, false);
|
|
1183
|
-
assert.equal("facet" in chart.fields.properties, false);
|
|
1184
|
-
assert.equal("tooltip" in chart.fields.properties, false);
|
|
1185
|
-
assert.equal("sort" in chart, false);
|
|
1186
|
-
assert.equal("notes" in display, false);
|
|
1187
|
-
assert.equal("tooltip" in display, false);
|
|
1188
|
-
assert.equal("empty_state" in display, false);
|
|
1189
|
-
assert.equal("reference_lines" in display, false);
|
|
1190
|
-
assert.match(server.SERVER_INSTRUCTIONS, /chart\.type "bar"/);
|
|
1191
|
-
assert.match(server.SERVER_INSTRUCTIONS, /Chart subtitles should add a reader-facing insight/);
|
|
1192
|
-
});
|
|
1193
|
-
|
|
1194
|
-
test("JavaScript MCP server rejects removed inline snake_case chart names", () => {
|
|
1195
|
-
const args = queryTablePayload();
|
|
1196
|
-
args.chart.type = "grouped_column";
|
|
1197
|
-
assert.throws(
|
|
1198
|
-
() => server.callTool("render_chart", args),
|
|
1199
|
-
/chart\.type must be one of/,
|
|
1200
|
-
);
|
|
1201
|
-
});
|
|
1202
|
-
|
|
1203
|
-
test("JavaScript MCP server strips inline chart notes while table notes remain supported", () => {
|
|
1204
|
-
const chartArgs = queryTablePayload();
|
|
1205
|
-
chartArgs.notes = ["top-level chart note"];
|
|
1206
|
-
chartArgs.chart.version = "legacy";
|
|
1207
|
-
chartArgs.chart.comparisonContext = { baseline: "legacy" };
|
|
1208
|
-
chartArgs.chart.sort = "legacy";
|
|
1209
|
-
chartArgs.chart.options = {
|
|
1210
|
-
show_values: true,
|
|
1211
|
-
category_label_policy: "wrap",
|
|
1212
|
-
show_latest_value: true,
|
|
1213
|
-
bins: 12,
|
|
1214
|
-
max_segments: 8,
|
|
1215
|
-
other_threshold: 0.1,
|
|
1216
|
-
show_percent: true,
|
|
1217
|
-
};
|
|
1218
|
-
chartArgs.chart.fields.facet = { field: "legacy" };
|
|
1219
|
-
chartArgs.chart.fields.tooltip = [{ field: "legacy" }];
|
|
1220
|
-
chartArgs.display.notes = ["presentation chart note"];
|
|
1221
|
-
chartArgs.display.layout = "split";
|
|
1222
|
-
chartArgs.display.source_panel = "sidebar";
|
|
1223
|
-
chartArgs.display.sql_panel_position = "bottom_right";
|
|
1224
|
-
chartArgs.display.table_panel_position = "bottom_left";
|
|
1225
|
-
chartArgs.display.legend = { title: "Legacy" };
|
|
1226
|
-
chartArgs.display.view_mode = "table";
|
|
1227
|
-
chartArgs.display.number_format = "currency";
|
|
1228
|
-
chartArgs.display.labels = { values: "all" };
|
|
1229
|
-
chartArgs.query_binding = { label: "legacy binding" };
|
|
1230
|
-
chartArgs.datasets = [{ id: "legacy dataset input" }];
|
|
1231
|
-
chartArgs.selected_dataset = "legacy dataset input";
|
|
1232
|
-
|
|
1233
|
-
const chartPayload = server.callTool("render_chart", chartArgs);
|
|
1234
|
-
const tablePayload = server.callTool("render_table", {
|
|
1235
|
-
title: "Preview",
|
|
1236
|
-
source: chartArgs.source,
|
|
1237
|
-
notes: ["table note"],
|
|
1238
|
-
columns: [{ key: "segment", label: "Segment", type: "text" }],
|
|
1239
|
-
rows: [{ segment: "Enterprise" }],
|
|
1240
|
-
});
|
|
1241
|
-
const tableSchema =
|
|
1242
|
-
server
|
|
1243
|
-
.toolDefinitions()
|
|
1244
|
-
.find((tool) => tool.name === "render_table")
|
|
1245
|
-
.inputSchema.properties;
|
|
1246
|
-
|
|
1247
|
-
assert.equal("notes" in chartPayload, false);
|
|
1248
|
-
assert.equal("notes" in (chartPayload.display || {}), false);
|
|
1249
|
-
assert.equal("version" in chartPayload.chart, false);
|
|
1250
|
-
assert.equal("comparisonContext" in chartPayload.chart, false);
|
|
1251
|
-
assert.equal("sort" in chartPayload.chart, false);
|
|
1252
|
-
assert.deepEqual(chartPayload.chart.options, undefined);
|
|
1253
|
-
assert.equal("facet" in chartPayload.chart.fields, false);
|
|
1254
|
-
assert.equal("tooltip" in chartPayload.chart.fields, false);
|
|
1255
|
-
assert.equal("layout" in (chartPayload.display || {}), false);
|
|
1256
|
-
assert.equal("source_panel" in (chartPayload.display || {}), false);
|
|
1257
|
-
assert.equal("sql_panel_position" in (chartPayload.display || {}), false);
|
|
1258
|
-
assert.equal("table_panel_position" in (chartPayload.display || {}), false);
|
|
1259
|
-
assert.equal("legend" in (chartPayload.display || {}), false);
|
|
1260
|
-
assert.equal("view_mode" in (chartPayload.display || {}), false);
|
|
1261
|
-
assert.equal("number_format" in (chartPayload.display || {}), false);
|
|
1262
|
-
assert.equal("legend" in chartPayload.chart_spec, false);
|
|
1263
|
-
assert.equal("labels" in chartPayload.chart_spec, false);
|
|
1264
|
-
assert.equal("viewMode" in chartPayload.chart_spec.surface, false);
|
|
1265
|
-
assert.equal("view_mode" in chartPayload, false);
|
|
1266
|
-
assert.equal("labels" in (chartPayload.display || {}), false);
|
|
1267
|
-
assert.equal("datasets" in chartPayload, false);
|
|
1268
|
-
assert.equal("selected_dataset" in chartPayload, false);
|
|
1269
|
-
assert.equal("query_binding" in chartPayload, false);
|
|
1270
|
-
assert.ok("notes" in tableSchema);
|
|
1271
|
-
assert.deepEqual(tablePayload.notes, ["table note"]);
|
|
1272
|
-
});
|
|
1273
|
-
|
|
1274
|
-
test("JavaScript MCP server preserves chart-compatible source SQL for table widgets", () => {
|
|
1275
|
-
const chartArgs = queryTablePayload();
|
|
1276
|
-
const payload = server.callTool("render_table", {
|
|
1277
|
-
title: "ARR rows",
|
|
1278
|
-
source: chartArgs.source,
|
|
1279
|
-
result_table: chartArgs.table,
|
|
1280
|
-
});
|
|
1281
|
-
const tableSchema =
|
|
1282
|
-
server
|
|
1283
|
-
.toolDefinitions()
|
|
1284
|
-
.find((tool) => tool.name === "render_table")
|
|
1285
|
-
.inputSchema.properties;
|
|
1286
|
-
|
|
1287
|
-
assert.equal(payload.widget_type, "table");
|
|
1288
|
-
assert.equal(payload.source.query.sql, "SELECT reporting_date, arr_b FROM gtm.example");
|
|
1289
|
-
assert.equal("query" in payload.source.query, false);
|
|
1290
|
-
assert.equal(payload.source.query.id, "query-123");
|
|
1291
|
-
assert.equal(payload.source_query, undefined);
|
|
1292
|
-
assert.ok(tableSchema.source);
|
|
1293
|
-
assert.ok(tableSchema.source.properties.query.properties.sql);
|
|
1294
|
-
assert.ok(tableSchema.source.properties.query.properties.filters);
|
|
1295
|
-
assert.ok(tableSchema.source.properties.query.properties.description);
|
|
1296
|
-
});
|
|
1297
|
-
|
|
1298
|
-
test("JavaScript MCP server removes source.query.query from table widget payloads", () => {
|
|
1299
|
-
const chartArgs = queryTablePayload();
|
|
1300
|
-
chartArgs.source.query.query = "Loads ARR by reporting date for widget tests.";
|
|
1301
|
-
|
|
1302
|
-
const payload = server.callTool("render_table", {
|
|
1303
|
-
title: "ARR rows",
|
|
1304
|
-
source: chartArgs.source,
|
|
1305
|
-
result_table: chartArgs.table,
|
|
1306
|
-
});
|
|
1307
|
-
|
|
1308
|
-
assert.equal(payload.widget_type, "table");
|
|
1309
|
-
assert.equal(payload.source.query.sql, "SELECT reporting_date, arr_b FROM gtm.example");
|
|
1310
|
-
assert.equal("query" in payload.source.query, false);
|
|
1311
|
-
assert.equal(payload.result_table.rows.length, 2);
|
|
1312
|
-
});
|
|
1313
|
-
|
|
1314
|
-
test("JavaScript MCP server rejects payloads that only use removed source.query.query", () => {
|
|
1315
|
-
const chartArgs = queryTablePayload();
|
|
1316
|
-
delete chartArgs.source.query.sql;
|
|
1317
|
-
chartArgs.source.query.query = "SELECT reporting_date, arr_b FROM gtm.example";
|
|
1318
|
-
|
|
1319
|
-
assert.throws(
|
|
1320
|
-
() => server.callTool("render_table", {
|
|
1321
|
-
title: "ARR rows",
|
|
1322
|
-
source: chartArgs.source,
|
|
1323
|
-
result_table: chartArgs.table,
|
|
1324
|
-
}),
|
|
1325
|
-
/actual SQL query text/,
|
|
1326
|
-
);
|
|
1327
|
-
});
|
|
1328
|
-
|
|
1329
|
-
test("JavaScript MCP server rejects redundant bar chart type aliases", () => {
|
|
1330
|
-
assert.throws(() => server.callTool("render_chart", {
|
|
1331
|
-
title: "Revenue mix",
|
|
1332
|
-
source: sourceQueryForTest(),
|
|
1333
|
-
table: {
|
|
1334
|
-
columns: [
|
|
1335
|
-
{ key: "segment", type: "text" },
|
|
1336
|
-
{ key: "channel", type: "text" },
|
|
1337
|
-
{ key: "revenue", type: "number" },
|
|
1338
|
-
],
|
|
1339
|
-
rows: [
|
|
1340
|
-
{ segment: "Enterprise", channel: "Direct", revenue: 40 },
|
|
1341
|
-
{ segment: "Enterprise", channel: "Partner", revenue: 60 },
|
|
1342
|
-
],
|
|
1343
|
-
},
|
|
1344
|
-
chart: {
|
|
1345
|
-
type: "horizontalStackedBar100",
|
|
1346
|
-
fields: {
|
|
1347
|
-
x: { field: "segment", type: "nominal" },
|
|
1348
|
-
y: { field: "revenue", type: "quantitative" },
|
|
1349
|
-
color: { field: "channel", type: "nominal" },
|
|
1350
|
-
},
|
|
1351
|
-
},
|
|
1352
|
-
}), /chart\.type must be one of/);
|
|
1353
|
-
});
|
|
1354
|
-
|
|
1355
|
-
test("JavaScript MCP server rejects invalid grouped and percent stacked bar configs", () => {
|
|
1356
|
-
const baseArgs = {
|
|
1357
|
-
title: "Revenue mix",
|
|
1358
|
-
source: sourceQueryForTest(),
|
|
1359
|
-
table: {
|
|
1360
|
-
columns: [
|
|
1361
|
-
{ key: "segment", type: "text" },
|
|
1362
|
-
{ key: "channel", type: "text" },
|
|
1363
|
-
{ key: "revenue", type: "number" },
|
|
1364
|
-
],
|
|
1365
|
-
rows: [{ segment: "Enterprise", channel: "Direct", revenue: 0 }],
|
|
1366
|
-
},
|
|
1367
|
-
chart: {
|
|
1368
|
-
type: "bar",
|
|
1369
|
-
options: { grouping: "stacked" },
|
|
1370
|
-
fields: {
|
|
1371
|
-
x: { field: "segment", type: "nominal" },
|
|
1372
|
-
y: { field: "revenue", type: "quantitative" },
|
|
1373
|
-
},
|
|
1374
|
-
},
|
|
1375
|
-
};
|
|
1376
|
-
|
|
1377
|
-
assert.throws(
|
|
1378
|
-
() => server.callTool("render_chart", baseArgs),
|
|
1379
|
-
/requires chart\.fields\.color\.field/,
|
|
1380
|
-
);
|
|
1381
|
-
|
|
1382
|
-
const invalidPercent = structuredClone(baseArgs);
|
|
1383
|
-
invalidPercent.chart.options.grouping = "stacked100";
|
|
1384
|
-
invalidPercent.chart.fields.color = { field: "channel", type: "nominal" };
|
|
1385
|
-
assert.throws(
|
|
1386
|
-
() => server.callTool("render_chart", invalidPercent),
|
|
1387
|
-
/positive denominator/,
|
|
1388
|
-
);
|
|
1389
|
-
});
|
|
1390
|
-
|
|
1391
|
-
test("JavaScript MCP server rejects auto chart setting modes", () => {
|
|
1392
|
-
const args = queryTablePayload();
|
|
1393
|
-
|
|
1394
|
-
args.chart.options = { points: "auto" };
|
|
1395
|
-
assert.throws(
|
|
1396
|
-
() => server.callTool("render_chart", args),
|
|
1397
|
-
/chart\.options\.points must be one of always, never/,
|
|
1398
|
-
);
|
|
1399
|
-
});
|
|
1400
|
-
|
|
1401
|
-
test("JavaScript MCP server warns on dense compact bars and pies", () => {
|
|
1402
|
-
const rows = Array.from({ length: 25 }, (_, index) => ({ category: `C${index}`, value: index + 1 }));
|
|
1403
|
-
const columns = [{ key: "category", type: "text" }, { key: "value", type: "number" }];
|
|
1404
|
-
const barPayload = server.callTool("render_chart", {
|
|
1405
|
-
title: "Dense categories",
|
|
1406
|
-
source: sourceQueryForTest(),
|
|
1407
|
-
table: { columns, rows },
|
|
1408
|
-
chart: {
|
|
1409
|
-
type: "bar",
|
|
1410
|
-
fields: { x: { field: "category" }, y: { field: "value" } },
|
|
1411
|
-
},
|
|
1412
|
-
});
|
|
1413
|
-
const piePayload = server.callTool("render_chart", {
|
|
1414
|
-
title: "Dense slices",
|
|
1415
|
-
source: sourceQueryForTest(),
|
|
1416
|
-
table: { columns, rows: rows.slice(0, 9) },
|
|
1417
|
-
chart: {
|
|
1418
|
-
type: "pie",
|
|
1419
|
-
fields: { x: { field: "category" }, y: { field: "value" } },
|
|
1420
|
-
},
|
|
1421
|
-
});
|
|
1422
|
-
|
|
1423
|
-
assert.match(barPayload.quality_warnings.join(" "), /Compact bar chart has 25 categories/);
|
|
1424
|
-
assert.match(piePayload.quality_warnings.join(" "), /Compact pie chart has 9 slices/);
|
|
1425
|
-
});
|
|
1426
|
-
|
|
1427
|
-
test("JavaScript MCP server returns typed resource errors", async () => {
|
|
1428
|
-
const response = await server.handleRpc({
|
|
1429
|
-
jsonrpc: "2.0",
|
|
1430
|
-
id: 1,
|
|
1431
|
-
method: "resources/read",
|
|
1432
|
-
params: { uri: "ui://widget/missing.html" },
|
|
1433
|
-
});
|
|
1434
|
-
|
|
1435
|
-
assert.equal(response.error.code, -32602);
|
|
1436
|
-
assert.match(response.error.message, /unknown Data Analytics widget resource/);
|
|
1437
|
-
});
|
|
1438
|
-
|
|
1439
|
-
test("JavaScript MCP server rejects legacy widget resource aliases", async () => {
|
|
1440
|
-
for (const uri of [
|
|
1441
|
-
server.CHART_WIDGET_URI.replace(".html", "-old.html"),
|
|
1442
|
-
server.TABLE_WIDGET_URI.replace(".html", "-old.html"),
|
|
1443
|
-
server.ARTIFACT_WIDGET_URI.replace(".html", "-old.html"),
|
|
1444
|
-
]) {
|
|
1445
|
-
const response = await server.handleRpc({
|
|
1446
|
-
jsonrpc: "2.0",
|
|
1447
|
-
id: 1,
|
|
1448
|
-
method: "resources/read",
|
|
1449
|
-
params: { uri },
|
|
1450
|
-
});
|
|
1451
|
-
|
|
1452
|
-
assert.equal(response.error.code, -32602);
|
|
1453
|
-
assert.match(response.error.message, /unknown Data Analytics widget resource/);
|
|
1454
|
-
}
|
|
1455
|
-
});
|
|
1456
|
-
|
|
1457
|
-
test("artifact share menu labels Sites publishing clearly", () => {
|
|
1458
|
-
const app = readFileSync(new URL("../src/analytics-app/App.tsx", import.meta.url), "utf8");
|
|
1459
|
-
assert.match(app, /site: "Publish to Sites"/);
|
|
1460
|
-
assert.doesNotMatch(app, /Publish hosted link/);
|
|
1461
|
-
});
|
|
1462
|
-
|
|
1463
|
-
test("local artifact fallback uses canonical chart encodings", () => {
|
|
1464
|
-
const widget = readFileSync(new URL("../src/datascience-artifact-widget.jsx", import.meta.url), "utf8");
|
|
1465
|
-
const fallback = widget.split("const fallbackPayload =", 2)[1].split("const hostedEmptyPayload", 1)[0];
|
|
1466
|
-
assert.match(fallback, /sourceId: "snapshot"/);
|
|
1467
|
-
assert.match(fallback, /encodings: \{/);
|
|
1468
|
-
assert.doesNotMatch(fallback, /xField:/);
|
|
1469
|
-
assert.doesNotMatch(fallback, /^\s+series:/m);
|
|
1470
|
-
});
|
|
1471
|
-
|
|
1472
|
-
test("artifact charts wire clickable legend state into the renderer", () => {
|
|
1473
|
-
const app = readFileSync(new URL("../src/analytics-app/App.tsx", import.meta.url), "utf8");
|
|
1474
|
-
assert.match(app, /const \[visibleSeries, setVisibleSeries\] = useState\(\)/);
|
|
1475
|
-
assert.match(app, /onVisibleSeriesChange=\{setVisibleSeries\}/);
|
|
1476
|
-
assert.match(app, /visibleSeries=\{visibleSeries\}/);
|
|
1477
|
-
});
|
|
1478
|
-
|
|
1479
|
-
test("JavaScript MCP server rejects unsafe widget fields", () => {
|
|
1480
|
-
const args = queryTablePayload();
|
|
1481
|
-
args.table.columns.push({ key: "customer_email", label: "Email" });
|
|
1482
|
-
args.table.rows[0].customer_email = "person@example.com";
|
|
1483
|
-
|
|
1484
|
-
assert.throws(
|
|
1485
|
-
() => server.callTool("render_chart", args),
|
|
1486
|
-
/looks unsafe/,
|
|
1487
|
-
);
|
|
1488
|
-
});
|
|
1489
|
-
|
|
1490
|
-
test("JavaScript MCP server allows reviewed customer dimensions in widgets", () => {
|
|
1491
|
-
const args = queryTablePayload();
|
|
1492
|
-
args.table.contains_sensitive_data = true;
|
|
1493
|
-
args.table.columns.push({ key: "customer_name", label: "Customer" });
|
|
1494
|
-
args.table.rows[0].customer_name = "Acme Corp";
|
|
1495
|
-
|
|
1496
|
-
const payload = server.callTool("render_chart", args);
|
|
1497
|
-
|
|
1498
|
-
assert.equal(payload.widget_type, "chart");
|
|
1499
|
-
assert.equal(payload.table.rows[0].customer_name, "Acme Corp");
|
|
1500
|
-
});
|