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.
Files changed (257) hide show
  1. package/package.json +1 -1
  2. package/template/.agents/skills/high-fidelity-html-prototype/SKILL.md +102 -0
  3. package/template/.codex/skills/data-analytics/.app.json +84 -0
  4. package/template/.codex/skills/data-analytics/.codex-plugin/plugin.json +66 -0
  5. package/template/.codex/skills/data-analytics/.mcp.json +30 -0
  6. package/template/.codex/skills/data-analytics/AGENTS.md +20 -0
  7. package/template/.codex/skills/data-analytics/DEPENDENCIES.MD +27 -0
  8. package/template/.codex/skills/data-analytics/README.md +60 -0
  9. package/template/.codex/skills/data-analytics/assets/datascience-artifact-widget.html +16 -0
  10. package/template/.codex/skills/data-analytics/assets/datascience-artifact-widget.html.gz.b64.part001 +1 -0
  11. package/template/.codex/skills/data-analytics/assets/datascience-artifact-widget.html.gz.b64.part002 +1 -0
  12. package/template/.codex/skills/data-analytics/assets/datascience-artifact-widget.html.gz.b64.part003 +1 -0
  13. package/template/.codex/skills/data-analytics/assets/datascience-artifact-widget.html.gz.b64.part004 +1 -0
  14. package/template/.codex/skills/data-analytics/assets/datascience-artifact-widget.html.gz.b64.part005 +1 -0
  15. package/template/.codex/skills/data-analytics/assets/datascience-artifact-widget.html.gz.b64.part006 +1 -0
  16. package/template/.codex/skills/data-analytics/assets/datascience-chart-widget.html +16 -0
  17. package/template/.codex/skills/data-analytics/assets/datascience-chart-widget.html.gz.b64.part001 +1 -0
  18. package/template/.codex/skills/data-analytics/assets/datascience-chart-widget.html.gz.b64.part002 +1 -0
  19. package/template/.codex/skills/data-analytics/assets/datascience-chart-widget.html.gz.b64.part003 +1 -0
  20. package/template/.codex/skills/data-analytics/assets/datascience-chart-widget.html.gz.b64.part004 +1 -0
  21. package/template/.codex/skills/data-analytics/assets/datascience-chart-widget.html.gz.b64.part005 +1 -0
  22. package/template/.codex/skills/data-analytics/assets/datascience-chart-widget.html.gz.b64.part006 +1 -0
  23. package/template/.codex/skills/data-analytics/assets/datascience-chart-widget.html.gz.b64.part007 +1 -0
  24. package/template/.codex/skills/data-analytics/assets/datascience-small.svg +5 -0
  25. package/template/.codex/skills/data-analytics/assets/datascience-table-widget.html +16 -0
  26. package/template/.codex/skills/data-analytics/assets/datascience-table-widget.html.gz.b64.part001 +1 -0
  27. package/template/.codex/skills/data-analytics/assets/datascience-table-widget.html.gz.b64.part002 +1 -0
  28. package/template/.codex/skills/data-analytics/assets/datascience-table-widget.html.gz.b64.part003 +1 -0
  29. package/template/.codex/skills/data-analytics/assets/datascience-table-widget.html.gz.b64.part004 +1 -0
  30. package/template/.codex/skills/data-analytics/assets/datascience.png +0 -0
  31. package/template/.codex/skills/data-analytics/assets/datascience.svg +10 -0
  32. package/template/.codex/skills/data-analytics/mcp/server.cjs +2963 -0
  33. package/template/.codex/skills/data-analytics/package-lock.json +3048 -0
  34. package/template/.codex/skills/data-analytics/package.json +43 -0
  35. package/template/.codex/skills/data-analytics/scripts/normalize-widget-assets.mjs +75 -0
  36. package/template/.codex/skills/data-analytics/skills/analyze-data-quality/SKILL.md +160 -0
  37. package/template/.codex/skills/data-analytics/skills/analyze-data-quality/agents/openai.yaml +6 -0
  38. package/template/.codex/skills/data-analytics/skills/build-dashboard/SKILL.md +148 -0
  39. package/template/.codex/skills/data-analytics/skills/build-dashboard/agents/openai.yaml +6 -0
  40. package/template/.codex/skills/data-analytics/skills/build-dashboard/specifications/bi-platform-dashboard.md +18 -0
  41. package/template/.codex/skills/data-analytics/skills/build-dashboard/specifications/html-dashboard.md +24 -0
  42. package/template/.codex/skills/data-analytics/skills/build-dashboard/specifications/mcp-artifact-dashboard.md +71 -0
  43. package/template/.codex/skills/data-analytics/skills/build-dashboard/specifications/streamlit-dashboard.md +85 -0
  44. package/template/.codex/skills/data-analytics/skills/build-report/SKILL.md +207 -0
  45. package/template/.codex/skills/data-analytics/skills/build-report/agents/openai.yaml +6 -0
  46. package/template/.codex/skills/data-analytics/skills/build-report/assets/executive-report-shell.html +70 -0
  47. package/template/.codex/skills/data-analytics/skills/build-report/assets/technical-report-shell.html +66 -0
  48. package/template/.codex/skills/data-analytics/skills/build-report/report-to-google-doc/SKILL.md +118 -0
  49. package/template/.codex/skills/data-analytics/skills/build-report/report-to-google-doc/agents/openai.yaml +6 -0
  50. package/template/.codex/skills/data-analytics/skills/build-report/report-to-google-doc/scripts/report_to_google_doc/__init__.py +1 -0
  51. package/template/.codex/skills/data-analytics/skills/build-report/report-to-google-doc/scripts/report_to_google_doc/cli.py +57 -0
  52. package/template/.codex/skills/data-analytics/skills/build-report/report-to-google-doc/scripts/report_to_google_doc/constants.py +61 -0
  53. package/template/.codex/skills/data-analytics/skills/build-report/report-to-google-doc/scripts/report_to_google_doc/docx_writer.py +362 -0
  54. package/template/.codex/skills/data-analytics/skills/build-report/report-to-google-doc/scripts/report_to_google_doc/html_parser.py +829 -0
  55. package/template/.codex/skills/data-analytics/skills/build-report/report-to-google-doc/scripts/report_to_google_doc/model.py +30 -0
  56. package/template/.codex/skills/data-analytics/skills/build-report/report-to-google-doc/scripts/report_to_google_doc/plan.py +129 -0
  57. package/template/.codex/skills/data-analytics/skills/build-report/report-to-google-doc/scripts/report_to_google_doc/quality.py +374 -0
  58. package/template/.codex/skills/data-analytics/skills/build-report/report-to-google-doc/scripts/report_to_google_doc/rendering.py +613 -0
  59. package/template/.codex/skills/data-analytics/skills/build-report/report-to-google-doc/scripts/report_to_google_doc/table_utils.py +54 -0
  60. package/template/.codex/skills/data-analytics/skills/build-report/report-to-google-doc/scripts/report_to_google_doc/utils.py +16 -0
  61. package/template/.codex/skills/data-analytics/skills/build-report/report-to-google-doc/scripts/report_to_google_doc_plan.py +9 -0
  62. package/template/.codex/skills/data-analytics/skills/build-report/report-to-google-slides/SKILL.md +78 -0
  63. package/template/.codex/skills/data-analytics/skills/build-report/report-to-google-slides/agents/openai.yaml +6 -0
  64. package/template/.codex/skills/data-analytics/skills/build-report/report-to-google-slides/scripts/report_to_google_slides.py +2379 -0
  65. package/template/.codex/skills/data-analytics/skills/build-report/report-to-pdf/SKILL.md +88 -0
  66. package/template/.codex/skills/data-analytics/skills/build-report/report-to-pdf/agents/openai.yaml +6 -0
  67. package/template/.codex/skills/data-analytics/skills/build-report/specifications/executive-report.md +97 -0
  68. package/template/.codex/skills/data-analytics/skills/build-report/specifications/mcp-app-report.md +59 -0
  69. package/template/.codex/skills/data-analytics/skills/build-report/specifications/technical-report.md +75 -0
  70. package/template/.codex/skills/data-analytics/skills/design-kpis/SKILL.md +103 -0
  71. package/template/.codex/skills/data-analytics/skills/design-kpis/agents/openai.yaml +6 -0
  72. package/template/.codex/skills/data-analytics/skills/gather-business-context/SKILL.md +68 -0
  73. package/template/.codex/skills/data-analytics/skills/gather-business-context/agents/openai.yaml +6 -0
  74. package/template/.codex/skills/data-analytics/skills/index/SKILL.md +251 -0
  75. package/template/.codex/skills/data-analytics/skills/index/agents/openai.yaml +6 -0
  76. package/template/.codex/skills/data-analytics/skills/jupyter-notebooks/SKILL.md +131 -0
  77. package/template/.codex/skills/data-analytics/skills/jupyter-notebooks/agents/openai.yaml +6 -0
  78. package/template/.codex/skills/data-analytics/skills/kpi-reporting/SKILL.md +141 -0
  79. package/template/.codex/skills/data-analytics/skills/kpi-reporting/agents/openai.yaml +6 -0
  80. package/template/.codex/skills/data-analytics/skills/kpi-reporting/references/report-templates.md +32 -0
  81. package/template/.codex/skills/data-analytics/skills/market-sizing/SKILL.md +106 -0
  82. package/template/.codex/skills/data-analytics/skills/market-sizing/agents/openai.yaml +6 -0
  83. package/template/.codex/skills/data-analytics/skills/metric-diagnostics/SKILL.md +130 -0
  84. package/template/.codex/skills/data-analytics/skills/metric-diagnostics/agents/openai.yaml +6 -0
  85. package/template/.codex/skills/data-analytics/skills/product-business-analysis/SKILL.md +141 -0
  86. package/template/.codex/skills/data-analytics/skills/product-business-analysis/agents/openai.yaml +6 -0
  87. package/template/.codex/skills/data-analytics/skills/spreadsheets/SKILL.md +178 -0
  88. package/template/.codex/skills/data-analytics/skills/spreadsheets/agents/openai.yaml +9 -0
  89. package/template/.codex/skills/data-analytics/skills/spreadsheets/assets/file-spreadsheet.png +0 -0
  90. package/template/.codex/skills/data-analytics/skills/spreadsheets/charts.md +31 -0
  91. package/template/.codex/skills/data-analytics/skills/spreadsheets/references/artifact_tool_api.md +466 -0
  92. package/template/.codex/skills/data-analytics/skills/spreadsheets/style_guidelines.md +99 -0
  93. package/template/.codex/skills/data-analytics/skills/user-context/SKILL.md +195 -0
  94. package/template/.codex/skills/data-analytics/skills/user-context/agents/openai.yaml +6 -0
  95. package/template/.codex/skills/data-analytics/skills/user-context/plugin-author-config/automation-config.md +26 -0
  96. package/template/.codex/skills/data-analytics/skills/user-context/plugin-author-config/source-category-config.json +51 -0
  97. package/template/.codex/skills/data-analytics/skills/user-context/plugin-author-config/user-context-config.md +66 -0
  98. package/template/.codex/skills/data-analytics/skills/user-context/references/automation.md +69 -0
  99. package/template/.codex/skills/data-analytics/skills/user-context/references/onboarding-examples.md +204 -0
  100. package/template/.codex/skills/data-analytics/skills/user-context/references/onboarding-state-template.json +87 -0
  101. package/template/.codex/skills/data-analytics/skills/user-context/references/onboarding.md +497 -0
  102. package/template/.codex/skills/data-analytics/skills/user-context/references/semantic-layer/connector-playbook.md +74 -0
  103. package/template/.codex/skills/data-analytics/skills/user-context/references/semantic-layer/setup.md +65 -0
  104. package/template/.codex/skills/data-analytics/skills/user-context/references/semantic-layer/skill-template.md +160 -0
  105. package/template/.codex/skills/data-analytics/skills/user-context/references/semantic-layer/source-intake.md +75 -0
  106. package/template/.codex/skills/data-analytics/skills/user-context/references/semantic-layer/weekly-polling-automation.md +96 -0
  107. package/template/.codex/skills/data-analytics/skills/user-context/references/source-category-runtime.md +263 -0
  108. package/template/.codex/skills/data-analytics/skills/user-context/scripts/data_analytics_preflight.py +1460 -0
  109. package/template/.codex/skills/data-analytics/skills/user-context/scripts/init_user_context_state.py +128 -0
  110. package/template/.codex/skills/data-analytics/skills/user-context/scripts/reset_user_context_state.py +101 -0
  111. package/template/.codex/skills/data-analytics/skills/user-context/scripts/validate_user_context_preflight.py +499 -0
  112. package/template/.codex/skills/data-analytics/skills/user-context/tests/test_state_helpers.py +978 -0
  113. package/template/.codex/skills/data-analytics/skills/validate-data/SKILL.md +190 -0
  114. package/template/.codex/skills/data-analytics/skills/validate-data/agents/openai.yaml +6 -0
  115. package/template/.codex/skills/data-analytics/skills/visualize-data/SKILL.md +157 -0
  116. package/template/.codex/skills/data-analytics/skills/visualize-data/agents/openai.yaml +6 -0
  117. package/template/.codex/skills/data-analytics/skills/visualize-data/references/seaborn-templates.md +774 -0
  118. package/template/.codex/skills/data-analytics/src/DESIGN.md +222 -0
  119. package/template/.codex/skills/data-analytics/src/analytics-app/App.tsx +3666 -0
  120. package/template/.codex/skills/data-analytics/src/analytics-app/analytics-layout.test.mjs +136 -0
  121. package/template/.codex/skills/data-analytics/src/analytics-app/charting/ChartFrame.tsx +54 -0
  122. package/template/.codex/skills/data-analytics/src/analytics-app/charting/ChartLegend.tsx +96 -0
  123. package/template/.codex/skills/data-analytics/src/analytics-app/charting/ChartRenderer.tsx +1647 -0
  124. package/template/.codex/skills/data-analytics/src/analytics-app/charting/ChartTooltip.tsx +245 -0
  125. package/template/.codex/skills/data-analytics/src/analytics-app/charting/chart-app-helpers.tsx +462 -0
  126. package/template/.codex/skills/data-analytics/src/analytics-app/charting/chart-capabilities.ts +107 -0
  127. package/template/.codex/skills/data-analytics/src/analytics-app/charting/chart-compatibility.ts +164 -0
  128. package/template/.codex/skills/data-analytics/src/analytics-app/charting/chart-contract.ts +192 -0
  129. package/template/.codex/skills/data-analytics/src/analytics-app/charting/chart-theme.ts +203 -0
  130. package/template/.codex/skills/data-analytics/src/analytics-app/charting/chart-tokens.css +619 -0
  131. package/template/.codex/skills/data-analytics/src/analytics-app/charting/chart-transforms.ts +402 -0
  132. package/template/.codex/skills/data-analytics/src/analytics-app/fonts/SystemSansVariableVF.woff2 +0 -0
  133. package/template/.codex/skills/data-analytics/src/analytics-app/imageExport.ts +370 -0
  134. package/template/.codex/skills/data-analytics/src/analytics-app/layout/AnalyticsLayoutCanvas.tsx +536 -0
  135. package/template/.codex/skills/data-analytics/src/analytics-app/layout/RichMarkdown.tsx +681 -0
  136. package/template/.codex/skills/data-analytics/src/analytics-app/layout/analyticsLayoutCore.ts +164 -0
  137. package/template/.codex/skills/data-analytics/src/analytics-app/main.tsx +21 -0
  138. package/template/.codex/skills/data-analytics/src/analytics-app/scripts/chart_contract.py +472 -0
  139. package/template/.codex/skills/data-analytics/src/analytics-app/scripts/check_analytics_app_runtime_links.py +75 -0
  140. package/template/.codex/skills/data-analytics/src/analytics-app/scripts/design_contract.py +142 -0
  141. package/template/.codex/skills/data-analytics/src/analytics-app/scripts/package_utils.py +142 -0
  142. package/template/.codex/skills/data-analytics/src/analytics-app/scripts/tests/test_package_utils.py +377 -0
  143. package/template/.codex/skills/data-analytics/src/analytics-app/styles.css +3468 -0
  144. package/template/.codex/skills/data-analytics/src/analytics-app/tables/DataTable.d.ts +44 -0
  145. package/template/.codex/skills/data-analytics/src/analytics-app/tables/DataTable.jsx +639 -0
  146. package/template/.codex/skills/data-analytics/src/analytics-app/tables/data-table.css +327 -0
  147. package/template/.codex/skills/data-analytics/src/analytics-app/tokens.css +225 -0
  148. package/template/.codex/skills/data-analytics/src/analytics-app/types.ts +204 -0
  149. package/template/.codex/skills/data-analytics/src/analytics-app-core.md +85 -0
  150. package/template/.codex/skills/data-analytics/src/codex-style-contract.md +30 -0
  151. package/template/.codex/skills/data-analytics/src/datascience-artifact-widget.html +12 -0
  152. package/template/.codex/skills/data-analytics/src/datascience-artifact-widget.jsx +1008 -0
  153. package/template/.codex/skills/data-analytics/src/datascience-chart-widget.css +2117 -0
  154. package/template/.codex/skills/data-analytics/src/datascience-chart-widget.html +122 -0
  155. package/template/.codex/skills/data-analytics/src/datascience-chart-widget.js +3588 -0
  156. package/template/.codex/skills/data-analytics/src/datascience-table-widget.css +483 -0
  157. package/template/.codex/skills/data-analytics/src/datascience-table-widget.html +32 -0
  158. package/template/.codex/skills/data-analytics/src/datascience-table-widget.js +333 -0
  159. package/template/.codex/skills/data-analytics/src/mcp-host.js +153 -0
  160. package/template/.codex/skills/data-analytics/src/recharts-config.js +315 -0
  161. package/template/.codex/skills/data-analytics/src/recharts-renderer.jsx +294 -0
  162. package/template/.codex/skills/data-analytics/src/sql-source-view.css +33 -0
  163. package/template/.codex/skills/data-analytics/src/sql-source-view.js +157 -0
  164. package/template/.codex/skills/data-analytics/src/styles/codex-theme.css +121 -0
  165. package/template/.codex/skills/data-analytics/src/table-renderer.jsx +28 -0
  166. package/template/.codex/skills/data-analytics/tests/chart-transforms.test.mjs +87 -0
  167. package/template/.codex/skills/data-analytics/tests/funnel-smoke.html +105 -0
  168. package/template/.codex/skills/data-analytics/tests/inline-widget-compare.html +233 -0
  169. package/template/.codex/skills/data-analytics/tests/mcp-server.test.mjs +1500 -0
  170. package/template/.codex/skills/data-analytics/tests/native-style-contract.test.mjs +219 -0
  171. package/template/.codex/skills/data-analytics/tests/recharts-config.test.mjs +144 -0
  172. package/template/.codex/skills/data-analytics/tests/recharts-renderer.test.mjs +11 -0
  173. package/template/.codex/skills/data-analytics/tests/rich-markdown.test.mjs +54 -0
  174. package/template/.codex/skills/data-analytics/tests/widget-render-harness.html +321 -0
  175. package/template/.codex/skills/data-analytics/tsconfig.json +19 -0
  176. package/template/.codex/skills/data-analytics/vite.config.ts +46 -0
  177. package/template/.codex/skills/high-fidelity-html-prototype/SKILL.md +102 -0
  178. package/template/.codex/skills/high-fidelity-html-prototype/agents/openai.yaml +5 -0
  179. package/template/.codex/skills/product-design/.app.json +7 -0
  180. package/template/.codex/skills/product-design/.codex-plugin/plugin.json +50 -0
  181. package/template/.codex/skills/product-design/README.md +50 -0
  182. package/template/.codex/skills/product-design/agents/openai.yaml +4 -0
  183. package/template/.codex/skills/product-design/assets/composerIcon.svg +9 -0
  184. package/template/.codex/skills/product-design/assets/logo.png +0 -0
  185. package/template/.codex/skills/product-design/package.json +11 -0
  186. package/template/.codex/skills/product-design/references/browser-order.md +9 -0
  187. package/template/.codex/skills/product-design/references/communication-protocol.md +45 -0
  188. package/template/.codex/skills/product-design/references/critical-overrides.md +58 -0
  189. package/template/.codex/skills/product-design/references/local-prototype-preflight.md +16 -0
  190. package/template/.codex/skills/product-design/scripts/bootstrap-prototype.mjs +147 -0
  191. package/template/.codex/skills/product-design/skills/audit/SKILL.md +159 -0
  192. package/template/.codex/skills/product-design/skills/audit/agents/openai.yaml +6 -0
  193. package/template/.codex/skills/product-design/skills/audit/references/design-audit-framework.md +73 -0
  194. package/template/.codex/skills/product-design/skills/design-qa/SKILL.md +133 -0
  195. package/template/.codex/skills/product-design/skills/design-qa/agents/openai.yaml +6 -0
  196. package/template/.codex/skills/product-design/skills/design-qa/references/qa-rubric.md +61 -0
  197. package/template/.codex/skills/product-design/skills/get-context/SKILL.md +81 -0
  198. package/template/.codex/skills/product-design/skills/get-context/agents/openai.yaml +6 -0
  199. package/template/.codex/skills/product-design/skills/ideate/SKILL.md +179 -0
  200. package/template/.codex/skills/product-design/skills/ideate/agents/openai.yaml +6 -0
  201. package/template/.codex/skills/product-design/skills/image-to-code/SKILL.md +105 -0
  202. package/template/.codex/skills/product-design/skills/image-to-code/agents/openai.yaml +6 -0
  203. package/template/.codex/skills/product-design/skills/index/SKILL.md +126 -0
  204. package/template/.codex/skills/product-design/skills/index/agents/openai.yaml +6 -0
  205. package/template/.codex/skills/product-design/skills/prototype/SKILL.md +128 -0
  206. package/template/.codex/skills/product-design/skills/prototype/agents/openai.yaml +6 -0
  207. package/template/.codex/skills/product-design/skills/prototype/references/existing-codebase-edits.md +18 -0
  208. package/template/.codex/skills/product-design/skills/research/SKILL.md +92 -0
  209. package/template/.codex/skills/product-design/skills/research/agents/openai.yaml +6 -0
  210. package/template/.codex/skills/product-design/skills/share/SKILL.md +41 -0
  211. package/template/.codex/skills/product-design/skills/share/agents/openai.yaml +6 -0
  212. package/template/.codex/skills/product-design/skills/url-to-code/SKILL.md +124 -0
  213. package/template/.codex/skills/product-design/skills/url-to-code/agents/openai.yaml +6 -0
  214. package/template/.codex/skills/product-design/skills/user-context/SKILL.md +147 -0
  215. package/template/.codex/skills/product-design/skills/user-context/agents/openai.yaml +6 -0
  216. package/template/.codex/skills/product-design/skills/user-context/plugin-author-config/user-context-template.md +63 -0
  217. package/template/.codex/skills/product-design/skills/user-context/references/onboarding.md +66 -0
  218. package/template/.codex/skills/product-design/skills/user-context/scripts/init_user_context.py +86 -0
  219. package/template/.codex/skills/product-design/skills/user-context/scripts/user_context_preflight.py +204 -0
  220. package/template/.codex/skills/product-design/templates/prototype/AGENTS.md +7 -0
  221. package/template/.codex/skills/product-design/templates/prototype/index.html +12 -0
  222. package/template/.codex/skills/product-design/templates/prototype/package.json +18 -0
  223. package/template/.codex/skills/product-design/templates/prototype/src/App.jsx +5 -0
  224. package/template/.codex/skills/product-design/templates/prototype/src/main.jsx +10 -0
  225. package/template/.codex/skills/product-design/templates/prototype/src/styles.css +13 -0
  226. package/template/.codex/skills/product-design/templates/prototype/vite.config.mjs +14 -0
  227. package/template/.codex/skills/product-design-prototype/SKILL.md +2 -4
  228. package/template/.codex/skills/prototype-review/SKILL.md +7 -7
  229. package/template/.codex/skills/yss-design-system/SKILL.md +4 -2
  230. package/template/.codex/skills/yss-product-lifecycle/SKILL.md +10 -7
  231. package/template/.codex/skills/yss-product-lifecycle/references/artifact-checklist.md +1 -0
  232. package/template/.codex/skills/yss-product-lifecycle/references/stage-routing.md +1 -1
  233. package/template/.codex/skills/yss-ui/SKILL.md +4 -10
  234. package/template/.codex/skills/yss-ui/references/quick-recipes.md +1 -1
  235. package/template/AGENTS.md +9 -3
  236. package/template/CONTEXT.md +1 -0
  237. package/template/docs/adr/adr-product-design-prototype-entrypoint.md +54 -0
  238. package/template/docs/api/templates/openapi-draft-review-checklist.md +5 -1
  239. package/template/docs/design/README.md +13 -6
  240. package/template/docs/design/prototypes/.gitkeep +1 -0
  241. package/template/docs/design/templates/interaction-spec-template.md +8 -3
  242. package/template/docs/design/templates/product-overview-design-template.md +34 -5
  243. package/template/docs/design/templates/prototype-confirmation-template.md +57 -0
  244. package/template/docs/design/templates/prototype-review-checklist.md +7 -3
  245. package/template/docs/design/templates/state-matrix-template.md +1 -1
  246. package/template/docs/process/harness-executive-blueprint.md +6 -1
  247. package/template/docs/process/harness-process-tailoring.md +3 -3
  248. package/template/docs/process/harness-work-unit-map.md +1 -1
  249. package/template/docs/process/lifecycle-artifact-map.md +7 -5
  250. package/template/docs/process/product-prototype-workflow.md +82 -0
  251. package/template/docs/templates/requirement-freeze-template.md +4 -3
  252. package/template/docs/user-guide/product-lifecycle-workflow.md +2 -2
  253. package/template/docs/user-guide/product-rd-lifecycle-best-practices.md +54 -24
  254. package/template/.codex/skills/component-story-prototype/SKILL.md +0 -55
  255. package/template/.codex/skills/component-story-prototype/agents/openai.yaml +0 -4
  256. package/template/.codex/skills/mock-api-prototype/SKILL.md +0 -48
  257. package/template/.codex/skills/mock-api-prototype/agents/openai.yaml +0 -4
