create-yss-spec 1.1.1 → 1.1.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/template/.agents/skills/high-fidelity-html-prototype/SKILL.md +102 -0
- package/template/.codex/skills/data-analytics/.app.json +84 -0
- package/template/.codex/skills/data-analytics/.codex-plugin/plugin.json +66 -0
- package/template/.codex/skills/data-analytics/.mcp.json +30 -0
- package/template/.codex/skills/data-analytics/AGENTS.md +20 -0
- package/template/.codex/skills/data-analytics/DEPENDENCIES.MD +27 -0
- package/template/.codex/skills/data-analytics/README.md +60 -0
- package/template/.codex/skills/data-analytics/assets/datascience-artifact-widget.html +16 -0
- package/template/.codex/skills/data-analytics/assets/datascience-artifact-widget.html.gz.b64.part001 +1 -0
- package/template/.codex/skills/data-analytics/assets/datascience-artifact-widget.html.gz.b64.part002 +1 -0
- package/template/.codex/skills/data-analytics/assets/datascience-artifact-widget.html.gz.b64.part003 +1 -0
- package/template/.codex/skills/data-analytics/assets/datascience-artifact-widget.html.gz.b64.part004 +1 -0
- package/template/.codex/skills/data-analytics/assets/datascience-artifact-widget.html.gz.b64.part005 +1 -0
- package/template/.codex/skills/data-analytics/assets/datascience-artifact-widget.html.gz.b64.part006 +1 -0
- package/template/.codex/skills/data-analytics/assets/datascience-chart-widget.html +16 -0
- package/template/.codex/skills/data-analytics/assets/datascience-chart-widget.html.gz.b64.part001 +1 -0
- package/template/.codex/skills/data-analytics/assets/datascience-chart-widget.html.gz.b64.part002 +1 -0
- package/template/.codex/skills/data-analytics/assets/datascience-chart-widget.html.gz.b64.part003 +1 -0
- package/template/.codex/skills/data-analytics/assets/datascience-chart-widget.html.gz.b64.part004 +1 -0
- package/template/.codex/skills/data-analytics/assets/datascience-chart-widget.html.gz.b64.part005 +1 -0
- package/template/.codex/skills/data-analytics/assets/datascience-chart-widget.html.gz.b64.part006 +1 -0
- package/template/.codex/skills/data-analytics/assets/datascience-chart-widget.html.gz.b64.part007 +1 -0
- package/template/.codex/skills/data-analytics/assets/datascience-small.svg +5 -0
- package/template/.codex/skills/data-analytics/assets/datascience-table-widget.html +16 -0
- package/template/.codex/skills/data-analytics/assets/datascience-table-widget.html.gz.b64.part001 +1 -0
- package/template/.codex/skills/data-analytics/assets/datascience-table-widget.html.gz.b64.part002 +1 -0
- package/template/.codex/skills/data-analytics/assets/datascience-table-widget.html.gz.b64.part003 +1 -0
- package/template/.codex/skills/data-analytics/assets/datascience-table-widget.html.gz.b64.part004 +1 -0
- package/template/.codex/skills/data-analytics/assets/datascience.png +0 -0
- package/template/.codex/skills/data-analytics/assets/datascience.svg +10 -0
- package/template/.codex/skills/data-analytics/mcp/server.cjs +2963 -0
- package/template/.codex/skills/data-analytics/package-lock.json +3048 -0
- package/template/.codex/skills/data-analytics/package.json +43 -0
- package/template/.codex/skills/data-analytics/scripts/normalize-widget-assets.mjs +75 -0
- package/template/.codex/skills/data-analytics/skills/analyze-data-quality/SKILL.md +160 -0
- package/template/.codex/skills/data-analytics/skills/analyze-data-quality/agents/openai.yaml +6 -0
- package/template/.codex/skills/data-analytics/skills/build-dashboard/SKILL.md +148 -0
- package/template/.codex/skills/data-analytics/skills/build-dashboard/agents/openai.yaml +6 -0
- package/template/.codex/skills/data-analytics/skills/build-dashboard/specifications/bi-platform-dashboard.md +18 -0
- package/template/.codex/skills/data-analytics/skills/build-dashboard/specifications/html-dashboard.md +24 -0
- package/template/.codex/skills/data-analytics/skills/build-dashboard/specifications/mcp-artifact-dashboard.md +71 -0
- package/template/.codex/skills/data-analytics/skills/build-dashboard/specifications/streamlit-dashboard.md +85 -0
- package/template/.codex/skills/data-analytics/skills/build-report/SKILL.md +207 -0
- package/template/.codex/skills/data-analytics/skills/build-report/agents/openai.yaml +6 -0
- package/template/.codex/skills/data-analytics/skills/build-report/assets/executive-report-shell.html +70 -0
- package/template/.codex/skills/data-analytics/skills/build-report/assets/technical-report-shell.html +66 -0
- package/template/.codex/skills/data-analytics/skills/build-report/report-to-google-doc/SKILL.md +118 -0
- package/template/.codex/skills/data-analytics/skills/build-report/report-to-google-doc/agents/openai.yaml +6 -0
- package/template/.codex/skills/data-analytics/skills/build-report/report-to-google-doc/scripts/report_to_google_doc/__init__.py +1 -0
- package/template/.codex/skills/data-analytics/skills/build-report/report-to-google-doc/scripts/report_to_google_doc/cli.py +57 -0
- package/template/.codex/skills/data-analytics/skills/build-report/report-to-google-doc/scripts/report_to_google_doc/constants.py +61 -0
- package/template/.codex/skills/data-analytics/skills/build-report/report-to-google-doc/scripts/report_to_google_doc/docx_writer.py +362 -0
- package/template/.codex/skills/data-analytics/skills/build-report/report-to-google-doc/scripts/report_to_google_doc/html_parser.py +829 -0
- package/template/.codex/skills/data-analytics/skills/build-report/report-to-google-doc/scripts/report_to_google_doc/model.py +30 -0
- package/template/.codex/skills/data-analytics/skills/build-report/report-to-google-doc/scripts/report_to_google_doc/plan.py +129 -0
- package/template/.codex/skills/data-analytics/skills/build-report/report-to-google-doc/scripts/report_to_google_doc/quality.py +374 -0
- package/template/.codex/skills/data-analytics/skills/build-report/report-to-google-doc/scripts/report_to_google_doc/rendering.py +613 -0
- package/template/.codex/skills/data-analytics/skills/build-report/report-to-google-doc/scripts/report_to_google_doc/table_utils.py +54 -0
- package/template/.codex/skills/data-analytics/skills/build-report/report-to-google-doc/scripts/report_to_google_doc/utils.py +16 -0
- package/template/.codex/skills/data-analytics/skills/build-report/report-to-google-doc/scripts/report_to_google_doc_plan.py +9 -0
- package/template/.codex/skills/data-analytics/skills/build-report/report-to-google-slides/SKILL.md +78 -0
- package/template/.codex/skills/data-analytics/skills/build-report/report-to-google-slides/agents/openai.yaml +6 -0
- package/template/.codex/skills/data-analytics/skills/build-report/report-to-google-slides/scripts/report_to_google_slides.py +2379 -0
- package/template/.codex/skills/data-analytics/skills/build-report/report-to-pdf/SKILL.md +88 -0
- package/template/.codex/skills/data-analytics/skills/build-report/report-to-pdf/agents/openai.yaml +6 -0
- package/template/.codex/skills/data-analytics/skills/build-report/specifications/executive-report.md +97 -0
- package/template/.codex/skills/data-analytics/skills/build-report/specifications/mcp-app-report.md +59 -0
- package/template/.codex/skills/data-analytics/skills/build-report/specifications/technical-report.md +75 -0
- package/template/.codex/skills/data-analytics/skills/design-kpis/SKILL.md +103 -0
- package/template/.codex/skills/data-analytics/skills/design-kpis/agents/openai.yaml +6 -0
- package/template/.codex/skills/data-analytics/skills/gather-business-context/SKILL.md +68 -0
- package/template/.codex/skills/data-analytics/skills/gather-business-context/agents/openai.yaml +6 -0
- package/template/.codex/skills/data-analytics/skills/index/SKILL.md +251 -0
- package/template/.codex/skills/data-analytics/skills/index/agents/openai.yaml +6 -0
- package/template/.codex/skills/data-analytics/skills/jupyter-notebooks/SKILL.md +131 -0
- package/template/.codex/skills/data-analytics/skills/jupyter-notebooks/agents/openai.yaml +6 -0
- package/template/.codex/skills/data-analytics/skills/kpi-reporting/SKILL.md +141 -0
- package/template/.codex/skills/data-analytics/skills/kpi-reporting/agents/openai.yaml +6 -0
- package/template/.codex/skills/data-analytics/skills/kpi-reporting/references/report-templates.md +32 -0
- package/template/.codex/skills/data-analytics/skills/market-sizing/SKILL.md +106 -0
- package/template/.codex/skills/data-analytics/skills/market-sizing/agents/openai.yaml +6 -0
- package/template/.codex/skills/data-analytics/skills/metric-diagnostics/SKILL.md +130 -0
- package/template/.codex/skills/data-analytics/skills/metric-diagnostics/agents/openai.yaml +6 -0
- package/template/.codex/skills/data-analytics/skills/product-business-analysis/SKILL.md +141 -0
- package/template/.codex/skills/data-analytics/skills/product-business-analysis/agents/openai.yaml +6 -0
- package/template/.codex/skills/data-analytics/skills/spreadsheets/SKILL.md +178 -0
- package/template/.codex/skills/data-analytics/skills/spreadsheets/agents/openai.yaml +9 -0
- package/template/.codex/skills/data-analytics/skills/spreadsheets/assets/file-spreadsheet.png +0 -0
- package/template/.codex/skills/data-analytics/skills/spreadsheets/charts.md +31 -0
- package/template/.codex/skills/data-analytics/skills/spreadsheets/references/artifact_tool_api.md +466 -0
- package/template/.codex/skills/data-analytics/skills/spreadsheets/style_guidelines.md +99 -0
- package/template/.codex/skills/data-analytics/skills/user-context/SKILL.md +195 -0
- package/template/.codex/skills/data-analytics/skills/user-context/agents/openai.yaml +6 -0
- package/template/.codex/skills/data-analytics/skills/user-context/plugin-author-config/automation-config.md +26 -0
- package/template/.codex/skills/data-analytics/skills/user-context/plugin-author-config/source-category-config.json +51 -0
- package/template/.codex/skills/data-analytics/skills/user-context/plugin-author-config/user-context-config.md +66 -0
- package/template/.codex/skills/data-analytics/skills/user-context/references/automation.md +69 -0
- package/template/.codex/skills/data-analytics/skills/user-context/references/onboarding-examples.md +204 -0
- package/template/.codex/skills/data-analytics/skills/user-context/references/onboarding-state-template.json +87 -0
- package/template/.codex/skills/data-analytics/skills/user-context/references/onboarding.md +497 -0
- package/template/.codex/skills/data-analytics/skills/user-context/references/semantic-layer/connector-playbook.md +74 -0
- package/template/.codex/skills/data-analytics/skills/user-context/references/semantic-layer/setup.md +65 -0
- package/template/.codex/skills/data-analytics/skills/user-context/references/semantic-layer/skill-template.md +160 -0
- package/template/.codex/skills/data-analytics/skills/user-context/references/semantic-layer/source-intake.md +75 -0
- package/template/.codex/skills/data-analytics/skills/user-context/references/semantic-layer/weekly-polling-automation.md +96 -0
- package/template/.codex/skills/data-analytics/skills/user-context/references/source-category-runtime.md +263 -0
- package/template/.codex/skills/data-analytics/skills/user-context/scripts/data_analytics_preflight.py +1460 -0
- package/template/.codex/skills/data-analytics/skills/user-context/scripts/init_user_context_state.py +128 -0
- package/template/.codex/skills/data-analytics/skills/user-context/scripts/reset_user_context_state.py +101 -0
- package/template/.codex/skills/data-analytics/skills/user-context/scripts/validate_user_context_preflight.py +499 -0
- package/template/.codex/skills/data-analytics/skills/user-context/tests/test_state_helpers.py +978 -0
- package/template/.codex/skills/data-analytics/skills/validate-data/SKILL.md +190 -0
- package/template/.codex/skills/data-analytics/skills/validate-data/agents/openai.yaml +6 -0
- package/template/.codex/skills/data-analytics/skills/visualize-data/SKILL.md +157 -0
- package/template/.codex/skills/data-analytics/skills/visualize-data/agents/openai.yaml +6 -0
- package/template/.codex/skills/data-analytics/skills/visualize-data/references/seaborn-templates.md +774 -0
- package/template/.codex/skills/data-analytics/src/DESIGN.md +222 -0
- package/template/.codex/skills/data-analytics/src/analytics-app/App.tsx +3666 -0
- package/template/.codex/skills/data-analytics/src/analytics-app/analytics-layout.test.mjs +136 -0
- package/template/.codex/skills/data-analytics/src/analytics-app/charting/ChartFrame.tsx +54 -0
- package/template/.codex/skills/data-analytics/src/analytics-app/charting/ChartLegend.tsx +96 -0
- package/template/.codex/skills/data-analytics/src/analytics-app/charting/ChartRenderer.tsx +1647 -0
- package/template/.codex/skills/data-analytics/src/analytics-app/charting/ChartTooltip.tsx +245 -0
- package/template/.codex/skills/data-analytics/src/analytics-app/charting/chart-app-helpers.tsx +462 -0
- package/template/.codex/skills/data-analytics/src/analytics-app/charting/chart-capabilities.ts +107 -0
- package/template/.codex/skills/data-analytics/src/analytics-app/charting/chart-compatibility.ts +164 -0
- package/template/.codex/skills/data-analytics/src/analytics-app/charting/chart-contract.ts +192 -0
- package/template/.codex/skills/data-analytics/src/analytics-app/charting/chart-theme.ts +203 -0
- package/template/.codex/skills/data-analytics/src/analytics-app/charting/chart-tokens.css +619 -0
- package/template/.codex/skills/data-analytics/src/analytics-app/charting/chart-transforms.ts +402 -0
- 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 +370 -0
- package/template/.codex/skills/data-analytics/src/analytics-app/layout/AnalyticsLayoutCanvas.tsx +536 -0
- package/template/.codex/skills/data-analytics/src/analytics-app/layout/RichMarkdown.tsx +681 -0
- package/template/.codex/skills/data-analytics/src/analytics-app/layout/analyticsLayoutCore.ts +164 -0
- package/template/.codex/skills/data-analytics/src/analytics-app/main.tsx +21 -0
- package/template/.codex/skills/data-analytics/src/analytics-app/scripts/chart_contract.py +472 -0
- package/template/.codex/skills/data-analytics/src/analytics-app/scripts/check_analytics_app_runtime_links.py +75 -0
- package/template/.codex/skills/data-analytics/src/analytics-app/scripts/design_contract.py +142 -0
- package/template/.codex/skills/data-analytics/src/analytics-app/scripts/package_utils.py +142 -0
- package/template/.codex/skills/data-analytics/src/analytics-app/scripts/tests/test_package_utils.py +377 -0
- package/template/.codex/skills/data-analytics/src/analytics-app/styles.css +3468 -0
- package/template/.codex/skills/data-analytics/src/analytics-app/tables/DataTable.d.ts +44 -0
- package/template/.codex/skills/data-analytics/src/analytics-app/tables/DataTable.jsx +639 -0
- package/template/.codex/skills/data-analytics/src/analytics-app/tables/data-table.css +327 -0
- package/template/.codex/skills/data-analytics/src/analytics-app/tokens.css +225 -0
- package/template/.codex/skills/data-analytics/src/analytics-app/types.ts +204 -0
- package/template/.codex/skills/data-analytics/src/analytics-app-core.md +85 -0
- package/template/.codex/skills/data-analytics/src/codex-style-contract.md +30 -0
- package/template/.codex/skills/data-analytics/src/datascience-artifact-widget.html +12 -0
- package/template/.codex/skills/data-analytics/src/datascience-artifact-widget.jsx +1008 -0
- package/template/.codex/skills/data-analytics/src/datascience-chart-widget.css +2117 -0
- package/template/.codex/skills/data-analytics/src/datascience-chart-widget.html +122 -0
- package/template/.codex/skills/data-analytics/src/datascience-chart-widget.js +3588 -0
- package/template/.codex/skills/data-analytics/src/datascience-table-widget.css +483 -0
- package/template/.codex/skills/data-analytics/src/datascience-table-widget.html +32 -0
- package/template/.codex/skills/data-analytics/src/datascience-table-widget.js +333 -0
- package/template/.codex/skills/data-analytics/src/mcp-host.js +153 -0
- package/template/.codex/skills/data-analytics/src/recharts-config.js +315 -0
- package/template/.codex/skills/data-analytics/src/recharts-renderer.jsx +294 -0
- package/template/.codex/skills/data-analytics/src/sql-source-view.css +33 -0
- package/template/.codex/skills/data-analytics/src/sql-source-view.js +157 -0
- package/template/.codex/skills/data-analytics/src/styles/codex-theme.css +121 -0
- package/template/.codex/skills/data-analytics/src/table-renderer.jsx +28 -0
- package/template/.codex/skills/data-analytics/tests/chart-transforms.test.mjs +87 -0
- package/template/.codex/skills/data-analytics/tests/funnel-smoke.html +105 -0
- package/template/.codex/skills/data-analytics/tests/inline-widget-compare.html +233 -0
- package/template/.codex/skills/data-analytics/tests/mcp-server.test.mjs +1500 -0
- package/template/.codex/skills/data-analytics/tests/native-style-contract.test.mjs +219 -0
- package/template/.codex/skills/data-analytics/tests/recharts-config.test.mjs +144 -0
- package/template/.codex/skills/data-analytics/tests/recharts-renderer.test.mjs +11 -0
- package/template/.codex/skills/data-analytics/tests/rich-markdown.test.mjs +54 -0
- package/template/.codex/skills/data-analytics/tests/widget-render-harness.html +321 -0
- package/template/.codex/skills/data-analytics/tsconfig.json +19 -0
- package/template/.codex/skills/data-analytics/vite.config.ts +46 -0
- package/template/.codex/skills/high-fidelity-html-prototype/SKILL.md +102 -0
- package/template/.codex/skills/high-fidelity-html-prototype/agents/openai.yaml +5 -0
- package/template/.codex/skills/product-design/.app.json +7 -0
- package/template/.codex/skills/product-design/.codex-plugin/plugin.json +50 -0
- package/template/.codex/skills/product-design/README.md +50 -0
- package/template/.codex/skills/product-design/agents/openai.yaml +4 -0
- package/template/.codex/skills/product-design/assets/composerIcon.svg +9 -0
- package/template/.codex/skills/product-design/assets/logo.png +0 -0
- package/template/.codex/skills/product-design/package.json +11 -0
- package/template/.codex/skills/product-design/references/browser-order.md +9 -0
- package/template/.codex/skills/product-design/references/communication-protocol.md +45 -0
- package/template/.codex/skills/product-design/references/critical-overrides.md +58 -0
- package/template/.codex/skills/product-design/references/local-prototype-preflight.md +16 -0
- package/template/.codex/skills/product-design/scripts/bootstrap-prototype.mjs +147 -0
- package/template/.codex/skills/product-design/skills/audit/SKILL.md +159 -0
- package/template/.codex/skills/product-design/skills/audit/agents/openai.yaml +6 -0
- package/template/.codex/skills/product-design/skills/audit/references/design-audit-framework.md +73 -0
- package/template/.codex/skills/product-design/skills/design-qa/SKILL.md +133 -0
- package/template/.codex/skills/product-design/skills/design-qa/agents/openai.yaml +6 -0
- package/template/.codex/skills/product-design/skills/design-qa/references/qa-rubric.md +61 -0
- package/template/.codex/skills/product-design/skills/get-context/SKILL.md +81 -0
- package/template/.codex/skills/product-design/skills/get-context/agents/openai.yaml +6 -0
- package/template/.codex/skills/product-design/skills/ideate/SKILL.md +179 -0
- package/template/.codex/skills/product-design/skills/ideate/agents/openai.yaml +6 -0
- package/template/.codex/skills/product-design/skills/image-to-code/SKILL.md +105 -0
- package/template/.codex/skills/product-design/skills/image-to-code/agents/openai.yaml +6 -0
- package/template/.codex/skills/product-design/skills/index/SKILL.md +126 -0
- package/template/.codex/skills/product-design/skills/index/agents/openai.yaml +6 -0
- package/template/.codex/skills/product-design/skills/prototype/SKILL.md +128 -0
- package/template/.codex/skills/product-design/skills/prototype/agents/openai.yaml +6 -0
- package/template/.codex/skills/product-design/skills/prototype/references/existing-codebase-edits.md +18 -0
- package/template/.codex/skills/product-design/skills/research/SKILL.md +92 -0
- package/template/.codex/skills/product-design/skills/research/agents/openai.yaml +6 -0
- package/template/.codex/skills/product-design/skills/share/SKILL.md +41 -0
- package/template/.codex/skills/product-design/skills/share/agents/openai.yaml +6 -0
- package/template/.codex/skills/product-design/skills/url-to-code/SKILL.md +124 -0
- package/template/.codex/skills/product-design/skills/url-to-code/agents/openai.yaml +6 -0
- package/template/.codex/skills/product-design/skills/user-context/SKILL.md +147 -0
- package/template/.codex/skills/product-design/skills/user-context/agents/openai.yaml +6 -0
- package/template/.codex/skills/product-design/skills/user-context/plugin-author-config/user-context-template.md +63 -0
- package/template/.codex/skills/product-design/skills/user-context/references/onboarding.md +66 -0
- package/template/.codex/skills/product-design/skills/user-context/scripts/init_user_context.py +86 -0
- package/template/.codex/skills/product-design/skills/user-context/scripts/user_context_preflight.py +204 -0
- package/template/.codex/skills/product-design/templates/prototype/AGENTS.md +7 -0
- package/template/.codex/skills/product-design/templates/prototype/index.html +12 -0
- package/template/.codex/skills/product-design/templates/prototype/package.json +18 -0
- package/template/.codex/skills/product-design/templates/prototype/src/App.jsx +5 -0
- package/template/.codex/skills/product-design/templates/prototype/src/main.jsx +10 -0
- package/template/.codex/skills/product-design/templates/prototype/src/styles.css +13 -0
- package/template/.codex/skills/product-design/templates/prototype/vite.config.mjs +14 -0
- package/template/.codex/skills/product-design-prototype/SKILL.md +2 -4
- package/template/.codex/skills/prototype-review/SKILL.md +7 -7
- package/template/.codex/skills/yss-design-system/SKILL.md +4 -2
- package/template/.codex/skills/yss-product-lifecycle/SKILL.md +10 -7
- package/template/.codex/skills/yss-product-lifecycle/references/artifact-checklist.md +1 -0
- package/template/.codex/skills/yss-product-lifecycle/references/stage-routing.md +1 -1
- package/template/.codex/skills/yss-ui/SKILL.md +4 -10
- package/template/.codex/skills/yss-ui/references/quick-recipes.md +1 -1
- package/template/AGENTS.md +9 -3
- package/template/CONTEXT.md +1 -0
- package/template/docs/adr/adr-product-design-prototype-entrypoint.md +54 -0
- package/template/docs/api/templates/openapi-draft-review-checklist.md +5 -1
- package/template/docs/design/README.md +13 -6
- package/template/docs/design/prototypes/.gitkeep +1 -0
- package/template/docs/design/templates/interaction-spec-template.md +8 -3
- package/template/docs/design/templates/product-overview-design-template.md +34 -5
- package/template/docs/design/templates/prototype-confirmation-template.md +57 -0
- package/template/docs/design/templates/prototype-review-checklist.md +7 -3
- package/template/docs/design/templates/state-matrix-template.md +1 -1
- package/template/docs/process/harness-executive-blueprint.md +6 -1
- package/template/docs/process/harness-process-tailoring.md +3 -3
- package/template/docs/process/harness-work-unit-map.md +1 -1
- package/template/docs/process/lifecycle-artifact-map.md +7 -5
- package/template/docs/process/product-prototype-workflow.md +82 -0
- package/template/docs/templates/requirement-freeze-template.md +4 -3
- package/template/docs/user-guide/product-lifecycle-workflow.md +2 -2
- package/template/docs/user-guide/product-rd-lifecycle-best-practices.md +54 -24
- package/template/.codex/skills/component-story-prototype/SKILL.md +0 -55
- package/template/.codex/skills/component-story-prototype/agents/openai.yaml +0 -4
- package/template/.codex/skills/mock-api-prototype/SKILL.md +0 -48
- package/template/.codex/skills/mock-api-prototype/agents/openai.yaml +0 -4
|
@@ -0,0 +1,613 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import base64
|
|
4
|
+
import re
|
|
5
|
+
import shutil
|
|
6
|
+
import subprocess
|
|
7
|
+
from io import BytesIO
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from .constants import DEFAULT_RENDER_WORKERS, DOC_CONTENT_WIDTH_PT
|
|
12
|
+
from .html_parser import inline_svg_report_styles
|
|
13
|
+
from .quality import expected_chart_colors, inspect_image_object
|
|
14
|
+
from .utils import collapse_ws
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def svg_float(value: Any, default: float = 0.0) -> float:
|
|
18
|
+
if value is None:
|
|
19
|
+
return default
|
|
20
|
+
match = re.search(r"-?[0-9]+(?:\.[0-9]+)?", str(value))
|
|
21
|
+
return float(match.group(0)) if match else default
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def parse_hex_color(
|
|
25
|
+
value: Any, default: tuple[int, int, int] = (17, 24, 39)
|
|
26
|
+
) -> tuple[int, int, int] | None:
|
|
27
|
+
if value is None:
|
|
28
|
+
return default
|
|
29
|
+
color = str(value).strip()
|
|
30
|
+
if color in {"none", "transparent"}:
|
|
31
|
+
return None
|
|
32
|
+
if color.startswith("#"):
|
|
33
|
+
raw = color[1:]
|
|
34
|
+
if len(raw) == 3:
|
|
35
|
+
raw = "".join(ch * 2 for ch in raw)
|
|
36
|
+
if len(raw) == 6:
|
|
37
|
+
return (int(raw[0:2], 16), int(raw[2:4], 16), int(raw[4:6], 16))
|
|
38
|
+
return default
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def color_with_opacity(color: tuple[int, int, int], opacity: Any) -> tuple[int, int, int]:
|
|
42
|
+
alpha = svg_float(opacity, 1.0)
|
|
43
|
+
alpha = max(0.0, min(1.0, alpha))
|
|
44
|
+
return (
|
|
45
|
+
round(255 * (1 - alpha) + color[0] * alpha),
|
|
46
|
+
round(255 * (1 - alpha) + color[1] * alpha),
|
|
47
|
+
round(255 * (1 - alpha) + color[2] * alpha),
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def svg_path_points(path_data: str) -> list[tuple[float, float]]:
|
|
52
|
+
points: list[tuple[float, float]] = []
|
|
53
|
+
for match in re.finditer(
|
|
54
|
+
r"[ML]\s*(-?[0-9]+(?:\.[0-9]+)?)\s*,?\s*(-?[0-9]+(?:\.[0-9]+)?)",
|
|
55
|
+
path_data,
|
|
56
|
+
flags=re.IGNORECASE,
|
|
57
|
+
):
|
|
58
|
+
points.append((float(match.group(1)), float(match.group(2))))
|
|
59
|
+
return points
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def load_svg_font(size: int, bold: bool = False) -> Any:
|
|
63
|
+
try:
|
|
64
|
+
from PIL import ImageFont
|
|
65
|
+
except ImportError: # pragma: no cover - dependency check path
|
|
66
|
+
return None
|
|
67
|
+
|
|
68
|
+
paths = []
|
|
69
|
+
if bold:
|
|
70
|
+
paths.extend(
|
|
71
|
+
[
|
|
72
|
+
"/System/Library/Fonts/Supplemental/Arial Bold.ttf",
|
|
73
|
+
"/System/Library/Fonts/Supplemental/Arial Bold Italic.ttf",
|
|
74
|
+
"/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf",
|
|
75
|
+
]
|
|
76
|
+
)
|
|
77
|
+
paths.extend(
|
|
78
|
+
[
|
|
79
|
+
"/System/Library/Fonts/Supplemental/Arial.ttf",
|
|
80
|
+
"/System/Library/Fonts/SFNS.ttf",
|
|
81
|
+
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
|
|
82
|
+
]
|
|
83
|
+
)
|
|
84
|
+
for path in paths:
|
|
85
|
+
if Path(path).exists():
|
|
86
|
+
return ImageFont.truetype(path, size)
|
|
87
|
+
return ImageFont.load_default()
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def render_simple_svg_with_pillow(
|
|
91
|
+
svg_markup: str, path: Path, width_px: int, height_px: int
|
|
92
|
+
) -> None:
|
|
93
|
+
"""Render the SVG subset produced by the report builders.
|
|
94
|
+
|
|
95
|
+
This intentionally covers the report-chart primitives we commonly emit
|
|
96
|
+
(`rect`, `line`, and `text`) so conversions do not depend on visible browser
|
|
97
|
+
automation or system Cairo libraries.
|
|
98
|
+
"""
|
|
99
|
+
|
|
100
|
+
try:
|
|
101
|
+
from bs4 import BeautifulSoup
|
|
102
|
+
from PIL import Image, ImageDraw
|
|
103
|
+
except ImportError as exc: # pragma: no cover - dependency check path
|
|
104
|
+
raise RuntimeError("Pillow SVG fallback requires beautifulsoup4 and pillow") from exc
|
|
105
|
+
|
|
106
|
+
scale = 2
|
|
107
|
+
soup = BeautifulSoup(inline_svg_report_styles(svg_markup), "xml")
|
|
108
|
+
svg = soup.find("svg") or soup
|
|
109
|
+
image = Image.new("RGB", (width_px * scale, height_px * scale), (255, 255, 255))
|
|
110
|
+
draw = ImageDraw.Draw(image)
|
|
111
|
+
|
|
112
|
+
def scaled_points_from_path(path_data: str) -> list[tuple[float, float]]:
|
|
113
|
+
points: list[tuple[float, float]] = []
|
|
114
|
+
for match in re.finditer(
|
|
115
|
+
r"[ML]\s*(-?[0-9]+(?:\.[0-9]+)?)\s+(-?[0-9]+(?:\.[0-9]+)?)", path_data
|
|
116
|
+
):
|
|
117
|
+
points.append((float(match.group(1)) * scale, float(match.group(2)) * scale))
|
|
118
|
+
return points
|
|
119
|
+
|
|
120
|
+
for el in svg.find_all(True):
|
|
121
|
+
if el.name == "rect":
|
|
122
|
+
fill = parse_hex_color(el.get("fill"), (255, 255, 255))
|
|
123
|
+
if fill is None:
|
|
124
|
+
continue
|
|
125
|
+
fill = color_with_opacity(fill, el.get("opacity"))
|
|
126
|
+
x = svg_float(el.get("x")) * scale
|
|
127
|
+
y = svg_float(el.get("y")) * scale
|
|
128
|
+
w = svg_float(el.get("width"), width_px) * scale
|
|
129
|
+
h = svg_float(el.get("height"), height_px) * scale
|
|
130
|
+
radius = max(svg_float(el.get("rx")), svg_float(el.get("ry"))) * scale
|
|
131
|
+
box = [x, y, x + w, y + h]
|
|
132
|
+
if radius:
|
|
133
|
+
draw.rounded_rectangle(box, radius=radius, fill=fill)
|
|
134
|
+
else:
|
|
135
|
+
draw.rectangle(box, fill=fill)
|
|
136
|
+
elif el.name == "path":
|
|
137
|
+
points = [(x * scale, y * scale) for x, y in svg_path_points(str(el.get("d") or ""))]
|
|
138
|
+
if len(points) < 2:
|
|
139
|
+
continue
|
|
140
|
+
fill = parse_hex_color(el.get("fill"), None)
|
|
141
|
+
if fill is not None and str(el.get("fill")).strip().lower() != "none":
|
|
142
|
+
draw.polygon(points, fill=color_with_opacity(fill, el.get("opacity")))
|
|
143
|
+
stroke = parse_hex_color(el.get("stroke"), None)
|
|
144
|
+
if stroke is not None:
|
|
145
|
+
width = max(1, round(svg_float(el.get("stroke-width"), 1) * scale))
|
|
146
|
+
draw.line(points, fill=stroke, width=width, joint="curve")
|
|
147
|
+
elif el.name == "circle":
|
|
148
|
+
cx = svg_float(el.get("cx")) * scale
|
|
149
|
+
cy = svg_float(el.get("cy")) * scale
|
|
150
|
+
radius = svg_float(el.get("r")) * scale
|
|
151
|
+
box = [cx - radius, cy - radius, cx + radius, cy + radius]
|
|
152
|
+
fill = parse_hex_color(el.get("fill"), None)
|
|
153
|
+
stroke = parse_hex_color(el.get("stroke"), None)
|
|
154
|
+
width = max(1, round(svg_float(el.get("stroke-width"), 1) * scale))
|
|
155
|
+
if fill is not None:
|
|
156
|
+
draw.ellipse(box, fill=color_with_opacity(fill, el.get("opacity")))
|
|
157
|
+
if stroke is not None:
|
|
158
|
+
draw.ellipse(box, outline=stroke, width=width)
|
|
159
|
+
elif el.name == "line":
|
|
160
|
+
stroke = parse_hex_color(el.get("stroke"))
|
|
161
|
+
if stroke is None:
|
|
162
|
+
continue
|
|
163
|
+
x1 = svg_float(el.get("x1")) * scale
|
|
164
|
+
y1 = svg_float(el.get("y1")) * scale
|
|
165
|
+
x2 = svg_float(el.get("x2")) * scale
|
|
166
|
+
y2 = svg_float(el.get("y2")) * scale
|
|
167
|
+
width = max(1, round(svg_float(el.get("stroke-width"), 1) * scale))
|
|
168
|
+
draw.line([x1, y1, x2, y2], fill=stroke, width=width)
|
|
169
|
+
if el.get("stroke-linecap") == "round":
|
|
170
|
+
radius = width / 2
|
|
171
|
+
for x, y in [(x1, y1), (x2, y2)]:
|
|
172
|
+
draw.ellipse([x - radius, y - radius, x + radius, y + radius], fill=stroke)
|
|
173
|
+
elif el.name == "path":
|
|
174
|
+
points = scaled_points_from_path(str(el.get("d") or ""))
|
|
175
|
+
if len(points) < 2:
|
|
176
|
+
continue
|
|
177
|
+
fill = parse_hex_color(el.get("fill"), None)
|
|
178
|
+
if fill is not None and str(el.get("d") or "").strip().endswith("Z"):
|
|
179
|
+
draw.polygon(points, fill=fill)
|
|
180
|
+
stroke = parse_hex_color(el.get("stroke"), None)
|
|
181
|
+
if stroke is not None:
|
|
182
|
+
width = max(1, round(svg_float(el.get("stroke-width"), 1) * scale))
|
|
183
|
+
draw.line(points, fill=stroke, width=width, joint="curve")
|
|
184
|
+
if el.get("stroke-linecap") == "round":
|
|
185
|
+
radius = width / 2
|
|
186
|
+
for x, y in (points[0], points[-1]):
|
|
187
|
+
draw.ellipse([x - radius, y - radius, x + radius, y + radius], fill=stroke)
|
|
188
|
+
elif el.name == "circle":
|
|
189
|
+
fill = parse_hex_color(el.get("fill"), None)
|
|
190
|
+
stroke = parse_hex_color(el.get("stroke"), None)
|
|
191
|
+
cx = svg_float(el.get("cx")) * scale
|
|
192
|
+
cy = svg_float(el.get("cy")) * scale
|
|
193
|
+
radius = svg_float(el.get("r")) * scale
|
|
194
|
+
box = [cx - radius, cy - radius, cx + radius, cy + radius]
|
|
195
|
+
if fill is not None:
|
|
196
|
+
draw.ellipse(box, fill=fill)
|
|
197
|
+
if stroke is not None:
|
|
198
|
+
width = max(1, round(svg_float(el.get("stroke-width"), 1) * scale))
|
|
199
|
+
draw.ellipse(box, outline=stroke, width=width)
|
|
200
|
+
elif el.name == "text":
|
|
201
|
+
text = collapse_ws(el.get_text("", strip=True)).strip()
|
|
202
|
+
if not text:
|
|
203
|
+
continue
|
|
204
|
+
fill = parse_hex_color(el.get("fill"))
|
|
205
|
+
if fill is None:
|
|
206
|
+
continue
|
|
207
|
+
size = max(1, round(svg_float(el.get("font-size"), 12) * scale))
|
|
208
|
+
weight = svg_float(el.get("font-weight"), 400)
|
|
209
|
+
font = load_svg_font(size, bold=weight >= 600)
|
|
210
|
+
anchor = {"middle": "ms", "end": "rs"}.get(str(el.get("text-anchor", "")).strip(), "ls")
|
|
211
|
+
draw.text(
|
|
212
|
+
(svg_float(el.get("x")) * scale, svg_float(el.get("y")) * scale),
|
|
213
|
+
text,
|
|
214
|
+
fill=fill,
|
|
215
|
+
font=font,
|
|
216
|
+
anchor=anchor,
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
image.save(path, optimize=True)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def load_report_font(size: int, bold: bool = False) -> Any:
|
|
223
|
+
try:
|
|
224
|
+
from PIL import ImageFont
|
|
225
|
+
except ImportError: # pragma: no cover - dependency check path
|
|
226
|
+
return None
|
|
227
|
+
|
|
228
|
+
paths = []
|
|
229
|
+
if bold:
|
|
230
|
+
paths.extend(
|
|
231
|
+
[
|
|
232
|
+
"/System/Library/Fonts/Supplemental/Arial Bold.ttf",
|
|
233
|
+
"/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf",
|
|
234
|
+
]
|
|
235
|
+
)
|
|
236
|
+
paths.extend(
|
|
237
|
+
[
|
|
238
|
+
"/System/Library/Fonts/Supplemental/Arial.ttf",
|
|
239
|
+
"/System/Library/Fonts/SFNS.ttf",
|
|
240
|
+
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
|
|
241
|
+
]
|
|
242
|
+
)
|
|
243
|
+
for path in paths:
|
|
244
|
+
if Path(path).exists():
|
|
245
|
+
return ImageFont.truetype(path, size)
|
|
246
|
+
return ImageFont.load_default()
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def draw_right_aligned_text(
|
|
250
|
+
draw: Any, xy: tuple[int, int], text: str, font: Any, fill: Any
|
|
251
|
+
) -> None:
|
|
252
|
+
bbox = draw.textbbox((0, 0), text, font=font)
|
|
253
|
+
draw.text((xy[0] - (bbox[2] - bbox[0]), xy[1]), text, font=font, fill=fill)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def render_two_col_table_grid(chart: dict[str, Any], path: Path) -> None:
|
|
257
|
+
try:
|
|
258
|
+
from PIL import Image, ImageDraw
|
|
259
|
+
except ImportError as exc: # pragma: no cover - dependency check path
|
|
260
|
+
raise RuntimeError("Two-column table rendering requires pillow") from exc
|
|
261
|
+
|
|
262
|
+
scale = 2
|
|
263
|
+
css_w = 920
|
|
264
|
+
gap = 20
|
|
265
|
+
pad_x = 18
|
|
266
|
+
pad_y = 4
|
|
267
|
+
panel_count = max(1, len(chart["panels"]))
|
|
268
|
+
panel_w = (css_w - gap * (panel_count - 1) - pad_x * 2) / panel_count
|
|
269
|
+
title_h = 28
|
|
270
|
+
header_h = 32
|
|
271
|
+
row_h = 34
|
|
272
|
+
panel_heights = [
|
|
273
|
+
title_h + header_h + max(0, len(panel["rows"]) - 1) * row_h for panel in chart["panels"]
|
|
274
|
+
]
|
|
275
|
+
css_h = pad_y * 2 + max(panel_heights)
|
|
276
|
+
image = Image.new("RGB", (css_w * scale, css_h * scale), (255, 255, 255))
|
|
277
|
+
draw = ImageDraw.Draw(image)
|
|
278
|
+
|
|
279
|
+
ink = (32, 37, 43)
|
|
280
|
+
muted = (98, 109, 121)
|
|
281
|
+
faint = (229, 234, 239)
|
|
282
|
+
green = (38, 132, 95)
|
|
283
|
+
red = (199, 81, 70)
|
|
284
|
+
|
|
285
|
+
title_font = load_report_font(13 * scale, bold=True)
|
|
286
|
+
header_font = load_report_font(13 * scale, bold=True)
|
|
287
|
+
body_font = load_report_font(13 * scale)
|
|
288
|
+
body_bold_font = load_report_font(13 * scale, bold=True)
|
|
289
|
+
|
|
290
|
+
def sx(value: float) -> int:
|
|
291
|
+
return round(value * scale)
|
|
292
|
+
|
|
293
|
+
for panel_idx, panel in enumerate(chart["panels"]):
|
|
294
|
+
x0 = pad_x + panel_idx * (panel_w + gap)
|
|
295
|
+
y0 = pad_y
|
|
296
|
+
rows = panel["rows"]
|
|
297
|
+
styles = panel.get("cell_styles", [[[] for _ in row] for row in rows])
|
|
298
|
+
title = str(panel.get("title") or "").upper()
|
|
299
|
+
draw.text((sx(x0), sx(y0 + 2)), title, font=title_font, fill=muted)
|
|
300
|
+
|
|
301
|
+
table_y = y0 + title_h
|
|
302
|
+
col_widths = [panel_w * 0.40, panel_w * 0.20, panel_w * 0.20, panel_w * 0.20]
|
|
303
|
+
col_lefts = [x0]
|
|
304
|
+
for width in col_widths[:-1]:
|
|
305
|
+
col_lefts.append(col_lefts[-1] + width)
|
|
306
|
+
table_right = x0 + sum(col_widths)
|
|
307
|
+
|
|
308
|
+
for r, row in enumerate(rows):
|
|
309
|
+
row_top = table_y + (0 if r == 0 else header_h + (r - 1) * row_h)
|
|
310
|
+
current_h = header_h if r == 0 else row_h
|
|
311
|
+
line_y = row_top + current_h
|
|
312
|
+
draw.line((sx(x0), sx(line_y), sx(table_right), sx(line_y)), fill=faint, width=scale)
|
|
313
|
+
for c, text in enumerate(row):
|
|
314
|
+
cell_styles = set(styles[r][c]) if r < len(styles) and c < len(styles[r]) else set()
|
|
315
|
+
is_numeric = c > 0
|
|
316
|
+
color = muted if r == 0 else ink
|
|
317
|
+
if "positive" in cell_styles:
|
|
318
|
+
color = green
|
|
319
|
+
elif "negative" in cell_styles:
|
|
320
|
+
color = red
|
|
321
|
+
font = header_font if r == 0 or "bold" in cell_styles else body_font
|
|
322
|
+
if "positive" in cell_styles or "negative" in cell_styles:
|
|
323
|
+
font = body_bold_font
|
|
324
|
+
y_text = row_top + (8 if r == 0 else 9)
|
|
325
|
+
if is_numeric:
|
|
326
|
+
draw_right_aligned_text(
|
|
327
|
+
draw,
|
|
328
|
+
(sx(col_lefts[c] + col_widths[c] - 7), sx(y_text)),
|
|
329
|
+
text,
|
|
330
|
+
font,
|
|
331
|
+
color,
|
|
332
|
+
)
|
|
333
|
+
else:
|
|
334
|
+
draw.text((sx(col_lefts[c] + 7), sx(y_text)), text, font=font, fill=color)
|
|
335
|
+
|
|
336
|
+
image.save(path, optimize=True)
|
|
337
|
+
chart["image_width_px"] = css_w * scale
|
|
338
|
+
chart["image_height_px"] = css_h * scale
|
|
339
|
+
chart["docs_width_pt"] = DOC_CONTENT_WIDTH_PT
|
|
340
|
+
chart["docs_height_pt"] = round(DOC_CONTENT_WIDTH_PT * css_h / css_w, 1)
|
|
341
|
+
chart["render_preflight_stats"] = inspect_image_object(
|
|
342
|
+
image,
|
|
343
|
+
expected_chart_colors(chart),
|
|
344
|
+
exact_color_scan=True,
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def load_bar_chart_font() -> Any:
|
|
349
|
+
try:
|
|
350
|
+
from PIL import ImageFont
|
|
351
|
+
except ImportError as exc: # pragma: no cover - dependency check path
|
|
352
|
+
raise RuntimeError("Bar chart rendering requires pillow") from exc
|
|
353
|
+
|
|
354
|
+
for font_path in [
|
|
355
|
+
"/System/Library/Fonts/SFNS.ttf",
|
|
356
|
+
"/System/Library/Fonts/Supplemental/Arial.ttf",
|
|
357
|
+
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
|
|
358
|
+
]:
|
|
359
|
+
if Path(font_path).exists():
|
|
360
|
+
return ImageFont.truetype(font_path, 28)
|
|
361
|
+
return ImageFont.load_default()
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def png_is_blank_or_near_empty(candidate: Path) -> bool:
|
|
365
|
+
try:
|
|
366
|
+
from PIL import Image
|
|
367
|
+
except ImportError:
|
|
368
|
+
return False
|
|
369
|
+
try:
|
|
370
|
+
with Image.open(candidate) as image:
|
|
371
|
+
rgb_image = image.convert("RGB")
|
|
372
|
+
if hasattr(rgb_image, "get_flattened_data"):
|
|
373
|
+
pixels = list(rgb_image.get_flattened_data())
|
|
374
|
+
else:
|
|
375
|
+
pixels = list(rgb_image.getdata())
|
|
376
|
+
except Exception:
|
|
377
|
+
return True
|
|
378
|
+
if not pixels:
|
|
379
|
+
return True
|
|
380
|
+
sample = pixels[: min(len(pixels), 10000)]
|
|
381
|
+
if len(set(sample)) <= 1 and len(set(pixels)) <= 1:
|
|
382
|
+
return True
|
|
383
|
+
non_white = sum(1 for red, green, blue in pixels if min(red, green, blue) < 245)
|
|
384
|
+
return non_white / len(pixels) < 0.002
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def render_svg_chart_image(chart: dict[str, Any], out_dir: Path) -> None:
|
|
388
|
+
image_dir = out_dir / "chart_images"
|
|
389
|
+
path = out_dir / chart["image_file"]
|
|
390
|
+
svg_markup = chart["svg_markup"].encode("utf-8")
|
|
391
|
+
try:
|
|
392
|
+
import cairosvg # type: ignore[import-not-found]
|
|
393
|
+
|
|
394
|
+
cairosvg.svg2png(bytestring=svg_markup, write_to=str(path))
|
|
395
|
+
except Exception:
|
|
396
|
+
rsvg_convert = shutil.which("rsvg-convert")
|
|
397
|
+
if rsvg_convert:
|
|
398
|
+
subprocess.run(
|
|
399
|
+
[rsvg_convert, "-f", "png", "-o", str(path)],
|
|
400
|
+
input=svg_markup,
|
|
401
|
+
check=True,
|
|
402
|
+
)
|
|
403
|
+
else:
|
|
404
|
+
width_px = int(chart.get("source_width_px", 1000))
|
|
405
|
+
height_px = int(chart.get("source_height_px", 600))
|
|
406
|
+
try:
|
|
407
|
+
render_simple_svg_with_pillow(chart["svg_markup"], path, width_px, height_px)
|
|
408
|
+
except Exception:
|
|
409
|
+
pass
|
|
410
|
+
if path.exists() and png_is_blank_or_near_empty(path):
|
|
411
|
+
path.unlink()
|
|
412
|
+
if not path.exists():
|
|
413
|
+
try:
|
|
414
|
+
from playwright.sync_api import sync_playwright
|
|
415
|
+
except ImportError as exc: # pragma: no cover - dependency check path
|
|
416
|
+
raise SystemExit(
|
|
417
|
+
"SVG chart rendering requires cairosvg, rsvg-convert, or "
|
|
418
|
+
"python playwright with an installed Chromium browser; the "
|
|
419
|
+
"built-in Pillow fallback could not render this SVG."
|
|
420
|
+
) from exc
|
|
421
|
+
width_px = int(chart.get("source_width_px", 1000))
|
|
422
|
+
height_px = int(chart.get("source_height_px", 600))
|
|
423
|
+
tmp_html = image_dir / f"{chart['id'].lower()}_render.html"
|
|
424
|
+
tmp_html.write_text(
|
|
425
|
+
"<!doctype html><meta charset='utf-8'>"
|
|
426
|
+
"<style>body{margin:0;background:white}svg{display:block}</style>"
|
|
427
|
+
+ chart["svg_markup"],
|
|
428
|
+
encoding="utf-8",
|
|
429
|
+
)
|
|
430
|
+
with sync_playwright() as playwright:
|
|
431
|
+
browser = playwright.chromium.launch(headless=True)
|
|
432
|
+
page = browser.new_page(
|
|
433
|
+
viewport={"width": width_px, "height": height_px},
|
|
434
|
+
device_scale_factor=2,
|
|
435
|
+
)
|
|
436
|
+
page.goto(tmp_html.resolve().as_uri())
|
|
437
|
+
page.locator("svg").screenshot(path=str(path))
|
|
438
|
+
browser.close()
|
|
439
|
+
tmp_html.unlink(missing_ok=True)
|
|
440
|
+
chart["image_width_px"] = chart.get("source_width_px", 1000)
|
|
441
|
+
chart["image_height_px"] = chart.get("source_height_px", 600)
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
def render_bar_chart_image(chart: dict[str, Any], out_dir: Path) -> None:
|
|
445
|
+
try:
|
|
446
|
+
from PIL import Image, ImageDraw
|
|
447
|
+
except ImportError as exc: # pragma: no cover - dependency check path
|
|
448
|
+
raise RuntimeError("Bar chart rendering requires pillow") from exc
|
|
449
|
+
|
|
450
|
+
css_w = 760
|
|
451
|
+
label_w = 92
|
|
452
|
+
value_w = 84
|
|
453
|
+
gap = 12
|
|
454
|
+
track_w = css_w - label_w - value_w - 2 * gap
|
|
455
|
+
row_h = 28
|
|
456
|
+
chart_gap = 10
|
|
457
|
+
pad_y = 6
|
|
458
|
+
scale = 2
|
|
459
|
+
ink = (23, 32, 38)
|
|
460
|
+
track = (232, 238, 241)
|
|
461
|
+
accent = (15, 118, 110)
|
|
462
|
+
blue = (29, 78, 216)
|
|
463
|
+
font = load_bar_chart_font()
|
|
464
|
+
|
|
465
|
+
rows = chart["rows"]
|
|
466
|
+
width = css_w * scale
|
|
467
|
+
height = (pad_y * 2 + len(rows) * row_h + (len(rows) - 1) * chart_gap) * scale
|
|
468
|
+
image = Image.new("RGB", (width, height), (255, 255, 255))
|
|
469
|
+
draw = ImageDraw.Draw(image)
|
|
470
|
+
fill_color = blue if chart.get("bar_color") == "blue" else accent
|
|
471
|
+
radius = 6 * scale
|
|
472
|
+
|
|
473
|
+
for idx, row in enumerate(rows):
|
|
474
|
+
y = (pad_y + idx * (row_h + chart_gap)) * scale
|
|
475
|
+
draw.text((0, y - 2 * scale), row["label"], font=font, fill=ink)
|
|
476
|
+
track_x = (label_w + gap) * scale
|
|
477
|
+
track_y = y + 5 * scale
|
|
478
|
+
track_width = track_w * scale
|
|
479
|
+
track_height = 18 * scale
|
|
480
|
+
draw.rounded_rectangle(
|
|
481
|
+
[track_x, track_y, track_x + track_width, track_y + track_height],
|
|
482
|
+
radius=radius,
|
|
483
|
+
fill=track,
|
|
484
|
+
)
|
|
485
|
+
fill_width = max(0, min(track_width, track_width * float(row["pct"]) / 100.0))
|
|
486
|
+
draw.rounded_rectangle(
|
|
487
|
+
[track_x, track_y, track_x + fill_width, track_y + track_height],
|
|
488
|
+
radius=radius,
|
|
489
|
+
fill=fill_color,
|
|
490
|
+
)
|
|
491
|
+
bbox = draw.textbbox((0, 0), row["value"], font=font)
|
|
492
|
+
value_width = bbox[2] - bbox[0]
|
|
493
|
+
value_x = (label_w + gap + track_w + gap + value_w) * scale - value_width
|
|
494
|
+
draw.text((value_x, y - 2 * scale), row["value"], font=font, fill=ink)
|
|
495
|
+
|
|
496
|
+
path = out_dir / chart["image_file"]
|
|
497
|
+
image.save(path, optimize=True)
|
|
498
|
+
chart["image_width_px"] = width
|
|
499
|
+
chart["image_height_px"] = height
|
|
500
|
+
chart["docs_width_pt"] = DOC_CONTENT_WIDTH_PT
|
|
501
|
+
chart["docs_height_pt"] = round(DOC_CONTENT_WIDTH_PT * height / width, 1)
|
|
502
|
+
chart["render_preflight_stats"] = inspect_image_object(image, expected_chart_colors(chart))
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
def render_embedded_image(chart: dict[str, Any], out_dir: Path) -> None:
|
|
506
|
+
try:
|
|
507
|
+
from PIL import Image, ImageOps
|
|
508
|
+
except ImportError as exc: # pragma: no cover - dependency check path
|
|
509
|
+
raise RuntimeError("Embedded image rendering requires pillow") from exc
|
|
510
|
+
|
|
511
|
+
src = str(chart.get("src") or "")
|
|
512
|
+
match = re.match(r"data:image/[^;]+;base64,(.+)", src, flags=re.DOTALL)
|
|
513
|
+
if not match:
|
|
514
|
+
raise RuntimeError("Only base64 data URI report images are supported")
|
|
515
|
+
|
|
516
|
+
raw = base64.b64decode(re.sub(r"\s+", "", match.group(1)), validate=False)
|
|
517
|
+
path = out_dir / chart["image_file"]
|
|
518
|
+
with Image.open(BytesIO(raw)) as image:
|
|
519
|
+
image = ImageOps.exif_transpose(image)
|
|
520
|
+
if image.mode == "RGBA":
|
|
521
|
+
background = Image.new("RGB", image.size, (255, 255, 255))
|
|
522
|
+
background.paste(image, mask=image.getchannel("A"))
|
|
523
|
+
image = background
|
|
524
|
+
elif image.mode != "RGB":
|
|
525
|
+
image = image.convert("RGB")
|
|
526
|
+
width, height = image.size
|
|
527
|
+
image.save(path, format="PNG", optimize=True)
|
|
528
|
+
chart["image_width_px"] = width
|
|
529
|
+
chart["image_height_px"] = height
|
|
530
|
+
chart["docs_width_pt"] = DOC_CONTENT_WIDTH_PT
|
|
531
|
+
chart["docs_height_pt"] = round(DOC_CONTENT_WIDTH_PT * height / width, 1) if width else 240
|
|
532
|
+
chart["render_preflight_stats"] = inspect_image_object(image, expected_chart_colors(chart))
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
def render_single_chart_image(chart: dict[str, Any], out_dir: Path) -> None:
|
|
536
|
+
kind = chart.get("kind")
|
|
537
|
+
if kind == "two_col_table_grid":
|
|
538
|
+
render_two_col_table_grid(chart, out_dir / chart["image_file"])
|
|
539
|
+
elif kind == "svg":
|
|
540
|
+
render_svg_chart_image(chart, out_dir)
|
|
541
|
+
elif kind == "bar_chart":
|
|
542
|
+
render_bar_chart_image(chart, out_dir)
|
|
543
|
+
elif kind == "embedded_image":
|
|
544
|
+
render_embedded_image(chart, out_dir)
|
|
545
|
+
else:
|
|
546
|
+
raise RuntimeError(f"Unsupported chart image kind: {kind}")
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
def choose_render_workers(charts: list[dict[str, Any]], requested_workers: int) -> int:
|
|
550
|
+
"""Pick a conservative local render worker count.
|
|
551
|
+
|
|
552
|
+
The renderer mix matters more than CPU count. SVG-only reports spend most
|
|
553
|
+
of their time inside external/native renderers, so modest concurrency is a
|
|
554
|
+
meaningful speedup. Pillow-heavy reports stay serial by default because the
|
|
555
|
+
extra thread overhead is less predictably useful for those small images.
|
|
556
|
+
"""
|
|
557
|
+
if not charts:
|
|
558
|
+
return 1
|
|
559
|
+
requested = int(requested_workers)
|
|
560
|
+
if requested > 0:
|
|
561
|
+
return min(requested, len(charts))
|
|
562
|
+
if all(chart.get("kind") == "svg" for chart in charts):
|
|
563
|
+
return min(6, len(charts))
|
|
564
|
+
return 1
|
|
565
|
+
|
|
566
|
+
|
|
567
|
+
def render_chart_images(
|
|
568
|
+
manifest: dict[str, Any], out_dir: Path, render_workers: int = DEFAULT_RENDER_WORKERS
|
|
569
|
+
) -> None:
|
|
570
|
+
"""Render every visual block before any remote Docs write happens.
|
|
571
|
+
|
|
572
|
+
The output PNGs are the quality boundary for charts and multi-column table
|
|
573
|
+
groups. Rendering them in parallel is safe because each chart writes to a
|
|
574
|
+
distinct filename and the manifest order is preserved for Docs insertion.
|
|
575
|
+
"""
|
|
576
|
+
charts = manifest.get("chart_images", [])
|
|
577
|
+
if not charts:
|
|
578
|
+
return
|
|
579
|
+
|
|
580
|
+
needs_pillow = any(
|
|
581
|
+
chart.get("kind") in {"bar_chart", "embedded_image", "two_col_table_grid"}
|
|
582
|
+
for chart in charts
|
|
583
|
+
)
|
|
584
|
+
if needs_pillow:
|
|
585
|
+
try:
|
|
586
|
+
__import__("PIL.Image")
|
|
587
|
+
except ImportError as exc: # pragma: no cover - dependency check path
|
|
588
|
+
raise SystemExit(
|
|
589
|
+
"Bar chart-image mode requires Pillow. Install pillow or rerun with "
|
|
590
|
+
"`--chart-mode table` to use the old native-table approximation."
|
|
591
|
+
) from exc
|
|
592
|
+
|
|
593
|
+
image_dir = out_dir / "chart_images"
|
|
594
|
+
image_dir.mkdir(parents=True, exist_ok=True)
|
|
595
|
+
|
|
596
|
+
workers = choose_render_workers(charts, render_workers)
|
|
597
|
+
if workers == 1 or len(charts) == 1:
|
|
598
|
+
for chart in charts:
|
|
599
|
+
render_single_chart_image(chart, out_dir)
|
|
600
|
+
return
|
|
601
|
+
|
|
602
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
603
|
+
|
|
604
|
+
with ThreadPoolExecutor(max_workers=min(workers, len(charts))) as executor:
|
|
605
|
+
futures = {
|
|
606
|
+
executor.submit(render_single_chart_image, chart, out_dir): chart for chart in charts
|
|
607
|
+
}
|
|
608
|
+
for future in as_completed(futures):
|
|
609
|
+
chart = futures[future]
|
|
610
|
+
try:
|
|
611
|
+
future.result()
|
|
612
|
+
except Exception as exc:
|
|
613
|
+
raise RuntimeError(f"Failed to render {chart.get('id')}: {exc}") from exc
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from .constants import DOC_CONTENT_WIDTH_PT
|
|
7
|
+
|
|
8
|
+
NUMERIC_TABLE_CELL_RE = re.compile(r"^[\s$%+,\-()./:0-9xXkKmMbBtT]+$")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def table_column_widths(table: dict[str, Any], columns: int) -> list[float]:
|
|
12
|
+
if columns <= 0:
|
|
13
|
+
return []
|
|
14
|
+
|
|
15
|
+
total_width = float(table.get("docs_width_pt", DOC_CONTENT_WIDTH_PT))
|
|
16
|
+
kind = table.get("kind")
|
|
17
|
+
if kind == "metric_cards" or columns <= 4:
|
|
18
|
+
width = total_width / columns
|
|
19
|
+
return [round(width, 1)] * columns
|
|
20
|
+
|
|
21
|
+
if columns == 5:
|
|
22
|
+
first = 120.0
|
|
23
|
+
elif columns >= 6:
|
|
24
|
+
first = 90.0
|
|
25
|
+
else:
|
|
26
|
+
first = total_width / columns
|
|
27
|
+
|
|
28
|
+
first = min(first, total_width / columns * 1.15)
|
|
29
|
+
remaining = (total_width - first) / (columns - 1)
|
|
30
|
+
return [round(first, 1)] + [round(remaining, 1)] * (columns - 1)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def table_cell_is_numeric(text: str) -> bool:
|
|
34
|
+
stripped = text.strip()
|
|
35
|
+
return bool(
|
|
36
|
+
stripped
|
|
37
|
+
and any(char.isdigit() for char in stripped)
|
|
38
|
+
and NUMERIC_TABLE_CELL_RE.match(stripped)
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def numeric_table_columns(rows: list[list[str]]) -> set[int]:
|
|
43
|
+
if not rows:
|
|
44
|
+
return set()
|
|
45
|
+
numeric_columns: set[int] = set()
|
|
46
|
+
for column_index in range(len(rows[0])):
|
|
47
|
+
values = [
|
|
48
|
+
row[column_index]
|
|
49
|
+
for row in rows[1:]
|
|
50
|
+
if column_index < len(row) and row[column_index].strip()
|
|
51
|
+
]
|
|
52
|
+
if values and all(table_cell_is_numeric(value) for value in values):
|
|
53
|
+
numeric_columns.add(column_index)
|
|
54
|
+
return numeric_columns
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def rgb(color: dict[str, float]) -> dict[str, Any]:
|
|
8
|
+
return {"color": {"rgbColor": color}}
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def docs_range(start: int, end: int) -> dict[str, int]:
|
|
12
|
+
return {"startIndex": start + 1, "endIndex": end + 1}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def collapse_ws(text: str) -> str:
|
|
16
|
+
return re.sub(r"\s+", " ", text)
|