@@ -0,0 +1,829 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ from pathlib import Path
5
+ from typing import Any
6
+
7
+ try:
8
+ from bs4 import BeautifulSoup, NavigableString, Tag
9
+ except ImportError as exc: # pragma: no cover - dependency check path
10
+ BeautifulSoup = NavigableString = Tag = None # type: ignore[assignment,misc]
11
+ BS4_IMPORT_ERROR = exc
12
+ else:
13
+ BS4_IMPORT_ERROR = None
14
+
15
+ from .constants import DOC_CONTENT_WIDTH_PT, SVG_REPORT_STYLE_DEFAULTS
16
+ from .model import Builder
17
+ from .utils import collapse_ws
18
+
19
+ FONT_WEIGHT_BOLD_RE = re.compile(r"font-weight\s*:\s*([^;}\s]+)", re.IGNORECASE)
20
+ CSS_RULE_RE = re.compile(r"([^{}]+)\{([^{}]*)\}", re.DOTALL)
21
+
22
+
23
+ def require_bs4() -> None:
24
+ if BS4_IMPORT_ERROR is not None:
25
+ raise SystemExit(
26
+ "This helper requires beautifulsoup4. Install it in the active Python "
27
+ "environment before running this helper."
28
+ ) from BS4_IMPORT_ERROR
29
+
30
+
31
+ def append_text(builder: Builder, text: str) -> None:
32
+ text = collapse_ws(text)
33
+ if not text:
34
+ return
35
+ if text.isspace():
36
+ if builder.offset and not builder.text().endswith((" ", "\n")):
37
+ builder.add(" ")
38
+ return
39
+ if builder.offset and not builder.text().endswith((" ", "\n")) and text[0] not in " .,;:!?)%]":
40
+ builder.add(" ")
41
+ builder.add(text)
42
+
43
+
44
+ def classes(node: Tag) -> set[str]:
45
+ raw = node.get("class") or []
46
+ return set(raw if isinstance(raw, list) else str(raw).split())
47
+
48
+
49
+ def direct_tag_children(node: Tag) -> list[Tag]:
50
+ return [child for child in node.children if isinstance(child, Tag)]
51
+
52
+
53
+ def direct_children_with_class(node: Tag, class_name: str) -> list[Tag]:
54
+ return [child for child in direct_tag_children(node) if class_name in classes(child)]
55
+
56
+
57
+ def font_weight_is_bold(value: str) -> bool:
58
+ value = value.strip().lower()
59
+ if value in {"bold", "bolder"}:
60
+ return True
61
+ try:
62
+ return int(value) >= 600
63
+ except ValueError:
64
+ return False
65
+
66
+
67
+ def inline_style_is_bold(style: str | None) -> bool:
68
+ if not style:
69
+ return False
70
+ match = FONT_WEIGHT_BOLD_RE.search(style)
71
+ return bool(match and font_weight_is_bold(match.group(1)))
72
+
73
+
74
+ def table_first_column_is_bold(raw_html: str) -> bool:
75
+ accepted_selectors = {
76
+ "td:first-child",
77
+ "td:nth-child(1)",
78
+ "tbody td:first-child",
79
+ "tbody td:nth-child(1)",
80
+ "table td:first-child",
81
+ "table td:nth-child(1)",
82
+ "table tbody td:first-child",
83
+ "table tbody td:nth-child(1)",
84
+ }
85
+ for selectors, body in CSS_RULE_RE.findall(raw_html):
86
+ weight = FONT_WEIGHT_BOLD_RE.search(body)
87
+ if not weight or not font_weight_is_bold(weight.group(1)):
88
+ continue
89
+ normalized_selectors = {
90
+ re.sub(r"\s+", " ", selector.strip().lower()) for selector in selectors.split(",")
91
+ }
92
+ if normalized_selectors & accepted_selectors:
93
+ return True
94
+ return False
95
+
96
+
97
+ def append_inline(builder: Builder, node: Tag | NavigableString) -> None:
98
+ if isinstance(node, NavigableString):
99
+ append_text(builder, str(node))
100
+ return
101
+ if not isinstance(node, Tag):
102
+ return
103
+
104
+ before = builder.offset
105
+ for child in node.children:
106
+ append_inline(builder, child)
107
+ after = builder.offset
108
+ if after <= before:
109
+ return
110
+
111
+ tag_classes = classes(node)
112
+ styles: list[str] = []
113
+ if node.name in {"strong", "b"}:
114
+ styles.append("bold")
115
+ if node.name in {"em", "i"}:
116
+ styles.append("italic")
117
+ if node.name == "code":
118
+ styles.append("code")
119
+ if "positive" in tag_classes:
120
+ styles.append("positive")
121
+ if "negative" in tag_classes:
122
+ styles.append("negative")
123
+ if node.name == "a" and node.get("href"):
124
+ styles.append("link")
125
+
126
+ for style in styles:
127
+ entry: dict[str, Any] = {
128
+ "start": before,
129
+ "end": after,
130
+ "style": style,
131
+ "text": builder.text()[before:after],
132
+ }
133
+ if style == "link":
134
+ entry["url"] = node["href"]
135
+ builder.inline_styles.append(entry)
136
+
137
+
138
+ def add_paragraph(builder: Builder, node: Tag, block_type: str | None = None) -> None:
139
+ start = builder.offset
140
+ append_inline(builder, node)
141
+ end = builder.offset
142
+ if end > start:
143
+ builder.add("\n")
144
+ if block_type:
145
+ builder.blocks.append({"start": start, "end": end + 1, "type": block_type})
146
+
147
+
148
+ def add_heading(builder: Builder, node: Tag) -> None:
149
+ start = builder.offset
150
+ append_inline(builder, node)
151
+ end = builder.offset
152
+ level = int(node.name[1]) if node.name and node.name[1:].isdigit() else 2
153
+ builder.add("\n")
154
+ builder.headings.append(
155
+ {"start": start, "end": end, "level": level, "text": builder.text()[start:end]}
156
+ )
157
+
158
+
159
+ def cell_text_styles_and_links(
160
+ cell: Tag, first_column_bold: bool = False
161
+ ) -> tuple[str, list[str], list[dict[str, Any]]]:
162
+ local_builder = Builder()
163
+ append_inline(local_builder, cell)
164
+ raw_text = local_builder.text()
165
+ leading_trim = len(raw_text) - len(raw_text.lstrip())
166
+ text = raw_text.strip()
167
+ styles = []
168
+ if cell.find(["strong", "b"]) or cell.name == "th" or inline_style_is_bold(cell.get("style")):
169
+ styles.append("bold")
170
+ if first_column_bold and cell.name == "td" and "bold" not in styles:
171
+ styles.append("bold")
172
+ if cell.find("code"):
173
+ styles.append("code")
174
+ cell_classes = classes(cell)
175
+ if "positive" in cell_classes or "pos" in cell_classes or "strong" in cell_classes:
176
+ styles.append("positive")
177
+ if "negative" in cell_classes or "neg" in cell_classes or "weak" in cell_classes:
178
+ styles.append("negative")
179
+ if "neutral" in cell_classes:
180
+ styles.append("neutral")
181
+ links = []
182
+ for style in local_builder.inline_styles:
183
+ if style.get("style") != "link":
184
+ continue
185
+ start = max(0, style["start"] - leading_trim)
186
+ end = min(len(text), style["end"] - leading_trim)
187
+ while start < end and text[start].isspace():
188
+ start += 1
189
+ while end > start and text[end - 1].isspace():
190
+ end -= 1
191
+ if start >= end:
192
+ continue
193
+ links.append(
194
+ {
195
+ "start": start,
196
+ "end": end,
197
+ "url": style["url"],
198
+ "text": text[start:end],
199
+ }
200
+ )
201
+ return text, styles, links
202
+
203
+
204
+ def table_from_html(
205
+ node: Tag,
206
+ first_column_bold: bool = False,
207
+ ) -> tuple[list[list[str]], list[list[list[str]]], list[list[list[dict[str, Any]]]]]:
208
+ rows: list[list[str]] = []
209
+ styles: list[list[list[str]]] = []
210
+ links: list[list[list[dict[str, Any]]]] = []
211
+ for tr in node.find_all("tr"):
212
+ row: list[str] = []
213
+ style_row: list[list[str]] = []
214
+ link_row: list[list[dict[str, Any]]] = []
215
+ for cell_index, cell in enumerate(tr.find_all(["th", "td"], recursive=False)):
216
+ text, cell_styles, cell_links = cell_text_styles_and_links(
217
+ cell, first_column_bold=first_column_bold and cell_index == 0
218
+ )
219
+ row.append(text)
220
+ style_row.append(cell_styles)
221
+ link_row.append(cell_links)
222
+ if row:
223
+ rows.append(row)
224
+ styles.append(style_row)
225
+ links.append(link_row)
226
+ return rows, styles, links
227
+
228
+
229
+ def panel_has_only_heading_and_table(panel: Tag) -> bool:
230
+ """Return true when a two-col panel can be safely replaced by a table image."""
231
+
232
+ table = panel.find("table")
233
+ if not isinstance(table, Tag):
234
+ return False
235
+
236
+ table_text = collapse_ws(table.get_text(" ", strip=True)).strip()
237
+ for child in direct_tag_children(panel):
238
+ if child.name in {"h2", "h3", "h4", "h5", "h6"}:
239
+ continue
240
+ if child is table:
241
+ continue
242
+ if child.find("table"):
243
+ child_text = collapse_ws(child.get_text(" ", strip=True)).strip()
244
+ if child_text == table_text:
245
+ continue
246
+ if collapse_ws(child.get_text(" ", strip=True)).strip():
247
+ return False
248
+ return True
249
+
250
+
251
+ def two_col_can_render_as_table_grid(node: Tag) -> bool:
252
+ panels = direct_tag_children(node)
253
+ return bool(panels) and all(panel_has_only_heading_and_table(panel) for panel in panels)
254
+
255
+
256
+ def add_two_col_layout(builder: Builder, node: Tag) -> None:
257
+ panels: list[dict[str, Any]] = []
258
+ for child in node.find_all(recursive=False):
259
+ if not isinstance(child, Tag):
260
+ continue
261
+ table = child.find("table")
262
+ if not isinstance(table, Tag):
263
+ continue
264
+ title_node = child.find(["h2", "h3", "h4", "h5", "h6"])
265
+ title = collapse_ws(title_node.get_text(" ", strip=True) if title_node else "").strip()
266
+ rows, styles, _links = table_from_html(table, first_column_bold=False)
267
+ if rows:
268
+ panels.append({"title": title, "rows": rows, "cell_styles": styles})
269
+
270
+ if not panels:
271
+ return
272
+
273
+ builder.chart_counter += 1
274
+ chart_id = f"CHART_IMAGE_{builder.chart_counter:02d}"
275
+ placeholder = f"[[{chart_id}]]"
276
+ start, end = builder.add(placeholder)
277
+ builder.add("\n\n")
278
+ image_file = f"chart_images/{chart_id.lower()}.png"
279
+ builder.chart_images.append(
280
+ {
281
+ "id": chart_id,
282
+ "kind": "two_col_table_grid",
283
+ "panels": panels,
284
+ "placeholder": placeholder,
285
+ "placeholder_start": start,
286
+ "placeholder_end": end,
287
+ "image_file": image_file,
288
+ "docs_width_pt": DOC_CONTENT_WIDTH_PT,
289
+ }
290
+ )
291
+ builder.placeholders.append(
292
+ {
293
+ "id": chart_id,
294
+ "kind": "chart_image",
295
+ "text_to_find": placeholder,
296
+ "start": start,
297
+ "end": end,
298
+ }
299
+ )
300
+
301
+
302
+ def add_table_placeholder(
303
+ builder: Builder,
304
+ kind: str,
305
+ rows: list[list[str]],
306
+ styles: list[list[list[str]]] | None = None,
307
+ links: list[list[list[dict[str, Any]]]] | None = None,
308
+ bar_color: str | None = None,
309
+ ) -> None:
310
+ if not rows:
311
+ return
312
+ builder.table_counter += 1
313
+ table_id = f"{kind.upper()}_{builder.table_counter:02d}"
314
+ placeholder = f"[[{table_id}]]"
315
+ start, end = builder.add(placeholder)
316
+ builder.add("\n\n")
317
+ table = {
318
+ "id": table_id,
319
+ "kind": kind,
320
+ "rows": rows,
321
+ "cell_styles": styles or [[[] for _ in row] for row in rows],
322
+ "cell_links": links or [[[] for _ in row] for row in rows],
323
+ "placeholder": placeholder,
324
+ "placeholder_start": start,
325
+ "placeholder_end": end,
326
+ }
327
+ if bar_color:
328
+ table["bar_color"] = bar_color
329
+ builder.tables.append(table)
330
+ builder.placeholders.append(
331
+ {
332
+ "id": table_id,
333
+ "kind": kind,
334
+ "text_to_find": placeholder,
335
+ "start": start,
336
+ "end": end,
337
+ }
338
+ )
339
+
340
+
341
+ def first_text_for_selectors(node: Tag, selectors: list[str]) -> str:
342
+ for selector in selectors:
343
+ found = node.select_one(selector)
344
+ if isinstance(found, Tag):
345
+ text = collapse_ws(found.get_text(" ", strip=True)).strip()
346
+ if text:
347
+ return text
348
+ return ""
349
+
350
+
351
+ def metric_nodes(node: Tag) -> list[Tag]:
352
+ direct_metrics = direct_children_with_class(node, "metric")
353
+ if direct_metrics:
354
+ return direct_metrics
355
+ return [metric for metric in node.select(".metric") if isinstance(metric, Tag)]
356
+
357
+
358
+ def add_metric_row(builder: Builder, node: Tag) -> None:
359
+ row: list[str] = []
360
+ style_row: list[list[str]] = []
361
+ for metric in metric_nodes(node):
362
+ value = first_text_for_selectors(metric, [".value", ".metric-value", ".k", "strong", "b"])
363
+ label = first_text_for_selectors(metric, [".label", ".metric-label", ".l", "span"])
364
+ note = first_text_for_selectors(metric, [".note", ".metric-note", ".sub"])
365
+ pieces = [piece for piece in (value, label, note) if piece]
366
+ if not pieces:
367
+ pieces = [collapse_ws(metric.get_text(" ", strip=True)).strip()]
368
+ row.append("\n".join(piece for piece in pieces if piece))
369
+ style_row.append(["metric"])
370
+ if not row:
371
+ return
372
+ add_table_placeholder(builder, "metric_cards", [row], [style_row])
373
+
374
+
375
+ def add_pill_row(builder: Builder, node: Tag) -> None:
376
+ labels = [collapse_ws(pill.get_text(" ", strip=True)).strip() for pill in node.select(".pill")]
377
+ labels = [label for label in labels if label]
378
+ if not labels:
379
+ return
380
+ start, end = builder.add(" | ".join(labels))
381
+ builder.add("\n")
382
+ builder.blocks.append({"start": start, "end": end + 1, "type": "muted"})
383
+
384
+
385
+ def add_muted_text(builder: Builder, text: str) -> None:
386
+ text = collapse_ws(text).strip()
387
+ if not text:
388
+ return
389
+ start, end = builder.add(text)
390
+ builder.add("\n")
391
+ builder.blocks.append({"start": start, "end": end + 1, "type": "muted"})
392
+
393
+
394
+ def add_embedded_image(builder: Builder, img: Tag, caption: str = "") -> bool:
395
+ src = str(img.get("src") or "").strip()
396
+ if not src:
397
+ return False
398
+
399
+ builder.chart_counter += 1
400
+ chart_id = f"CHART_IMAGE_{builder.chart_counter:02d}"
401
+ placeholder = f"[[{chart_id}]]"
402
+ start, end = builder.add(placeholder)
403
+ builder.add("\n\n")
404
+ image_file = f"chart_images/{chart_id.lower()}.png"
405
+ builder.chart_images.append(
406
+ {
407
+ "id": chart_id,
408
+ "kind": "embedded_image",
409
+ "src": src,
410
+ "alt": collapse_ws(str(img.get("alt") or "")).strip(),
411
+ "caption": caption,
412
+ "placeholder": placeholder,
413
+ "placeholder_start": start,
414
+ "placeholder_end": end,
415
+ "image_file": image_file,
416
+ "docs_width_pt": DOC_CONTENT_WIDTH_PT,
417
+ }
418
+ )
419
+ builder.placeholders.append(
420
+ {
421
+ "id": chart_id,
422
+ "kind": "chart_image",
423
+ "text_to_find": placeholder,
424
+ "start": start,
425
+ "end": end,
426
+ }
427
+ )
428
+ return True
429
+
430
+
431
+ def figure_caption_text(node: Tag) -> str:
432
+ parts: list[str] = []
433
+ for selector in ["figcaption", ".caption", ".spark-axis"]:
434
+ for caption_node in node.select(selector):
435
+ if isinstance(caption_node, Tag):
436
+ text = collapse_ws(caption_node.get_text(" ", strip=True)).strip()
437
+ if text and text not in parts:
438
+ parts.append(text)
439
+ return " ".join(parts)
440
+
441
+
442
+ def add_figure(builder: Builder, node: Tag) -> bool:
443
+ img = node.find("img")
444
+ if isinstance(img, Tag):
445
+ caption = figure_caption_text(node)
446
+ added = add_embedded_image(builder, img, caption=caption)
447
+ if added and caption:
448
+ add_muted_text(builder, caption)
449
+ return added
450
+
451
+ svg = node.find("svg")
452
+ if isinstance(svg, Tag):
453
+ add_svg_chart(builder, svg)
454
+ caption = figure_caption_text(node)
455
+ if caption:
456
+ add_muted_text(builder, caption)
457
+ return True
458
+
459
+ return False
460
+
461
+
462
+ def css_bar_widths(stylesheet_text: str) -> dict[str, float]:
463
+ widths: dict[str, float] = {}
464
+ for selectors, body in CSS_RULE_RE.findall(stylesheet_text):
465
+ match = re.search(r"width\s*:\s*([0-9.]+)%", body)
466
+ if not match:
467
+ continue
468
+ pct = float(match.group(1))
469
+ for selector in selectors.split(","):
470
+ class_names = re.findall(r"\.([A-Za-z0-9_-]+)", selector)
471
+ if not class_names or not ({"bar", "fill", "bar-fill"} & set(class_names)):
472
+ continue
473
+ for class_name in class_names:
474
+ if class_name not in {"bar", "fill", "bar-fill"}:
475
+ widths[class_name] = pct
476
+ return widths
477
+
478
+
479
+ def bar_width_pct(fill: Tag | None, width_by_class: dict[str, float]) -> float:
480
+ if not isinstance(fill, Tag):
481
+ return 100.0
482
+ match = re.search(r"width\s*:\s*([0-9.]+)", str(fill.get("style", "")))
483
+ if match:
484
+ return float(match.group(1))
485
+ for class_name in classes(fill):
486
+ if class_name in width_by_class:
487
+ return width_by_class[class_name]
488
+ return 100.0
489
+
490
+
491
+ def add_bar_chart(builder: Builder, node: Tag, width_by_class: dict[str, float]) -> None:
492
+ chart_rows: list[dict[str, Any]] = []
493
+ bar_color = "blue" if node.select_one(".fill.blue, .bar-fill.blue, .bar.blue") else "accent"
494
+ for line in node.select(".bar-line, .bar-row"):
495
+ spans = line.find_all("span")
496
+ label_node = line.select_one(".bar-label, .label")
497
+ value_node = line.select_one(".bar-value, .value")
498
+ bar_head = line.select_one(".bar-head")
499
+ if isinstance(bar_head, Tag):
500
+ label_node = label_node or bar_head.find("span")
501
+ value_node = value_node or bar_head.find("strong") or bar_head.find("b")
502
+ direct_parts = [
503
+ child
504
+ for child in direct_tag_children(line)
505
+ if "bar-track" not in classes(child)
506
+ and not (classes(child) & {"bar", "fill", "bar-fill"})
507
+ and collapse_ws(child.get_text(" ", strip=True)).strip()
508
+ ]
509
+ if not isinstance(label_node, Tag) and spans:
510
+ label_node = spans[0]
511
+ if not isinstance(label_node, Tag) and direct_parts:
512
+ label_node = direct_parts[0]
513
+ if not isinstance(value_node, Tag) and len(spans) >= 2:
514
+ value_node = spans[-1]
515
+ if not isinstance(value_node, Tag) and len(direct_parts) >= 2:
516
+ value_node = direct_parts[-1]
517
+ if not isinstance(label_node, Tag) or not isinstance(value_node, Tag):
518
+ continue
519
+ label = collapse_ws(label_node.get_text(" ", strip=True))
520
+ value = collapse_ws(value_node.get_text(" ", strip=True))
521
+ fill = line.select_one(".fill, .bar-fill, .bar")
522
+ pct = bar_width_pct(fill, width_by_class)
523
+ chart_rows.append({"label": label, "value": value, "pct": pct})
524
+ if not chart_rows:
525
+ return
526
+
527
+ if builder.chart_mode == "table":
528
+ rows: list[list[str]] = []
529
+ style_rows: list[list[list[str]]] = []
530
+ for row in chart_rows:
531
+ blocks = max(1, round(row["pct"] / 5))
532
+ rows.append([row["label"], "█" * blocks, row["value"]])
533
+ style_rows.append([[], ["bar"], []])
534
+ add_table_placeholder(builder, "bar_chart", rows, style_rows, bar_color=bar_color)
535
+ return
536
+
537
+ builder.chart_counter += 1
538
+ chart_id = f"CHART_IMAGE_{builder.chart_counter:02d}"
539
+ placeholder = f"[[{chart_id}]]"
540
+ start, end = builder.add(placeholder)
541
+ builder.add("\n\n")
542
+ image_file = f"chart_images/{chart_id.lower()}.png"
543
+ builder.chart_images.append(
544
+ {
545
+ "id": chart_id,
546
+ "kind": "bar_chart",
547
+ "aria_label": node.get("aria-label", ""),
548
+ "rows": chart_rows,
549
+ "bar_color": bar_color,
550
+ "placeholder": placeholder,
551
+ "placeholder_start": start,
552
+ "placeholder_end": end,
553
+ "image_file": image_file,
554
+ }
555
+ )
556
+ builder.placeholders.append(
557
+ {
558
+ "id": chart_id,
559
+ "kind": "chart_image",
560
+ "text_to_find": placeholder,
561
+ "start": start,
562
+ "end": end,
563
+ }
564
+ )
565
+
566
+
567
+ def svg_dimensions(svg: Tag) -> tuple[int, int]:
568
+ width = svg.get("width")
569
+ height = svg.get("height")
570
+ try:
571
+ return int(float(str(width))), int(float(str(height)))
572
+ except (TypeError, ValueError):
573
+ pass
574
+ view_box = svg.get("viewBox") or svg.get("viewbox")
575
+ if view_box:
576
+ parts = str(view_box).replace(",", " ").split()
577
+ if len(parts) == 4:
578
+ try:
579
+ return int(float(parts[2])), int(float(parts[3]))
580
+ except ValueError:
581
+ pass
582
+ return 1000, 600
583
+
584
+
585
+ def inline_svg_report_styles(svg_markup: str) -> str:
586
+ """Apply report chart CSS classes as explicit SVG attributes.
587
+
588
+ Sanitized reports commonly keep chart colors in the page stylesheet while
589
+ SVG elements only carry classes. Once the SVG is extracted into the Docs
590
+ conversion plan, renderers that do not have the original stylesheet can
591
+ silently lose bars or labels. Inlining the known report chart classes keeps
592
+ the generated PNG visually faithful and renderer-independent.
593
+ """
594
+
595
+ soup = BeautifulSoup(svg_markup, "xml")
596
+ svg = soup.find("svg") or soup
597
+ for el in svg.find_all(True):
598
+ merged: dict[str, str] = {}
599
+ for class_name in classes(el):
600
+ merged.update(SVG_REPORT_STYLE_DEFAULTS.get(class_name, {}))
601
+ for attr, value in merged.items():
602
+ if el.get(attr) is None:
603
+ el[attr] = value
604
+ return str(svg)
605
+
606
+
607
+ def add_svg_chart(builder: Builder, node: Tag) -> None:
608
+ svg = node if node.name == "svg" else node.find("svg")
609
+ if not isinstance(svg, Tag):
610
+ return
611
+
612
+ builder.chart_counter += 1
613
+ chart_id = f"CHART_IMAGE_{builder.chart_counter:02d}"
614
+ placeholder = f"[[{chart_id}]]"
615
+ start, end = builder.add(placeholder)
616
+ builder.add("\n\n")
617
+
618
+ width_px, height_px = svg_dimensions(svg)
619
+ docs_height = round(DOC_CONTENT_WIDTH_PT * height_px / width_px, 1) if width_px else 240
620
+ image_file = f"chart_images/{chart_id.lower()}.png"
621
+ builder.chart_images.append(
622
+ {
623
+ "id": chart_id,
624
+ "kind": "svg",
625
+ "aria_label": svg.get("aria-label", node.get("aria-label", "")),
626
+ "svg_markup": inline_svg_report_styles(str(svg)),
627
+ "source_width_px": width_px,
628
+ "source_height_px": height_px,
629
+ "placeholder": placeholder,
630
+ "placeholder_start": start,
631
+ "placeholder_end": end,
632
+ "image_file": image_file,
633
+ "docs_width_pt": DOC_CONTENT_WIDTH_PT,
634
+ "docs_height_pt": docs_height,
635
+ }
636
+ )
637
+ builder.placeholders.append(
638
+ {
639
+ "id": chart_id,
640
+ "kind": "chart_image",
641
+ "text_to_find": placeholder,
642
+ "start": start,
643
+ "end": end,
644
+ }
645
+ )
646
+
647
+
648
+ def parse_html(path: Path, chart_mode: str = "image") -> dict[str, Any]:
649
+ require_bs4()
650
+ raw_html = path.read_text(encoding="utf-8")
651
+ soup = BeautifulSoup(raw_html, "html.parser")
652
+ root = soup.find("main") or soup.body or soup
653
+ builder = Builder(chart_mode=chart_mode)
654
+ stylesheet_text = "\n".join(style.get_text("\n") for style in soup.find_all("style"))
655
+ first_column_bold = table_first_column_is_bold(stylesheet_text)
656
+ bar_width_by_class = css_bar_widths(stylesheet_text)
657
+
658
+ blockish_tags = {"h1", "h2", "h3", "h4", "p", "table", "ul", "ol", "figure", "img", "svg"}
659
+ blockish_classes = {
660
+ "bar-chart",
661
+ "charts",
662
+ "callout",
663
+ "chart",
664
+ "figure",
665
+ "implication",
666
+ "bars",
667
+ "metric-grid",
668
+ "metrics",
669
+ "pillrow",
670
+ "summary",
671
+ "spark",
672
+ "two-col",
673
+ }
674
+
675
+ def has_blockish_child(node: Tag) -> bool:
676
+ for child in direct_tag_children(node):
677
+ if child.name in blockish_tags or classes(child) & blockish_classes:
678
+ return True
679
+ if node.find(list(blockish_tags)):
680
+ return True
681
+ return any(node.find(class_=class_name) for class_name in blockish_classes)
682
+
683
+ def process_children(node: Tag) -> None:
684
+ for child in node.children:
685
+ if isinstance(child, Tag):
686
+ process_node(child)
687
+
688
+ def add_summary_callout(builder: Builder, node: Tag) -> None:
689
+ start = builder.offset
690
+ append_inline(builder, node)
691
+ end = builder.offset
692
+ if end > start:
693
+ builder.add("\n")
694
+ builder.blocks.append({"start": start, "end": end + 1, "type": "summary_callout"})
695
+
696
+ def process_node(node: Tag) -> None:
697
+ node_classes = classes(node)
698
+ if "two-col" in node_classes:
699
+ if two_col_can_render_as_table_grid(node):
700
+ add_two_col_layout(builder, node)
701
+ else:
702
+ builder.two_col_text_blocks += 1
703
+ process_children(node)
704
+ return
705
+ if node.name in {"h1", "h2", "h3", "h4"}:
706
+ add_heading(builder, node)
707
+ return
708
+ if node.name == "figure" or "figure" in node_classes:
709
+ if add_figure(builder, node):
710
+ return
711
+ process_children(node)
712
+ return
713
+ if node.name == "img":
714
+ add_embedded_image(builder, node)
715
+ return
716
+ if node.name == "p":
717
+ if "note" in node_classes:
718
+ block_type = "note"
719
+ elif "source" in node_classes or "source-list" in node_classes:
720
+ block_type = "source_list"
721
+ elif "sub" in node_classes:
722
+ block_type = "muted"
723
+ else:
724
+ block_type = None
725
+ add_paragraph(builder, node, block_type)
726
+ return
727
+ if (
728
+ "metric-grid" in node_classes
729
+ or "metrics" in node_classes
730
+ or direct_children_with_class(node, "metric")
731
+ ):
732
+ add_metric_row(builder, node)
733
+ return
734
+ if node.name == "table":
735
+ rows, styles, links = table_from_html(node, first_column_bold=first_column_bold)
736
+ add_table_placeholder(builder, "table", rows, styles, links)
737
+ return
738
+ if node.name in {"ul", "ol"}:
739
+ start = builder.offset
740
+ for li in node.find_all("li", recursive=False):
741
+ li_start = builder.offset
742
+ append_inline(builder, li)
743
+ if builder.offset > li_start:
744
+ builder.add("\n")
745
+ end = builder.offset
746
+ builder.add("\n")
747
+ if end > start:
748
+ builder.lists.append({"start": start, "end": end, "ordered": node.name == "ol"})
749
+ return
750
+ child_tags = direct_tag_children(node)
751
+ if "pillrow" in node_classes or (
752
+ child_tags and all("pill" in classes(child) for child in child_tags)
753
+ ):
754
+ add_pill_row(builder, node)
755
+ return
756
+ if "callout" in node_classes or "implication" in node_classes:
757
+ add_summary_callout(builder, node)
758
+ return
759
+ if "bar-chart" in node_classes or "bars" in node_classes:
760
+ add_bar_chart(builder, node, bar_width_by_class)
761
+ return
762
+ if "chart" in node_classes and node.find("svg"):
763
+ add_svg_chart(builder, node)
764
+ return
765
+ if node.name == "svg":
766
+ add_svg_chart(builder, node)
767
+ return
768
+
769
+ if not has_blockish_child(node) and collapse_ws(node.get_text(" ", strip=True)).strip():
770
+ add_paragraph(builder, node)
771
+ return
772
+ process_children(node)
773
+
774
+ for child in root.children:
775
+ if isinstance(child, Tag):
776
+ process_node(child)
777
+
778
+ title = (soup.title.get_text(" ", strip=True) if soup.title else "").strip()
779
+ h1 = root.find("h1")
780
+ if h1:
781
+ title = collapse_ws(h1.get_text(" ", strip=True)).strip() or title
782
+ two_col_blocks = root.select(".two-col")
783
+ bar_blocks = [block for block in root.select(".bar-chart, .bars") if block.name != "svg"]
784
+
785
+ return {
786
+ "source_html": str(path),
787
+ "title": title,
788
+ "skeleton_text": builder.text().rstrip() + "\n",
789
+ "headings": builder.headings,
790
+ "inline_styles": builder.inline_styles,
791
+ "blocks": builder.blocks,
792
+ "lists": builder.lists,
793
+ "placeholders": builder.placeholders,
794
+ "tables": builder.tables,
795
+ "chart_images": builder.chart_images,
796
+ "counts": {
797
+ "headings": len(builder.headings),
798
+ "inline_styles": len(builder.inline_styles),
799
+ "blocks": len(builder.blocks),
800
+ "lists": len(builder.lists),
801
+ "tables": len(builder.tables),
802
+ "chart_images": len(builder.chart_images),
803
+ "two_col_source_blocks": len(two_col_blocks),
804
+ "two_col_image_blocks": sum(
805
+ 1 for chart in builder.chart_images if chart.get("kind") == "two_col_table_grid"
806
+ ),
807
+ "two_col_text_blocks": builder.two_col_text_blocks,
808
+ "embedded_image_blocks": sum(
809
+ 1 for chart in builder.chart_images if chart.get("kind") == "embedded_image"
810
+ ),
811
+ "svg_image_blocks": sum(
812
+ 1 for chart in builder.chart_images if chart.get("kind") == "svg"
813
+ ),
814
+ "bar_source_blocks": len(bar_blocks),
815
+ "bar_image_blocks": sum(
816
+ 1 for chart in builder.chart_images if chart.get("kind") == "bar_chart"
817
+ ),
818
+ "bar_table_blocks": sum(
819
+ 1 for table in builder.tables if table.get("kind") == "bar_chart"
820
+ ),
821
+ "images": len(root.find_all("img")),
822
+ "svgs": len(root.find_all("svg")),
823
+ "canvas": len(root.find_all("canvas")),
824
+ },
825
+ "source_flags": {
826
+ "auth_page_detected": "login.microsoftonline.com" in raw_html
827
+ or title.strip().lower() == "sign in to your account"
828
+ },
829
+ }