create-yss-spec 3.5.0 → 3.5.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (882) hide show
  1. package/README.md +6 -2
  2. package/package.json +1 -1
  3. package/src/api/_sync-service.js +31 -6
  4. package/src/cli/args.js +2 -0
  5. package/src/cli/help.js +4 -1
  6. package/src/commands/attach.js +9 -3
  7. package/src/commands/diff.js +1 -0
  8. package/src/commands/doctor.js +1 -0
  9. package/src/commands/init.js +2 -1
  10. package/src/commands/sync.js +1 -0
  11. package/src/family-identity.js +1 -1
  12. package/src/template/asset-runtime.js +59 -49
  13. package/src/template/attach-planner.js +1 -1
  14. package/src/template/distribution-runtime.js +9 -9
  15. package/src/template/instance-runtime.js +9 -9
  16. package/src/template/layout-migration.js +160 -0
  17. package/src/template/migration-runtime.js +2 -2
  18. package/src/template/sync-planner.js +1 -1
  19. package/template/.agents/rules/yss-ai-skills.md +1 -1
  20. package/template/.agents/skills/.strategic-design-skills-manifest.json +7 -8
  21. package/template/.agents/skills/.yss-skills-manifest.json +2 -2
  22. package/template/.agents/skills/alibaba-java-code-style/SKILL.md +1 -1
  23. package/template/.agents/skills/code-review/SKILL.md +6 -6
  24. package/template/.agents/skills/code-review/references/yss-review-standards.md +1 -1
  25. package/template/.agents/skills/codebase-design/SKILL.md +4 -0
  26. package/template/{.codex/skills/improve-codebase-architecture/SKILL.md → .agents/skills/codebase-design/references/architecture-audit.md} +3 -9
  27. package/template/.agents/skills/cross-repo-implementation-routing/SKILL.md +2 -2
  28. package/template/.agents/skills/frontend-commit/SKILL.md +4 -93
  29. package/template/.agents/skills/git-commit-core/SKILL.md +18 -0
  30. package/template/.agents/skills/i-have-adhd/SKILL.md +1 -1
  31. package/template/.agents/skills/i-have-adhd/references/yss-adaptation.md +1 -1
  32. package/template/.agents/skills/i-have-adhd/tests/verify-integration.mjs +2 -2
  33. package/template/.agents/skills/implementation-repo-onboarding/SKILL.md +2 -2
  34. package/template/.agents/skills/java-backend-commit/SKILL.md +4 -96
  35. package/template/.agents/skills/llm-wiki/SKILL.md +12 -13
  36. package/template/.agents/skills/llm-wiki/assets/CLAUDE.md.template +7 -1
  37. package/template/.agents/skills/llm-wiki/references/compile.md +21 -114
  38. package/template/.agents/skills/llm-wiki/references/discover.md +3 -3
  39. package/template/.agents/skills/llm-wiki/references/ingest.md +8 -33
  40. package/template/.agents/skills/llm-wiki/references/lint.md +9 -46
  41. package/template/.agents/skills/llm-wiki/references/query.md +9 -18
  42. package/template/.agents/skills/llm-wiki/references/schema.md +56 -29
  43. package/template/.agents/skills/llm-wiki/references/transactions.md +48 -0
  44. package/template/.agents/skills/llm-wiki/references/writing.md +4 -4
  45. package/template/.agents/skills/llm-wiki/scripts/advise.mjs +46 -58
  46. package/template/.agents/skills/llm-wiki/scripts/advise.test.mjs +11 -16
  47. package/template/.agents/skills/llm-wiki/scripts/core.mjs +262 -0
  48. package/template/.agents/skills/llm-wiki/scripts/extract.mjs +35 -18
  49. package/template/.agents/skills/llm-wiki/scripts/extract.test.mjs +5 -8
  50. package/template/.agents/skills/llm-wiki/scripts/feedback.mjs +51 -0
  51. package/template/.agents/skills/llm-wiki/scripts/inventory.mjs +43 -171
  52. package/template/.agents/skills/llm-wiki/scripts/inventory.test.mjs +25 -18
  53. package/template/.agents/skills/llm-wiki/scripts/lint-wikilinks.mjs +69 -92
  54. package/template/.agents/skills/llm-wiki/scripts/lint-wikilinks.test.mjs +25 -37
  55. package/template/.agents/skills/llm-wiki/scripts/migrate.mjs +51 -0
  56. package/template/.agents/skills/llm-wiki/scripts/query.mjs +149 -0
  57. package/template/.agents/skills/llm-wiki/scripts/regressions.test.mjs +101 -0
  58. package/template/.agents/skills/llm-wiki/scripts/sources.mjs +166 -0
  59. package/template/.agents/skills/llm-wiki/scripts/transaction.mjs +384 -0
  60. package/template/.agents/skills/llm-wiki/scripts/v2.test.mjs +354 -0
  61. package/template/.agents/skills/prototype/LOGIC.md +1 -1
  62. package/template/.agents/skills/prototype/SKILL.md +5 -19
  63. package/template/.agents/skills/prototype-review/SKILL.md +4 -4
  64. package/template/.agents/skills/setup-matt-pocock-skills/SKILL.md +11 -11
  65. package/template/.agents/skills/triage/SKILL.md +1 -1
  66. package/template/.agents/skills/yss-application/references/profiles/layered-mvc-service.md +1 -1
  67. package/template/.agents/skills/yss-application/references/profiles/mvc-data-analysis-v1.md +1 -1
  68. package/template/.agents/skills/yss-backend-spec-review/SKILL.md +1 -1
  69. package/template/.agents/skills/yss-backend-spec-review/references/check-inputs.md +1 -1
  70. package/template/.agents/skills/yss-ddd-scaffold-generator/SKILL.md +2 -2
  71. package/template/.agents/skills/yss-ddd-scaffold-generator/scripts/generate_scaffold.mjs +1 -1
  72. package/template/.agents/skills/yss-design-system/SKILL.md +10 -10
  73. package/template/.agents/skills/yss-design-system/references/design-qa-theme.md +3 -3
  74. package/template/.agents/skills/yss-design-system/references/design-system.md +3 -3
  75. package/template/.agents/skills/yss-exception/SKILL.md +1 -1
  76. package/template/.agents/skills/yss-frontend-scaffold-generator/SKILL.md +2 -2
  77. package/template/.agents/skills/yss-frontend-scaffold-generator/scripts/generate_scaffold.mjs +1 -1
  78. package/template/.agents/skills/yss-frontend-scaffold-generator/scripts/refresh_baseline.mjs +2 -2
  79. package/template/.agents/skills/yss-implementation-contract-compiler/SKILL.md +11 -9
  80. package/template/.agents/skills/yss-implementation-contract-compiler/references/boundaries.md +3 -4
  81. package/template/.agents/skills/yss-implementation-contract-compiler/references/compiler-contract.yaml +9 -9
  82. package/template/.agents/skills/yss-implementation-contract-compiler/references/slice-implementation-contract-v2.md +3 -3
  83. package/template/.agents/skills/yss-implementation-contract-compiler/references/slice-implementation-contract.md +1 -1
  84. package/template/.agents/skills/yss-implementation-contract-compiler/references/strategic-handoff-routing.md +1 -1
  85. package/template/.agents/skills/yss-implementation-contract-compiler/references/yss-skill-execution-result.md +14 -0
  86. package/template/.agents/skills/yss-layered-mvc-scaffold-generator/SKILL.md +2 -2
  87. package/template/.agents/skills/yss-layered-mvc-scaffold-generator/scripts/data-analysis-profile.mjs +5 -5
  88. package/template/.agents/skills/yss-layered-mvc-scaffold-generator/scripts/generate_scaffold.mjs +2 -2
  89. package/template/.agents/skills/yss-layered-mvc-scaffold-generator/scripts/scaffold-generator.test.mjs +42 -0
  90. package/template/.agents/skills/yss-openapi-draft-review/SKILL.md +2 -2
  91. package/template/.agents/skills/yss-openapi-governance/SKILL.md +7 -7
  92. package/template/.agents/skills/yss-product-lifecycle/SKILL.md +17 -17
  93. package/template/.agents/skills/yss-product-lifecycle/references/external-input-questionnaire.md +19 -0
  94. package/template/.agents/skills/yss-product-lifecycle/references/matt-yss-adapter.md +10 -10
  95. package/template/.agents/skills/yss-product-lifecycle/references/orchestration-contract.yaml +85 -47
  96. package/template/.agents/skills/yss-product-lifecycle/references/orchestration.md +24 -16
  97. package/template/.agents/skills/yss-product-lifecycle/references/plan-requirements.md +10 -0
  98. package/template/.agents/skills/yss-product-lifecycle/references/request-triage.md +2 -0
  99. package/template/.agents/skills/yss-product-lifecycle/references/state-model.md +4 -4
  100. package/template/.agents/skills/yss-product-lifecycle/references/user-decisions.md +2 -2
  101. package/template/.agents/skills/yss-prototype-stage/SKILL.md +7 -7
  102. package/template/.agents/skills/yss-prototype-stage/references/existing-ui-entry.md +1 -1
  103. package/template/.agents/skills/yss-prototype-stage/scripts/build-antd-prototype.mjs +1 -1
  104. package/template/.agents/skills/yss-prototype-stage/scripts/offline-html.mjs +3 -3
  105. package/template/.agents/skills/yss-prototype-stage/tests/browser-smoke.mjs +2 -2
  106. package/template/.agents/skills/yss-prototype-stage/tests/run-scenarios.mjs +4 -4
  107. package/template/.agents/skills/yss-prototype-stage/tests/workbench-smoke.mjs +1 -1
  108. package/template/.agents/skills/yss-repository/SKILL.md +2 -2
  109. package/template/.agents/skills/yss-repository/references/profiles/layered-mvc-service.md +1 -1
  110. package/template/.agents/skills/yss-repository/references/profiles/mvc-data-analysis-v1.md +1 -1
  111. package/template/.agents/skills/yss-repository/tests/profile-routing.test.mjs +2 -2
  112. package/template/.agents/skills/yss-research/SKILL.md +6 -2
  113. package/template/.agents/skills/yss-skill-source-index-refresh/references/backend-component-platform-compatibility.md +1 -1
  114. package/template/.agents/skills/yss-stage-decision/SKILL.md +5 -5
  115. package/template/.agents/skills/yss-stage-decision/references/domain-strategy-contract.md +1 -1
  116. package/template/.agents/skills/yss-stage-decision/references/strategic-handoff-routing.md +1 -1
  117. package/template/.agents/skills/yss-tactical-design/SKILL.md +2 -2
  118. package/template/.agents/skills/yss-tactical-design/references/strategic-handoff-routing.md +1 -1
  119. package/template/.agents/skills/yss-tactical-design/references/tactical-design-contract.md +1 -1
  120. package/template/.agents/skills/yss-technical-design/SKILL.md +6 -6
  121. package/template/.agents/skills/yss-technical-design/references/technical-design-contract.md +1 -1
  122. package/template/.agents/skills/yss-ui/SKILL.md +1 -1
  123. package/template/.agents/skills/yss-up-springboot3/SKILL.md +1 -1
  124. package/template/.agents/skills/yss-web-controller/SKILL.md +1 -1
  125. package/template/.agents/skills/yss-web-controller/references/profiles/layered-mvc-service.md +1 -1
  126. package/template/.codex/skills/alibaba-java-code-style/SKILL.md +1 -1
  127. package/template/.codex/skills/code-review/SKILL.md +6 -6
  128. package/template/.codex/skills/code-review/references/yss-review-standards.md +1 -1
  129. package/template/.codex/skills/codebase-design/SKILL.md +4 -0
  130. package/template/{.pi/skills/improve-codebase-architecture/SKILL.md → .codex/skills/codebase-design/references/architecture-audit.md} +3 -9
  131. package/template/.codex/skills/cross-repo-implementation-routing/SKILL.md +2 -2
  132. package/template/.codex/skills/frontend-commit/SKILL.md +4 -93
  133. package/template/.codex/skills/git-commit-core/SKILL.md +18 -0
  134. package/template/.codex/skills/i-have-adhd/SKILL.md +1 -1
  135. package/template/.codex/skills/i-have-adhd/references/yss-adaptation.md +1 -1
  136. package/template/.codex/skills/i-have-adhd/tests/verify-integration.mjs +2 -2
  137. package/template/.codex/skills/implementation-repo-onboarding/SKILL.md +2 -2
  138. package/template/.codex/skills/java-backend-commit/SKILL.md +4 -96
  139. package/template/.codex/skills/llm-wiki/SKILL.md +12 -13
  140. package/template/.codex/skills/llm-wiki/assets/CLAUDE.md.template +7 -1
  141. package/template/.codex/skills/llm-wiki/references/compile.md +21 -114
  142. package/template/.codex/skills/llm-wiki/references/discover.md +3 -3
  143. package/template/.codex/skills/llm-wiki/references/ingest.md +8 -33
  144. package/template/.codex/skills/llm-wiki/references/lint.md +9 -46
  145. package/template/.codex/skills/llm-wiki/references/query.md +9 -18
  146. package/template/.codex/skills/llm-wiki/references/schema.md +56 -29
  147. package/template/.codex/skills/llm-wiki/references/transactions.md +48 -0
  148. package/template/.codex/skills/llm-wiki/references/writing.md +4 -4
  149. package/template/.codex/skills/llm-wiki/scripts/advise.mjs +46 -58
  150. package/template/.codex/skills/llm-wiki/scripts/advise.test.mjs +11 -16
  151. package/template/.codex/skills/llm-wiki/scripts/core.mjs +262 -0
  152. package/template/.codex/skills/llm-wiki/scripts/extract.mjs +35 -18
  153. package/template/.codex/skills/llm-wiki/scripts/extract.test.mjs +5 -8
  154. package/template/.codex/skills/llm-wiki/scripts/feedback.mjs +51 -0
  155. package/template/.codex/skills/llm-wiki/scripts/inventory.mjs +43 -171
  156. package/template/.codex/skills/llm-wiki/scripts/inventory.test.mjs +25 -18
  157. package/template/.codex/skills/llm-wiki/scripts/lint-wikilinks.mjs +69 -92
  158. package/template/.codex/skills/llm-wiki/scripts/lint-wikilinks.test.mjs +25 -37
  159. package/template/.codex/skills/llm-wiki/scripts/migrate.mjs +51 -0
  160. package/template/.codex/skills/llm-wiki/scripts/query.mjs +149 -0
  161. package/template/.codex/skills/llm-wiki/scripts/regressions.test.mjs +101 -0
  162. package/template/.codex/skills/llm-wiki/scripts/sources.mjs +166 -0
  163. package/template/.codex/skills/llm-wiki/scripts/transaction.mjs +384 -0
  164. package/template/.codex/skills/llm-wiki/scripts/v2.test.mjs +354 -0
  165. package/template/.codex/skills/prototype/LOGIC.md +1 -1
  166. package/template/.codex/skills/prototype/SKILL.md +5 -19
  167. package/template/.codex/skills/prototype-review/SKILL.md +4 -4
  168. package/template/.codex/skills/setup-matt-pocock-skills/SKILL.md +11 -11
  169. package/template/.codex/skills/triage/SKILL.md +1 -1
  170. package/template/.codex/skills/yss-application/references/profiles/layered-mvc-service.md +1 -1
  171. package/template/.codex/skills/yss-application/references/profiles/mvc-data-analysis-v1.md +1 -1
  172. package/template/.codex/skills/yss-backend-spec-review/SKILL.md +1 -1
  173. package/template/.codex/skills/yss-backend-spec-review/references/check-inputs.md +1 -1
  174. package/template/.codex/skills/yss-ddd-scaffold-generator/SKILL.md +2 -2
  175. package/template/.codex/skills/yss-ddd-scaffold-generator/scripts/generate_scaffold.mjs +1 -1
  176. package/template/.codex/skills/yss-design-system/SKILL.md +10 -10
  177. package/template/.codex/skills/yss-design-system/references/design-qa-theme.md +3 -3
  178. package/template/.codex/skills/yss-design-system/references/design-system.md +3 -3
  179. package/template/.codex/skills/yss-exception/SKILL.md +1 -1
  180. package/template/.codex/skills/yss-frontend-scaffold-generator/SKILL.md +2 -2
  181. package/template/.codex/skills/yss-frontend-scaffold-generator/scripts/generate_scaffold.mjs +1 -1
  182. package/template/.codex/skills/yss-frontend-scaffold-generator/scripts/refresh_baseline.mjs +2 -2
  183. package/template/.codex/skills/yss-implementation-contract-compiler/SKILL.md +11 -9
  184. package/template/.codex/skills/yss-implementation-contract-compiler/references/boundaries.md +3 -4
  185. package/template/.codex/skills/yss-implementation-contract-compiler/references/compiler-contract.yaml +9 -9
  186. package/template/.codex/skills/yss-implementation-contract-compiler/references/slice-implementation-contract-v2.md +3 -3
  187. package/template/.codex/skills/yss-implementation-contract-compiler/references/slice-implementation-contract.md +1 -1
  188. package/template/.codex/skills/yss-implementation-contract-compiler/references/strategic-handoff-routing.md +1 -1
  189. package/template/.codex/skills/yss-implementation-contract-compiler/references/yss-skill-execution-result.md +14 -0
  190. package/template/.codex/skills/yss-layered-mvc-scaffold-generator/SKILL.md +2 -2
  191. package/template/.codex/skills/yss-layered-mvc-scaffold-generator/scripts/data-analysis-profile.mjs +5 -5
  192. package/template/.codex/skills/yss-layered-mvc-scaffold-generator/scripts/generate_scaffold.mjs +2 -2
  193. package/template/.codex/skills/yss-layered-mvc-scaffold-generator/scripts/scaffold-generator.test.mjs +42 -0
  194. package/template/.codex/skills/yss-openapi-draft-review/SKILL.md +2 -2
  195. package/template/.codex/skills/yss-openapi-governance/SKILL.md +7 -7
  196. package/template/.codex/skills/yss-product-lifecycle/SKILL.md +17 -17
  197. package/template/.codex/skills/yss-product-lifecycle/references/external-input-questionnaire.md +19 -0
  198. package/template/.codex/skills/yss-product-lifecycle/references/matt-yss-adapter.md +10 -10
  199. package/template/.codex/skills/yss-product-lifecycle/references/orchestration-contract.yaml +85 -47
  200. package/template/.codex/skills/yss-product-lifecycle/references/orchestration.md +24 -16
  201. package/template/.codex/skills/yss-product-lifecycle/references/plan-requirements.md +10 -0
  202. package/template/.codex/skills/yss-product-lifecycle/references/request-triage.md +2 -0
  203. package/template/.codex/skills/yss-product-lifecycle/references/state-model.md +4 -4
  204. package/template/.codex/skills/yss-product-lifecycle/references/user-decisions.md +2 -2
  205. package/template/.codex/skills/yss-prototype-stage/SKILL.md +7 -7
  206. package/template/.codex/skills/yss-prototype-stage/references/existing-ui-entry.md +1 -1
  207. package/template/.codex/skills/yss-prototype-stage/scripts/build-antd-prototype.mjs +1 -1
  208. package/template/.codex/skills/yss-prototype-stage/scripts/offline-html.mjs +3 -3
  209. package/template/.codex/skills/yss-prototype-stage/tests/browser-smoke.mjs +2 -2
  210. package/template/.codex/skills/yss-prototype-stage/tests/run-scenarios.mjs +4 -4
  211. package/template/.codex/skills/yss-prototype-stage/tests/workbench-smoke.mjs +1 -1
  212. package/template/.codex/skills/yss-repository/SKILL.md +2 -2
  213. package/template/.codex/skills/yss-repository/references/profiles/layered-mvc-service.md +1 -1
  214. package/template/.codex/skills/yss-repository/references/profiles/mvc-data-analysis-v1.md +1 -1
  215. package/template/.codex/skills/yss-repository/tests/profile-routing.test.mjs +2 -2
  216. package/template/.codex/skills/yss-research/SKILL.md +6 -2
  217. package/template/.codex/skills/yss-skill-source-index-refresh/references/backend-component-platform-compatibility.md +1 -1
  218. package/template/.codex/skills/yss-stage-decision/SKILL.md +5 -5
  219. package/template/.codex/skills/yss-stage-decision/references/domain-strategy-contract.md +1 -1
  220. package/template/.codex/skills/yss-stage-decision/references/strategic-handoff-routing.md +1 -1
  221. package/template/.codex/skills/yss-tactical-design/SKILL.md +2 -2
  222. package/template/.codex/skills/yss-tactical-design/references/strategic-handoff-routing.md +1 -1
  223. package/template/.codex/skills/yss-tactical-design/references/tactical-design-contract.md +1 -1
  224. package/template/.codex/skills/yss-technical-design/SKILL.md +6 -6
  225. package/template/.codex/skills/yss-technical-design/references/technical-design-contract.md +1 -1
  226. package/template/.codex/skills/yss-ui/SKILL.md +1 -1
  227. package/template/.codex/skills/yss-up-springboot3/SKILL.md +1 -1
  228. package/template/.codex/skills/yss-web-controller/SKILL.md +1 -1
  229. package/template/.codex/skills/yss-web-controller/references/profiles/layered-mvc-service.md +1 -1
  230. package/template/.cursor/skills/alibaba-java-code-style/SKILL.md +1 -1
  231. package/template/.cursor/skills/code-review/SKILL.md +6 -6
  232. package/template/.cursor/skills/code-review/references/yss-review-standards.md +1 -1
  233. package/template/.cursor/skills/codebase-design/SKILL.md +4 -0
  234. package/template/.cursor/skills/{improve-codebase-architecture/SKILL.md → codebase-design/references/architecture-audit.md} +3 -9
  235. package/template/.cursor/skills/cross-repo-implementation-routing/SKILL.md +2 -2
  236. package/template/.cursor/skills/frontend-commit/SKILL.md +4 -93
  237. package/template/.cursor/skills/git-commit-core/SKILL.md +18 -0
  238. package/template/.cursor/skills/i-have-adhd/SKILL.md +1 -1
  239. package/template/.cursor/skills/i-have-adhd/references/yss-adaptation.md +1 -1
  240. package/template/.cursor/skills/i-have-adhd/tests/verify-integration.mjs +2 -2
  241. package/template/.cursor/skills/implementation-repo-onboarding/SKILL.md +2 -2
  242. package/template/.cursor/skills/java-backend-commit/SKILL.md +4 -96
  243. package/template/.cursor/skills/llm-wiki/SKILL.md +12 -13
  244. package/template/.cursor/skills/llm-wiki/assets/CLAUDE.md.template +7 -1
  245. package/template/.cursor/skills/llm-wiki/references/compile.md +21 -114
  246. package/template/.cursor/skills/llm-wiki/references/discover.md +3 -3
  247. package/template/.cursor/skills/llm-wiki/references/ingest.md +8 -33
  248. package/template/.cursor/skills/llm-wiki/references/lint.md +9 -46
  249. package/template/.cursor/skills/llm-wiki/references/query.md +9 -18
  250. package/template/.cursor/skills/llm-wiki/references/schema.md +56 -29
  251. package/template/.cursor/skills/llm-wiki/references/transactions.md +48 -0
  252. package/template/.cursor/skills/llm-wiki/references/writing.md +4 -4
  253. package/template/.cursor/skills/llm-wiki/scripts/advise.mjs +46 -58
  254. package/template/.cursor/skills/llm-wiki/scripts/advise.test.mjs +11 -16
  255. package/template/.cursor/skills/llm-wiki/scripts/core.mjs +262 -0
  256. package/template/.cursor/skills/llm-wiki/scripts/extract.mjs +35 -18
  257. package/template/.cursor/skills/llm-wiki/scripts/extract.test.mjs +5 -8
  258. package/template/.cursor/skills/llm-wiki/scripts/feedback.mjs +51 -0
  259. package/template/.cursor/skills/llm-wiki/scripts/inventory.mjs +43 -171
  260. package/template/.cursor/skills/llm-wiki/scripts/inventory.test.mjs +25 -18
  261. package/template/.cursor/skills/llm-wiki/scripts/lint-wikilinks.mjs +69 -92
  262. package/template/.cursor/skills/llm-wiki/scripts/lint-wikilinks.test.mjs +25 -37
  263. package/template/.cursor/skills/llm-wiki/scripts/migrate.mjs +51 -0
  264. package/template/.cursor/skills/llm-wiki/scripts/query.mjs +149 -0
  265. package/template/.cursor/skills/llm-wiki/scripts/regressions.test.mjs +101 -0
  266. package/template/.cursor/skills/llm-wiki/scripts/sources.mjs +166 -0
  267. package/template/.cursor/skills/llm-wiki/scripts/transaction.mjs +384 -0
  268. package/template/.cursor/skills/llm-wiki/scripts/v2.test.mjs +354 -0
  269. package/template/.cursor/skills/prototype/LOGIC.md +1 -1
  270. package/template/.cursor/skills/prototype/SKILL.md +5 -19
  271. package/template/.cursor/skills/prototype-review/SKILL.md +4 -4
  272. package/template/.cursor/skills/setup-matt-pocock-skills/SKILL.md +11 -11
  273. package/template/.cursor/skills/triage/SKILL.md +1 -1
  274. package/template/.cursor/skills/yss-application/references/profiles/layered-mvc-service.md +1 -1
  275. package/template/.cursor/skills/yss-application/references/profiles/mvc-data-analysis-v1.md +1 -1
  276. package/template/.cursor/skills/yss-backend-spec-review/SKILL.md +1 -1
  277. package/template/.cursor/skills/yss-backend-spec-review/references/check-inputs.md +1 -1
  278. package/template/.cursor/skills/yss-ddd-scaffold-generator/SKILL.md +2 -2
  279. package/template/.cursor/skills/yss-ddd-scaffold-generator/scripts/generate_scaffold.mjs +1 -1
  280. package/template/.cursor/skills/yss-design-system/SKILL.md +10 -10
  281. package/template/.cursor/skills/yss-design-system/references/design-qa-theme.md +3 -3
  282. package/template/.cursor/skills/yss-design-system/references/design-system.md +3 -3
  283. package/template/.cursor/skills/yss-exception/SKILL.md +1 -1
  284. package/template/.cursor/skills/yss-frontend-scaffold-generator/SKILL.md +2 -2
  285. package/template/.cursor/skills/yss-frontend-scaffold-generator/scripts/generate_scaffold.mjs +1 -1
  286. package/template/.cursor/skills/yss-frontend-scaffold-generator/scripts/refresh_baseline.mjs +2 -2
  287. package/template/.cursor/skills/yss-implementation-contract-compiler/SKILL.md +11 -9
  288. package/template/.cursor/skills/yss-implementation-contract-compiler/references/boundaries.md +3 -4
  289. package/template/.cursor/skills/yss-implementation-contract-compiler/references/compiler-contract.yaml +9 -9
  290. package/template/.cursor/skills/yss-implementation-contract-compiler/references/slice-implementation-contract-v2.md +3 -3
  291. package/template/.cursor/skills/yss-implementation-contract-compiler/references/slice-implementation-contract.md +1 -1
  292. package/template/.cursor/skills/yss-implementation-contract-compiler/references/strategic-handoff-routing.md +1 -1
  293. package/template/.cursor/skills/yss-implementation-contract-compiler/references/yss-skill-execution-result.md +14 -0
  294. package/template/.cursor/skills/yss-layered-mvc-scaffold-generator/SKILL.md +2 -2
  295. package/template/.cursor/skills/yss-layered-mvc-scaffold-generator/scripts/data-analysis-profile.mjs +5 -5
  296. package/template/.cursor/skills/yss-layered-mvc-scaffold-generator/scripts/generate_scaffold.mjs +2 -2
  297. package/template/.cursor/skills/yss-layered-mvc-scaffold-generator/scripts/scaffold-generator.test.mjs +42 -0
  298. package/template/.cursor/skills/yss-openapi-draft-review/SKILL.md +2 -2
  299. package/template/.cursor/skills/yss-openapi-governance/SKILL.md +7 -7
  300. package/template/.cursor/skills/yss-product-lifecycle/SKILL.md +17 -17
  301. package/template/.cursor/skills/yss-product-lifecycle/references/external-input-questionnaire.md +19 -0
  302. package/template/.cursor/skills/yss-product-lifecycle/references/matt-yss-adapter.md +10 -10
  303. package/template/.cursor/skills/yss-product-lifecycle/references/orchestration-contract.yaml +85 -47
  304. package/template/.cursor/skills/yss-product-lifecycle/references/orchestration.md +24 -16
  305. package/template/.cursor/skills/yss-product-lifecycle/references/plan-requirements.md +10 -0
  306. package/template/.cursor/skills/yss-product-lifecycle/references/request-triage.md +2 -0
  307. package/template/.cursor/skills/yss-product-lifecycle/references/state-model.md +4 -4
  308. package/template/.cursor/skills/yss-product-lifecycle/references/user-decisions.md +2 -2
  309. package/template/.cursor/skills/yss-prototype-stage/SKILL.md +7 -7
  310. package/template/.cursor/skills/yss-prototype-stage/references/existing-ui-entry.md +1 -1
  311. package/template/.cursor/skills/yss-prototype-stage/scripts/build-antd-prototype.mjs +1 -1
  312. package/template/.cursor/skills/yss-prototype-stage/scripts/offline-html.mjs +3 -3
  313. package/template/.cursor/skills/yss-prototype-stage/tests/browser-smoke.mjs +2 -2
  314. package/template/.cursor/skills/yss-prototype-stage/tests/run-scenarios.mjs +4 -4
  315. package/template/.cursor/skills/yss-prototype-stage/tests/workbench-smoke.mjs +1 -1
  316. package/template/.cursor/skills/yss-repository/SKILL.md +2 -2
  317. package/template/.cursor/skills/yss-repository/references/profiles/layered-mvc-service.md +1 -1
  318. package/template/.cursor/skills/yss-repository/references/profiles/mvc-data-analysis-v1.md +1 -1
  319. package/template/.cursor/skills/yss-repository/tests/profile-routing.test.mjs +2 -2
  320. package/template/.cursor/skills/yss-research/SKILL.md +6 -2
  321. package/template/.cursor/skills/yss-skill-source-index-refresh/references/backend-component-platform-compatibility.md +1 -1
  322. package/template/.cursor/skills/yss-stage-decision/SKILL.md +5 -5
  323. package/template/.cursor/skills/yss-stage-decision/references/domain-strategy-contract.md +1 -1
  324. package/template/.cursor/skills/yss-stage-decision/references/strategic-handoff-routing.md +1 -1
  325. package/template/.cursor/skills/yss-tactical-design/SKILL.md +2 -2
  326. package/template/.cursor/skills/yss-tactical-design/references/strategic-handoff-routing.md +1 -1
  327. package/template/.cursor/skills/yss-tactical-design/references/tactical-design-contract.md +1 -1
  328. package/template/.cursor/skills/yss-technical-design/SKILL.md +6 -6
  329. package/template/.cursor/skills/yss-technical-design/references/technical-design-contract.md +1 -1
  330. package/template/.cursor/skills/yss-ui/SKILL.md +1 -1
  331. package/template/.cursor/skills/yss-up-springboot3/SKILL.md +1 -1
  332. package/template/.cursor/skills/yss-web-controller/SKILL.md +1 -1
  333. package/template/.cursor/skills/yss-web-controller/references/profiles/layered-mvc-service.md +1 -1
  334. package/template/.cursorrules +1 -1
  335. package/template/.pi/skills/alibaba-java-code-style/SKILL.md +1 -1
  336. package/template/.pi/skills/code-review/SKILL.md +6 -6
  337. package/template/.pi/skills/code-review/references/yss-review-standards.md +1 -1
  338. package/template/.pi/skills/codebase-design/SKILL.md +4 -0
  339. package/template/{.agents/skills/improve-codebase-architecture/SKILL.md → .pi/skills/codebase-design/references/architecture-audit.md} +3 -9
  340. package/template/.pi/skills/cross-repo-implementation-routing/SKILL.md +2 -2
  341. package/template/.pi/skills/frontend-commit/SKILL.md +4 -93
  342. package/template/.pi/skills/git-commit-core/SKILL.md +18 -0
  343. package/template/.pi/skills/i-have-adhd/SKILL.md +1 -1
  344. package/template/.pi/skills/i-have-adhd/references/yss-adaptation.md +1 -1
  345. package/template/.pi/skills/i-have-adhd/tests/verify-integration.mjs +2 -2
  346. package/template/.pi/skills/implementation-repo-onboarding/SKILL.md +2 -2
  347. package/template/.pi/skills/java-backend-commit/SKILL.md +4 -96
  348. package/template/.pi/skills/llm-wiki/SKILL.md +12 -13
  349. package/template/.pi/skills/llm-wiki/assets/CLAUDE.md.template +7 -1
  350. package/template/.pi/skills/llm-wiki/references/compile.md +21 -114
  351. package/template/.pi/skills/llm-wiki/references/discover.md +3 -3
  352. package/template/.pi/skills/llm-wiki/references/ingest.md +8 -33
  353. package/template/.pi/skills/llm-wiki/references/lint.md +9 -46
  354. package/template/.pi/skills/llm-wiki/references/query.md +9 -18
  355. package/template/.pi/skills/llm-wiki/references/schema.md +56 -29
  356. package/template/.pi/skills/llm-wiki/references/transactions.md +48 -0
  357. package/template/.pi/skills/llm-wiki/references/writing.md +4 -4
  358. package/template/.pi/skills/llm-wiki/scripts/advise.mjs +46 -58
  359. package/template/.pi/skills/llm-wiki/scripts/advise.test.mjs +11 -16
  360. package/template/.pi/skills/llm-wiki/scripts/core.mjs +262 -0
  361. package/template/.pi/skills/llm-wiki/scripts/extract.mjs +35 -18
  362. package/template/.pi/skills/llm-wiki/scripts/extract.test.mjs +5 -8
  363. package/template/.pi/skills/llm-wiki/scripts/feedback.mjs +51 -0
  364. package/template/.pi/skills/llm-wiki/scripts/inventory.mjs +43 -171
  365. package/template/.pi/skills/llm-wiki/scripts/inventory.test.mjs +25 -18
  366. package/template/.pi/skills/llm-wiki/scripts/lint-wikilinks.mjs +69 -92
  367. package/template/.pi/skills/llm-wiki/scripts/lint-wikilinks.test.mjs +25 -37
  368. package/template/.pi/skills/llm-wiki/scripts/migrate.mjs +51 -0
  369. package/template/.pi/skills/llm-wiki/scripts/query.mjs +149 -0
  370. package/template/.pi/skills/llm-wiki/scripts/regressions.test.mjs +101 -0
  371. package/template/.pi/skills/llm-wiki/scripts/sources.mjs +166 -0
  372. package/template/.pi/skills/llm-wiki/scripts/transaction.mjs +384 -0
  373. package/template/.pi/skills/llm-wiki/scripts/v2.test.mjs +354 -0
  374. package/template/.pi/skills/prototype/LOGIC.md +1 -1
  375. package/template/.pi/skills/prototype/SKILL.md +5 -19
  376. package/template/.pi/skills/prototype-review/SKILL.md +4 -4
  377. package/template/.pi/skills/setup-matt-pocock-skills/SKILL.md +11 -11
  378. package/template/.pi/skills/triage/SKILL.md +1 -1
  379. package/template/.pi/skills/yss-application/references/profiles/layered-mvc-service.md +1 -1
  380. package/template/.pi/skills/yss-application/references/profiles/mvc-data-analysis-v1.md +1 -1
  381. package/template/.pi/skills/yss-backend-spec-review/SKILL.md +1 -1
  382. package/template/.pi/skills/yss-backend-spec-review/references/check-inputs.md +1 -1
  383. package/template/.pi/skills/yss-ddd-scaffold-generator/SKILL.md +2 -2
  384. package/template/.pi/skills/yss-ddd-scaffold-generator/scripts/generate_scaffold.mjs +1 -1
  385. package/template/.pi/skills/yss-design-system/SKILL.md +10 -10
  386. package/template/.pi/skills/yss-design-system/references/design-qa-theme.md +3 -3
  387. package/template/.pi/skills/yss-design-system/references/design-system.md +3 -3
  388. package/template/.pi/skills/yss-exception/SKILL.md +1 -1
  389. package/template/.pi/skills/yss-frontend-scaffold-generator/SKILL.md +2 -2
  390. package/template/.pi/skills/yss-frontend-scaffold-generator/scripts/generate_scaffold.mjs +1 -1
  391. package/template/.pi/skills/yss-frontend-scaffold-generator/scripts/refresh_baseline.mjs +2 -2
  392. package/template/.pi/skills/yss-implementation-contract-compiler/SKILL.md +11 -9
  393. package/template/.pi/skills/yss-implementation-contract-compiler/references/boundaries.md +3 -4
  394. package/template/.pi/skills/yss-implementation-contract-compiler/references/compiler-contract.yaml +9 -9
  395. package/template/.pi/skills/yss-implementation-contract-compiler/references/slice-implementation-contract-v2.md +3 -3
  396. package/template/.pi/skills/yss-implementation-contract-compiler/references/slice-implementation-contract.md +1 -1
  397. package/template/.pi/skills/yss-implementation-contract-compiler/references/strategic-handoff-routing.md +1 -1
  398. package/template/.pi/skills/yss-implementation-contract-compiler/references/yss-skill-execution-result.md +14 -0
  399. package/template/.pi/skills/yss-layered-mvc-scaffold-generator/SKILL.md +2 -2
  400. package/template/.pi/skills/yss-layered-mvc-scaffold-generator/scripts/data-analysis-profile.mjs +5 -5
  401. package/template/.pi/skills/yss-layered-mvc-scaffold-generator/scripts/generate_scaffold.mjs +2 -2
  402. package/template/.pi/skills/yss-layered-mvc-scaffold-generator/scripts/scaffold-generator.test.mjs +42 -0
  403. package/template/.pi/skills/yss-openapi-draft-review/SKILL.md +2 -2
  404. package/template/.pi/skills/yss-openapi-governance/SKILL.md +7 -7
  405. package/template/.pi/skills/yss-product-lifecycle/SKILL.md +17 -17
  406. package/template/.pi/skills/yss-product-lifecycle/references/external-input-questionnaire.md +19 -0
  407. package/template/.pi/skills/yss-product-lifecycle/references/matt-yss-adapter.md +10 -10
  408. package/template/.pi/skills/yss-product-lifecycle/references/orchestration-contract.yaml +85 -47
  409. package/template/.pi/skills/yss-product-lifecycle/references/orchestration.md +24 -16
  410. package/template/.pi/skills/yss-product-lifecycle/references/plan-requirements.md +10 -0
  411. package/template/.pi/skills/yss-product-lifecycle/references/request-triage.md +2 -0
  412. package/template/.pi/skills/yss-product-lifecycle/references/state-model.md +4 -4
  413. package/template/.pi/skills/yss-product-lifecycle/references/user-decisions.md +2 -2
  414. package/template/.pi/skills/yss-prototype-stage/SKILL.md +7 -7
  415. package/template/.pi/skills/yss-prototype-stage/references/existing-ui-entry.md +1 -1
  416. package/template/.pi/skills/yss-prototype-stage/scripts/build-antd-prototype.mjs +1 -1
  417. package/template/.pi/skills/yss-prototype-stage/scripts/offline-html.mjs +3 -3
  418. package/template/.pi/skills/yss-prototype-stage/tests/browser-smoke.mjs +2 -2
  419. package/template/.pi/skills/yss-prototype-stage/tests/run-scenarios.mjs +4 -4
  420. package/template/.pi/skills/yss-prototype-stage/tests/workbench-smoke.mjs +1 -1
  421. package/template/.pi/skills/yss-repository/SKILL.md +2 -2
  422. package/template/.pi/skills/yss-repository/references/profiles/layered-mvc-service.md +1 -1
  423. package/template/.pi/skills/yss-repository/references/profiles/mvc-data-analysis-v1.md +1 -1
  424. package/template/.pi/skills/yss-repository/tests/profile-routing.test.mjs +2 -2
  425. package/template/.pi/skills/yss-research/SKILL.md +6 -2
  426. package/template/.pi/skills/yss-skill-source-index-refresh/references/backend-component-platform-compatibility.md +1 -1
  427. package/template/.pi/skills/yss-stage-decision/SKILL.md +5 -5
  428. package/template/.pi/skills/yss-stage-decision/references/domain-strategy-contract.md +1 -1
  429. package/template/.pi/skills/yss-stage-decision/references/strategic-handoff-routing.md +1 -1
  430. package/template/.pi/skills/yss-tactical-design/SKILL.md +2 -2
  431. package/template/.pi/skills/yss-tactical-design/references/strategic-handoff-routing.md +1 -1
  432. package/template/.pi/skills/yss-tactical-design/references/tactical-design-contract.md +1 -1
  433. package/template/.pi/skills/yss-technical-design/SKILL.md +6 -6
  434. package/template/.pi/skills/yss-technical-design/references/technical-design-contract.md +1 -1
  435. package/template/.pi/skills/yss-ui/SKILL.md +1 -1
  436. package/template/.pi/skills/yss-up-springboot3/SKILL.md +1 -1
  437. package/template/.pi/skills/yss-web-controller/SKILL.md +1 -1
  438. package/template/.pi/skills/yss-web-controller/references/profiles/layered-mvc-service.md +1 -1
  439. package/template/{docs → .template-spec}/adr/README.md +1 -1
  440. package/template/{docs → .template-spec}/agents/README.md +2 -2
  441. package/template/{docs → .template-spec}/agents/backend-architecture-profiles.md +2 -2
  442. package/template/{docs → .template-spec}/agents/digital-human-roles.md +6 -6
  443. package/template/{docs → .template-spec}/agents/digital-human-roles.yaml +6 -6
  444. package/template/{docs → .template-spec}/agents/gitlab-workflow-skills.md +2 -2
  445. package/template/{docs → .template-spec}/agents/issue-tracker.md +1 -1
  446. package/template/{docs → .template-spec}/agents/skill-migrations.md +17 -2
  447. package/template/{docs → .template-spec}/agents/triage-labels.md +1 -1
  448. package/template/{docs → .template-spec}/agents/yss-skill-registry.yaml +12 -31
  449. package/template/{docs → .template-spec}/architecture/README.md +5 -36
  450. package/template/{docs → .template-spec}/architecture/templates/tech-design-template.md +3 -1
  451. package/template/{docs → .template-spec}/design/README.md +12 -11
  452. package/template/{docs → .template-spec}/design/design.md +19 -19
  453. package/template/{docs/design/templates/interaction-spec-template.md → .template-spec/design/templates/examples/model-publishing-interaction.example.md} +5 -3
  454. package/template/{docs/design/templates/state-matrix-template.md → .template-spec/design/templates/examples/model-publishing-state-matrix.example.md} +3 -1
  455. package/template/.template-spec/design/templates/interaction-spec-template.md +61 -0
  456. package/template/{docs → .template-spec}/design/templates/product-overview-design-template.md +11 -11
  457. package/template/{docs → .template-spec}/design/templates/prototype-evidence-template.yaml +4 -4
  458. package/template/{docs → .template-spec}/design/templates/prototype-review-checklist.md +1 -1
  459. package/template/.template-spec/design/templates/state-matrix-template.md +29 -0
  460. package/template/.template-spec/design/tokens/.design-md-projection.json +13 -0
  461. package/template/{docs → .template-spec}/engineering/backend-platforms.md +1 -1
  462. package/template/{docs → .template-spec}/plan/entry-review.md +8 -4
  463. package/template/{docs → .template-spec}/plan/templates/competitive-analysis-template.md +2 -2
  464. package/template/{docs → .template-spec}/plan/templates/plan-template.md +1 -1
  465. package/template/{docs → .template-spec}/process/contract-reading.md +25 -1
  466. package/template/{docs → .template-spec}/process/lifecycle-artifact-map.md +5 -5
  467. package/template/.template-spec/process/lifecycle-registry-baseline.json +255 -0
  468. package/template/{docs → .template-spec}/process/lifecycle-registry.yaml +25 -12
  469. package/template/{docs → .template-spec}/process/plan-migration.md +1 -1
  470. package/template/.template-spec/process/research-completion.md +23 -0
  471. package/template/{docs → .template-spec}/process/schemas/digital-human-task-package.schema.json +209 -6
  472. package/template/{docs → .template-spec}/process/schemas/frontend-implementation-evidence.schema.json +7 -15
  473. package/template/{docs → .template-spec}/process/schemas/lifecycle-checkpoint.schema.json +15 -0
  474. package/template/{docs → .template-spec}/process/schemas/lifecycle-registry.schema.json +1 -1
  475. package/template/{docs → .template-spec}/process/schemas/strategic-design-handoff-v3.schema.json +1 -1
  476. package/template/{docs → .template-spec}/process/schemas/strategic-design-handoff-v4.schema.json +1 -1
  477. package/template/{docs → .template-spec}/process/schemas/strategic-design-handoff-v5.schema.json +1 -1
  478. package/template/{docs → .template-spec}/process/schemas/strategic-design-handoff.schema.json +1 -1
  479. package/template/{docs → .template-spec}/process/stage-tracking.md +1 -1
  480. package/template/.template-spec/process/subagent-collaboration.md +21 -0
  481. package/template/{docs → .template-spec}/process/templates/frontend-implementation-verification-template.yaml +15 -1
  482. package/template/{docs → .template-spec}/templates/build-architecture-checklist-template.md +2 -2
  483. package/template/{docs → .template-spec}/templates/digital-human-runtime-profile-template.md +2 -2
  484. package/template/{docs → .template-spec}/templates/grok-bot-profile-template.md +1 -1
  485. package/template/{docs → .template-spec}/templates/local-parent-ticket-template.md +1 -1
  486. package/template/.template-spec/templates/release-note-template.md +29 -0
  487. package/template/.template-spec/templates/retro-report-template.md +24 -0
  488. package/template/{docs → .template-spec}/templates/subagent-task-package-template.md +3 -3
  489. package/template/.template-spec/templates/verification-record-template.md +64 -0
  490. package/template/{docs → .template-spec}/templates/vertical-slice-ticket-template.md +5 -10
  491. package/template/{docs → .template-spec}/user-guide//347/224/250/346/210/267/346/211/213/345/206/214.md +1 -1
  492. package/template/{docs → .template-spec}/user-guide//347/224/250/346/210/267/346/211/213/345/206/214/347/264/242/345/274/225.md +4 -4
  493. package/template/AGENTS.md +13 -13
  494. package/template/CONTEXT.md +8 -4
  495. package/template/README.md +33 -45
  496. package/template/scripts/backend-delivery +1 -1
  497. package/template/scripts/contract +5 -5
  498. package/template/scripts/lib/api-contract-decision.mjs +1 -1
  499. package/template/scripts/lib/approved-execution-context.mjs +5 -5
  500. package/template/scripts/lib/backend-delivery.mjs +4 -4
  501. package/template/scripts/lib/backend-platform.mjs +1 -1
  502. package/template/scripts/lib/backend-review.mjs +1 -1
  503. package/template/scripts/lib/backend-scaffold-prerequisites.mjs +1 -1
  504. package/template/scripts/lib/backend-standards-coverage.mjs +2 -2
  505. package/template/scripts/lib/context-reconciliation.mjs +1 -1
  506. package/template/scripts/lib/contract-views.mjs +66 -11
  507. package/template/scripts/lib/delivery-preflight.mjs +6 -6
  508. package/template/scripts/lib/design-md.mjs +2 -2
  509. package/template/scripts/lib/digital-human-roles.mjs +2 -2
  510. package/template/scripts/lib/execution-evidence.mjs +51 -0
  511. package/template/scripts/lib/existing-backend-architecture.mjs +1 -1
  512. package/template/scripts/lib/existing-ui-baseline.mjs +1 -1
  513. package/template/scripts/lib/frontend-delivery.mjs +2 -2
  514. package/template/scripts/lib/governance-layout.mjs +74 -0
  515. package/template/scripts/lib/harness-execution-scope.mjs +3 -3
  516. package/template/scripts/lib/implementation-contract-compiler.mjs +2 -0
  517. package/template/scripts/lib/lifecycle-context-query.mjs +26 -6
  518. package/template/scripts/lib/lifecycle-registry.mjs +46 -7
  519. package/template/scripts/lib/lifecycle-status.mjs +137 -0
  520. package/template/scripts/lib/lifecycle-transition.mjs +90 -11
  521. package/template/scripts/lib/maintenance-research.mjs +63 -0
  522. package/template/scripts/lib/openapi-draft-validation.mjs +1 -1
  523. package/template/scripts/lib/plan-spec-entry-view.mjs +48 -0
  524. package/template/scripts/lib/plan-spec-entry.mjs +3 -3
  525. package/template/scripts/lib/read-only-intake.mjs +132 -0
  526. package/template/scripts/lib/skill-registry.mjs +10 -9
  527. package/template/scripts/lib/skill-supply-chain.mjs +5 -1
  528. package/template/scripts/lib/slice-contract-preparation.mjs +3 -3
  529. package/template/scripts/lib/slice-contract.mjs +1 -1
  530. package/template/scripts/lib/slice-repositories.mjs +1 -1
  531. package/template/scripts/lib/slice-task-package.mjs +4 -4
  532. package/template/scripts/lib/stage-tracking-migration.mjs +2 -2
  533. package/template/scripts/lib/stage-tracking.mjs +5 -5
  534. package/template/scripts/lib/strategic-feedback.mjs +2 -2
  535. package/template/scripts/lib/strategic-handoff-routing.mjs +2 -2
  536. package/template/scripts/lib/strategic-handoff.mjs +15 -15
  537. package/template/scripts/lib/task-package-schema.mjs +2 -2
  538. package/template/scripts/lib/task-package.mjs +10 -3
  539. package/template/scripts/lib/technical-design-boundary.mjs +1 -1
  540. package/template/scripts/lib/user-decision.mjs +2 -1
  541. package/template/scripts/lifecycle-status +14 -0
  542. package/template/scripts/plan-spec-entry +21 -0
  543. package/template/scripts/prepare-read-only-intake +16 -0
  544. package/template/scripts/run-read-only-intake +14 -0
  545. package/template/scripts/strategic-consumer-entry +1 -1
  546. package/template/scripts/strategic-handoff +1 -1
  547. package/template/scripts/verify-digital-human-task-package +2 -2
  548. package/template/scripts/verify-frontend-implementation-evidence +25 -2
  549. package/template/scripts/verify-governance-layout +15 -0
  550. package/template/scripts/verify-lifecycle-checkpoint +1 -1
  551. package/template/scripts/verify-maintenance-research +13 -0
  552. package/template/scripts/verify-project-instance +2 -0
  553. package/template/scripts/verify-scaffold-architecture-decisions +1 -1
  554. package/template/scripts/verify-strategic-context-import +3 -3
  555. package/template/skills-lock.json +82 -139
  556. package/template.manifest.json +135 -46
  557. package/template.snapshot.json +5 -6
  558. package/template/.agents/skills/grill-with-docs/SKILL.md +0 -20
  559. package/template/.agents/skills/grill-with-docs/agents/openai.yaml +0 -5
  560. package/template/.agents/skills/improve-codebase-architecture/agents/openai.yaml +0 -5
  561. package/template/.agents/skills/prototype/UI.md +0 -112
  562. package/template/.agents/skills/setup-matt-pocock-skills/domain.md +0 -37
  563. package/template/.agents/skills/to-questionnaire/SKILL.md +0 -55
  564. package/template/.agents/skills/to-questionnaire/agents/openai.yaml +0 -5
  565. package/template/.agents/skills/wait-what/SKILL.md +0 -7
  566. package/template/.agents/skills/wait-what/agents/openai.yaml +0 -5
  567. package/template/.codex/skills/data-analytics/.app.json +0 -84
  568. package/template/.codex/skills/data-analytics/.codex-plugin/plugin.json +0 -66
  569. package/template/.codex/skills/data-analytics/.mcp.json +0 -30
  570. package/template/.codex/skills/data-analytics/AGENTS.md +0 -20
  571. package/template/.codex/skills/data-analytics/DEPENDENCIES.MD +0 -27
  572. package/template/.codex/skills/data-analytics/README.md +0 -60
  573. package/template/.codex/skills/data-analytics/__yss_dotfile__.gitignore +0 -2
  574. package/template/.codex/skills/data-analytics/assets/datascience-artifact-widget.html +0 -16
  575. package/template/.codex/skills/data-analytics/assets/datascience-artifact-widget.html.gz.b64.part001 +0 -1
  576. package/template/.codex/skills/data-analytics/assets/datascience-artifact-widget.html.gz.b64.part002 +0 -1
  577. package/template/.codex/skills/data-analytics/assets/datascience-artifact-widget.html.gz.b64.part003 +0 -1
  578. package/template/.codex/skills/data-analytics/assets/datascience-artifact-widget.html.gz.b64.part004 +0 -1
  579. package/template/.codex/skills/data-analytics/assets/datascience-artifact-widget.html.gz.b64.part005 +0 -1
  580. package/template/.codex/skills/data-analytics/assets/datascience-artifact-widget.html.gz.b64.part006 +0 -1
  581. package/template/.codex/skills/data-analytics/assets/datascience-chart-widget.html +0 -16
  582. package/template/.codex/skills/data-analytics/assets/datascience-chart-widget.html.gz.b64.part001 +0 -1
  583. package/template/.codex/skills/data-analytics/assets/datascience-chart-widget.html.gz.b64.part002 +0 -1
  584. package/template/.codex/skills/data-analytics/assets/datascience-chart-widget.html.gz.b64.part003 +0 -1
  585. package/template/.codex/skills/data-analytics/assets/datascience-chart-widget.html.gz.b64.part004 +0 -1
  586. package/template/.codex/skills/data-analytics/assets/datascience-chart-widget.html.gz.b64.part005 +0 -1
  587. package/template/.codex/skills/data-analytics/assets/datascience-chart-widget.html.gz.b64.part006 +0 -1
  588. package/template/.codex/skills/data-analytics/assets/datascience-chart-widget.html.gz.b64.part007 +0 -1
  589. package/template/.codex/skills/data-analytics/assets/datascience-small.svg +0 -5
  590. package/template/.codex/skills/data-analytics/assets/datascience-table-widget.html +0 -16
  591. package/template/.codex/skills/data-analytics/assets/datascience-table-widget.html.gz.b64.part001 +0 -1
  592. package/template/.codex/skills/data-analytics/assets/datascience-table-widget.html.gz.b64.part002 +0 -1
  593. package/template/.codex/skills/data-analytics/assets/datascience-table-widget.html.gz.b64.part003 +0 -1
  594. package/template/.codex/skills/data-analytics/assets/datascience-table-widget.html.gz.b64.part004 +0 -1
  595. package/template/.codex/skills/data-analytics/assets/datascience.png +0 -0
  596. package/template/.codex/skills/data-analytics/assets/datascience.svg +0 -10
  597. package/template/.codex/skills/data-analytics/mcp/server.cjs +0 -2964
  598. package/template/.codex/skills/data-analytics/package-lock.json +0 -3048
  599. package/template/.codex/skills/data-analytics/package.json +0 -43
  600. package/template/.codex/skills/data-analytics/scripts/normalize-widget-assets.mjs +0 -75
  601. package/template/.codex/skills/data-analytics/skills/analyze-data-quality/SKILL.md +0 -137
  602. package/template/.codex/skills/data-analytics/skills/analyze-data-quality/agents/openai.yaml +0 -6
  603. package/template/.codex/skills/data-analytics/skills/analyze-data-quality/references/quality-checks.md +0 -29
  604. package/template/.codex/skills/data-analytics/skills/build-dashboard/SKILL.md +0 -148
  605. package/template/.codex/skills/data-analytics/skills/build-dashboard/agents/openai.yaml +0 -6
  606. package/template/.codex/skills/data-analytics/skills/build-dashboard/specifications/bi-platform-dashboard.md +0 -18
  607. package/template/.codex/skills/data-analytics/skills/build-dashboard/specifications/html-dashboard.md +0 -24
  608. package/template/.codex/skills/data-analytics/skills/build-dashboard/specifications/mcp-artifact-dashboard.md +0 -71
  609. package/template/.codex/skills/data-analytics/skills/build-dashboard/specifications/streamlit-dashboard.md +0 -85
  610. package/template/.codex/skills/data-analytics/skills/build-report/SKILL.md +0 -207
  611. package/template/.codex/skills/data-analytics/skills/build-report/agents/openai.yaml +0 -6
  612. package/template/.codex/skills/data-analytics/skills/build-report/assets/executive-report-shell.html +0 -70
  613. package/template/.codex/skills/data-analytics/skills/build-report/assets/technical-report-shell.html +0 -66
  614. package/template/.codex/skills/data-analytics/skills/build-report/report-to-google-doc/SKILL.md +0 -108
  615. package/template/.codex/skills/data-analytics/skills/build-report/report-to-google-doc/agents/openai.yaml +0 -6
  616. package/template/.codex/skills/data-analytics/skills/build-report/report-to-google-doc/scripts/report_to_google_doc/__init__.py +0 -1
  617. package/template/.codex/skills/data-analytics/skills/build-report/report-to-google-doc/scripts/report_to_google_doc/cli.py +0 -52
  618. package/template/.codex/skills/data-analytics/skills/build-report/report-to-google-doc/scripts/report_to_google_doc/constants.py +0 -61
  619. package/template/.codex/skills/data-analytics/skills/build-report/report-to-google-doc/scripts/report_to_google_doc/docx_writer.py +0 -362
  620. package/template/.codex/skills/data-analytics/skills/build-report/report-to-google-doc/scripts/report_to_google_doc/html_parser.py +0 -829
  621. package/template/.codex/skills/data-analytics/skills/build-report/report-to-google-doc/scripts/report_to_google_doc/model.py +0 -30
  622. package/template/.codex/skills/data-analytics/skills/build-report/report-to-google-doc/scripts/report_to_google_doc/plan.py +0 -127
  623. package/template/.codex/skills/data-analytics/skills/build-report/report-to-google-doc/scripts/report_to_google_doc/quality.py +0 -374
  624. package/template/.codex/skills/data-analytics/skills/build-report/report-to-google-doc/scripts/report_to_google_doc/rendering.py +0 -613
  625. package/template/.codex/skills/data-analytics/skills/build-report/report-to-google-doc/scripts/report_to_google_doc/table_utils.py +0 -54
  626. package/template/.codex/skills/data-analytics/skills/build-report/report-to-google-doc/scripts/report_to_google_doc/utils.py +0 -16
  627. package/template/.codex/skills/data-analytics/skills/build-report/report-to-google-doc/scripts/report_to_google_doc_plan.py +0 -9
  628. package/template/.codex/skills/data-analytics/skills/build-report/report-to-google-doc/tests/test_delivery_plan.py +0 -45
  629. package/template/.codex/skills/data-analytics/skills/build-report/report-to-google-slides/SKILL.md +0 -77
  630. package/template/.codex/skills/data-analytics/skills/build-report/report-to-google-slides/agents/openai.yaml +0 -6
  631. package/template/.codex/skills/data-analytics/skills/build-report/report-to-google-slides/scripts/report_to_google_slides.py +0 -2379
  632. package/template/.codex/skills/data-analytics/skills/build-report/report-to-pdf/SKILL.md +0 -88
  633. package/template/.codex/skills/data-analytics/skills/build-report/report-to-pdf/agents/openai.yaml +0 -6
  634. package/template/.codex/skills/data-analytics/skills/build-report/specifications/executive-report.md +0 -97
  635. package/template/.codex/skills/data-analytics/skills/build-report/specifications/mcp-app-report.md +0 -59
  636. package/template/.codex/skills/data-analytics/skills/build-report/specifications/technical-report.md +0 -75
  637. package/template/.codex/skills/data-analytics/skills/design-kpis/SKILL.md +0 -103
  638. package/template/.codex/skills/data-analytics/skills/design-kpis/agents/openai.yaml +0 -6
  639. package/template/.codex/skills/data-analytics/skills/gather-business-context/SKILL.md +0 -68
  640. package/template/.codex/skills/data-analytics/skills/gather-business-context/agents/openai.yaml +0 -6
  641. package/template/.codex/skills/data-analytics/skills/index/SKILL.md +0 -251
  642. package/template/.codex/skills/data-analytics/skills/index/agents/openai.yaml +0 -6
  643. package/template/.codex/skills/data-analytics/skills/jupyter-notebooks/SKILL.md +0 -131
  644. package/template/.codex/skills/data-analytics/skills/jupyter-notebooks/agents/openai.yaml +0 -6
  645. package/template/.codex/skills/data-analytics/skills/kpi-reporting/SKILL.md +0 -141
  646. package/template/.codex/skills/data-analytics/skills/kpi-reporting/agents/openai.yaml +0 -6
  647. package/template/.codex/skills/data-analytics/skills/kpi-reporting/references/report-templates.md +0 -32
  648. package/template/.codex/skills/data-analytics/skills/market-sizing/SKILL.md +0 -106
  649. package/template/.codex/skills/data-analytics/skills/market-sizing/agents/openai.yaml +0 -6
  650. package/template/.codex/skills/data-analytics/skills/metric-diagnostics/SKILL.md +0 -130
  651. package/template/.codex/skills/data-analytics/skills/metric-diagnostics/agents/openai.yaml +0 -6
  652. package/template/.codex/skills/data-analytics/skills/product-business-analysis/SKILL.md +0 -141
  653. package/template/.codex/skills/data-analytics/skills/product-business-analysis/agents/openai.yaml +0 -6
  654. package/template/.codex/skills/data-analytics/skills/spreadsheets/SKILL.md +0 -178
  655. package/template/.codex/skills/data-analytics/skills/spreadsheets/agents/openai.yaml +0 -9
  656. package/template/.codex/skills/data-analytics/skills/spreadsheets/assets/file-spreadsheet.png +0 -0
  657. package/template/.codex/skills/data-analytics/skills/spreadsheets/charts.md +0 -31
  658. package/template/.codex/skills/data-analytics/skills/spreadsheets/references/artifact_tool_api.md +0 -466
  659. package/template/.codex/skills/data-analytics/skills/spreadsheets/style_guidelines.md +0 -99
  660. package/template/.codex/skills/data-analytics/skills/user-context/SKILL.md +0 -197
  661. package/template/.codex/skills/data-analytics/skills/user-context/agents/openai.yaml +0 -6
  662. package/template/.codex/skills/data-analytics/skills/user-context/plugin-author-config/automation-config.md +0 -26
  663. package/template/.codex/skills/data-analytics/skills/user-context/plugin-author-config/source-category-config.json +0 -51
  664. package/template/.codex/skills/data-analytics/skills/user-context/plugin-author-config/user-context-config.md +0 -66
  665. package/template/.codex/skills/data-analytics/skills/user-context/references/automation.md +0 -69
  666. package/template/.codex/skills/data-analytics/skills/user-context/references/onboarding-examples.md +0 -204
  667. package/template/.codex/skills/data-analytics/skills/user-context/references/onboarding-state-template.json +0 -87
  668. package/template/.codex/skills/data-analytics/skills/user-context/references/onboarding.md +0 -497
  669. package/template/.codex/skills/data-analytics/skills/user-context/references/semantic-layer/connector-playbook.md +0 -74
  670. package/template/.codex/skills/data-analytics/skills/user-context/references/semantic-layer/setup.md +0 -65
  671. package/template/.codex/skills/data-analytics/skills/user-context/references/semantic-layer/skill-template.md +0 -160
  672. package/template/.codex/skills/data-analytics/skills/user-context/references/semantic-layer/source-intake.md +0 -75
  673. package/template/.codex/skills/data-analytics/skills/user-context/references/semantic-layer/weekly-polling-automation.md +0 -96
  674. package/template/.codex/skills/data-analytics/skills/user-context/references/source-category-runtime.md +0 -263
  675. package/template/.codex/skills/data-analytics/skills/user-context/scripts/data_analytics_preflight.py +0 -1462
  676. package/template/.codex/skills/data-analytics/skills/user-context/scripts/init_user_context_state.py +0 -128
  677. package/template/.codex/skills/data-analytics/skills/user-context/scripts/reset_user_context_state.py +0 -101
  678. package/template/.codex/skills/data-analytics/skills/user-context/scripts/validate_user_context_preflight.py +0 -499
  679. package/template/.codex/skills/data-analytics/skills/user-context/tests/test_state_helpers.py +0 -978
  680. package/template/.codex/skills/data-analytics/skills/validate-data/SKILL.md +0 -126
  681. package/template/.codex/skills/data-analytics/skills/validate-data/agents/openai.yaml +0 -6
  682. package/template/.codex/skills/data-analytics/skills/validate-data/references/validation-methods.md +0 -70
  683. package/template/.codex/skills/data-analytics/skills/visualize-data/SKILL.md +0 -157
  684. package/template/.codex/skills/data-analytics/skills/visualize-data/agents/openai.yaml +0 -6
  685. package/template/.codex/skills/data-analytics/skills/visualize-data/references/seaborn-templates.md +0 -774
  686. package/template/.codex/skills/data-analytics/src/DESIGN.md +0 -222
  687. package/template/.codex/skills/data-analytics/src/analytics-app/App.tsx +0 -3666
  688. package/template/.codex/skills/data-analytics/src/analytics-app/analytics-layout.test.mjs +0 -136
  689. package/template/.codex/skills/data-analytics/src/analytics-app/charting/ChartFrame.tsx +0 -54
  690. package/template/.codex/skills/data-analytics/src/analytics-app/charting/ChartLegend.tsx +0 -96
  691. package/template/.codex/skills/data-analytics/src/analytics-app/charting/ChartRenderer.tsx +0 -1647
  692. package/template/.codex/skills/data-analytics/src/analytics-app/charting/ChartTooltip.tsx +0 -245
  693. package/template/.codex/skills/data-analytics/src/analytics-app/charting/chart-app-helpers.tsx +0 -462
  694. package/template/.codex/skills/data-analytics/src/analytics-app/charting/chart-capabilities.ts +0 -107
  695. package/template/.codex/skills/data-analytics/src/analytics-app/charting/chart-compatibility.ts +0 -164
  696. package/template/.codex/skills/data-analytics/src/analytics-app/charting/chart-contract.ts +0 -192
  697. package/template/.codex/skills/data-analytics/src/analytics-app/charting/chart-theme.ts +0 -203
  698. package/template/.codex/skills/data-analytics/src/analytics-app/charting/chart-tokens.css +0 -619
  699. package/template/.codex/skills/data-analytics/src/analytics-app/charting/chart-transforms.ts +0 -402
  700. package/template/.codex/skills/data-analytics/src/analytics-app/fonts/SystemSansVariableVF.woff2 +0 -0
  701. package/template/.codex/skills/data-analytics/src/analytics-app/imageExport.ts +0 -370
  702. package/template/.codex/skills/data-analytics/src/analytics-app/layout/AnalyticsLayoutCanvas.tsx +0 -536
  703. package/template/.codex/skills/data-analytics/src/analytics-app/layout/RichMarkdown.tsx +0 -681
  704. package/template/.codex/skills/data-analytics/src/analytics-app/layout/analyticsLayoutCore.ts +0 -164
  705. package/template/.codex/skills/data-analytics/src/analytics-app/main.tsx +0 -21
  706. package/template/.codex/skills/data-analytics/src/analytics-app/scripts/chart_contract.py +0 -472
  707. package/template/.codex/skills/data-analytics/src/analytics-app/scripts/check_analytics_app_runtime_links.py +0 -75
  708. package/template/.codex/skills/data-analytics/src/analytics-app/scripts/design_contract.py +0 -142
  709. package/template/.codex/skills/data-analytics/src/analytics-app/scripts/package_utils.py +0 -142
  710. package/template/.codex/skills/data-analytics/src/analytics-app/scripts/tests/test_package_utils.py +0 -377
  711. package/template/.codex/skills/data-analytics/src/analytics-app/styles.css +0 -3468
  712. package/template/.codex/skills/data-analytics/src/analytics-app/tables/DataTable.d.ts +0 -44
  713. package/template/.codex/skills/data-analytics/src/analytics-app/tables/DataTable.jsx +0 -639
  714. package/template/.codex/skills/data-analytics/src/analytics-app/tables/data-table.css +0 -327
  715. package/template/.codex/skills/data-analytics/src/analytics-app/tokens.css +0 -225
  716. package/template/.codex/skills/data-analytics/src/analytics-app/types.ts +0 -204
  717. package/template/.codex/skills/data-analytics/src/analytics-app-core.md +0 -85
  718. package/template/.codex/skills/data-analytics/src/codex-style-contract.md +0 -30
  719. package/template/.codex/skills/data-analytics/src/datascience-artifact-widget.html +0 -12
  720. package/template/.codex/skills/data-analytics/src/datascience-artifact-widget.jsx +0 -1008
  721. package/template/.codex/skills/data-analytics/src/datascience-chart-widget.css +0 -2117
  722. package/template/.codex/skills/data-analytics/src/datascience-chart-widget.html +0 -122
  723. package/template/.codex/skills/data-analytics/src/datascience-chart-widget.js +0 -3588
  724. package/template/.codex/skills/data-analytics/src/datascience-table-widget.css +0 -483
  725. package/template/.codex/skills/data-analytics/src/datascience-table-widget.html +0 -32
  726. package/template/.codex/skills/data-analytics/src/datascience-table-widget.js +0 -333
  727. package/template/.codex/skills/data-analytics/src/mcp-host.js +0 -153
  728. package/template/.codex/skills/data-analytics/src/recharts-config.js +0 -315
  729. package/template/.codex/skills/data-analytics/src/recharts-renderer.jsx +0 -294
  730. package/template/.codex/skills/data-analytics/src/sql-source-view.css +0 -33
  731. package/template/.codex/skills/data-analytics/src/sql-source-view.js +0 -157
  732. package/template/.codex/skills/data-analytics/src/styles/codex-theme.css +0 -121
  733. package/template/.codex/skills/data-analytics/src/table-renderer.jsx +0 -28
  734. package/template/.codex/skills/data-analytics/tests/chart-transforms.test.mjs +0 -87
  735. package/template/.codex/skills/data-analytics/tests/funnel-smoke.html +0 -105
  736. package/template/.codex/skills/data-analytics/tests/inline-widget-compare.html +0 -233
  737. package/template/.codex/skills/data-analytics/tests/mcp-server.test.mjs +0 -1500
  738. package/template/.codex/skills/data-analytics/tests/native-style-contract.test.mjs +0 -219
  739. package/template/.codex/skills/data-analytics/tests/recharts-config.test.mjs +0 -144
  740. package/template/.codex/skills/data-analytics/tests/recharts-renderer.test.mjs +0 -11
  741. package/template/.codex/skills/data-analytics/tests/rich-markdown.test.mjs +0 -54
  742. package/template/.codex/skills/data-analytics/tests/widget-render-harness.html +0 -321
  743. package/template/.codex/skills/data-analytics/tsconfig.json +0 -19
  744. package/template/.codex/skills/data-analytics/vite.config.ts +0 -46
  745. package/template/.codex/skills/grill-with-docs/SKILL.md +0 -20
  746. package/template/.codex/skills/grill-with-docs/agents/openai.yaml +0 -5
  747. package/template/.codex/skills/improve-codebase-architecture/agents/openai.yaml +0 -5
  748. package/template/.codex/skills/prototype/UI.md +0 -112
  749. package/template/.codex/skills/setup-matt-pocock-skills/domain.md +0 -37
  750. package/template/.codex/skills/to-questionnaire/SKILL.md +0 -55
  751. package/template/.codex/skills/to-questionnaire/agents/openai.yaml +0 -5
  752. package/template/.codex/skills/wait-what/SKILL.md +0 -7
  753. package/template/.codex/skills/wait-what/agents/openai.yaml +0 -5
  754. package/template/.cursor/skills/grill-with-docs/SKILL.md +0 -20
  755. package/template/.cursor/skills/grill-with-docs/agents/openai.yaml +0 -5
  756. package/template/.cursor/skills/improve-codebase-architecture/agents/openai.yaml +0 -5
  757. package/template/.cursor/skills/prototype/UI.md +0 -112
  758. package/template/.cursor/skills/setup-matt-pocock-skills/domain.md +0 -37
  759. package/template/.cursor/skills/to-questionnaire/SKILL.md +0 -55
  760. package/template/.cursor/skills/to-questionnaire/agents/openai.yaml +0 -5
  761. package/template/.cursor/skills/wait-what/SKILL.md +0 -7
  762. package/template/.cursor/skills/wait-what/agents/openai.yaml +0 -5
  763. package/template/.pi/skills/grill-with-docs/SKILL.md +0 -20
  764. package/template/.pi/skills/grill-with-docs/agents/openai.yaml +0 -5
  765. package/template/.pi/skills/improve-codebase-architecture/agents/openai.yaml +0 -5
  766. package/template/.pi/skills/prototype/UI.md +0 -112
  767. package/template/.pi/skills/setup-matt-pocock-skills/domain.md +0 -37
  768. package/template/.pi/skills/to-questionnaire/SKILL.md +0 -55
  769. package/template/.pi/skills/to-questionnaire/agents/openai.yaml +0 -5
  770. package/template/.pi/skills/wait-what/SKILL.md +0 -7
  771. package/template/.pi/skills/wait-what/agents/openai.yaml +0 -5
  772. package/template/docs/agents/archify-integration.md +0 -19
  773. package/template/docs/agents/domain.md +0 -31
  774. package/template/docs/design/design-system-sync.yaml +0 -13
  775. package/template/docs/design/preview-dark.html +0 -178
  776. package/template/docs/design/preview.css +0 -730
  777. package/template/docs/design/preview.html +0 -178
  778. package/template/docs/design/preview.js +0 -71
  779. package/template/docs/design/tokens/.design-md-projection.json +0 -13
  780. package/template/docs/engineering/evidence/aliyun-artifact-resolution.json +0 -446
  781. package/template/docs/process/MATT-POCOCK-ENGINEERING-SKILLS.md +0 -13
  782. package/template/docs/process/github-workflows.md +0 -52
  783. package/template/docs/process/subagent-collaboration.md +0 -13
  784. package/template/docs/templates/release-note-template.md +0 -14
  785. package/template/docs/templates/retro-report-template.md +0 -16
  786. package/template/docs/templates/verification-record-template.md +0 -53
  787. /package/template/.agents/skills/{improve-codebase-architecture → codebase-design/references}/HTML-REPORT.md +0 -0
  788. /package/template/.codex/skills/{improve-codebase-architecture → codebase-design/references}/HTML-REPORT.md +0 -0
  789. /package/template/.cursor/skills/{improve-codebase-architecture → codebase-design/references}/HTML-REPORT.md +0 -0
  790. /package/template/.pi/skills/{improve-codebase-architecture → codebase-design/references}/HTML-REPORT.md +0 -0
  791. /package/template/{docs → .template-spec}/api/templates/openapi-draft-review-checklist.md +0 -0
  792. /package/template/{docs → .template-spec}/api/templates/openapi-draft-validation-record-template.yaml +0 -0
  793. /package/template/{docs → .template-spec}/api/templates/openapi-freeze-record-template.md +0 -0
  794. /package/template/{docs → .template-spec}/api/templates/openapi-json-export-record-template.md +0 -0
  795. /package/template/{docs → .template-spec}/architecture/templates/architecture-deepening-template.md +0 -0
  796. /package/template/{docs → .template-spec}/architecture/templates/architecture-review-checklist.md +0 -0
  797. /package/template/{docs → .template-spec}/architecture/templates/business-architecture-template.md +0 -0
  798. /package/template/{docs → .template-spec}/architecture/templates/data-architecture-template.md +0 -0
  799. /package/template/{docs → .template-spec}/architecture/templates/engineering-baseline-review-template.md +0 -0
  800. /package/template/{docs → .template-spec}/architecture/templates/functional-architecture-template.md +0 -0
  801. /package/template/{docs → .template-spec}/architecture/templates/system-overview-design-template.md +0 -0
  802. /package/template/{docs → .template-spec}/design/diagrams/README.md +0 -0
  803. /package/template/{docs → .template-spec}/design/schemas/visual-baseline.schema.json +0 -0
  804. /package/template/{docs → .template-spec}/design/templates/prototype-confirmation-template.md +0 -0
  805. /package/template/{docs → .template-spec}/design/templates/visual-baseline-template.yaml +0 -0
  806. /package/template/{docs → .template-spec}/design/tokens/theme.json +0 -0
  807. /package/template/{docs → .template-spec}/design/tokens/tokens.compact.json +0 -0
  808. /package/template/{docs → .template-spec}/design/tokens/tokens.dark.json +0 -0
  809. /package/template/{docs → .template-spec}/design/tokens/tokens.default.json +0 -0
  810. /package/template/{docs → .template-spec}/design/tokens/variables.css +0 -0
  811. /package/template/{docs → .template-spec}/design/tokens/variables.dark.css +0 -0
  812. /package/template/{docs → .template-spec}/engineering/backend-platforms.json +0 -0
  813. /package/template/{docs → .template-spec}/plan/README.md +0 -0
  814. /package/template/{docs → .template-spec}/plan/templates/competitive-matrix-template.md +0 -0
  815. /package/template/{docs → .template-spec}/plan/templates/market-analysis-template.md +0 -0
  816. /package/template/{docs → .template-spec}/plan/templates/user-pain-points-template.md +0 -0
  817. /package/template/{docs → .template-spec}/process/delivery-preflight.md +0 -0
  818. /package/template/{docs → .template-spec}/process/document-writing.md +0 -0
  819. /package/template/{docs → .template-spec}/process/existing-backend-architecture.md +0 -0
  820. /package/template/{docs → .template-spec}/process/existing-ui-baseline.md +0 -0
  821. /package/template/{docs → .template-spec}/process/frontend-backend-delivery.md +0 -0
  822. /package/template/{docs → .template-spec}/process/harness-process-tailoring.md +0 -0
  823. /package/template/{docs → .template-spec}/process/implementation-repo-integration.md +0 -0
  824. /package/template/{docs/process/lifecycle-registry-baseline.json → .template-spec/process/lifecycle-registry-baseline-v1.json} +0 -0
  825. /package/template/{docs → .template-spec}/process/schemas/api-contract-decision-v2.schema.json +0 -0
  826. /package/template/{docs → .template-spec}/process/schemas/api-contract-decision.schema.json +0 -0
  827. /package/template/{docs → .template-spec}/process/schemas/backend-architecture-identity.schema.json +0 -0
  828. /package/template/{docs → .template-spec}/process/schemas/backend-delivery-verification.schema.json +0 -0
  829. /package/template/{docs → .template-spec}/process/schemas/backend-delivery.schema.json +0 -0
  830. /package/template/{docs → .template-spec}/process/schemas/context-reconciliation.schema.json +0 -0
  831. /package/template/{docs → .template-spec}/process/schemas/data-architecture-decision.schema.json +0 -0
  832. /package/template/{docs → .template-spec}/process/schemas/delivery-preflight-input.schema.json +0 -0
  833. /package/template/{docs → .template-spec}/process/schemas/delivery-preflight-result.schema.json +0 -0
  834. /package/template/{docs → .template-spec}/process/schemas/existing-ui-baseline.schema.json +0 -0
  835. /package/template/{docs → .template-spec}/process/schemas/frontend-delivery-acceptance-v1.schema.json +0 -0
  836. /package/template/{docs → .template-spec}/process/schemas/frontend-delivery-acceptance-v3.schema.json +0 -0
  837. /package/template/{docs → .template-spec}/process/schemas/frontend-delivery-acceptance.schema.json +0 -0
  838. /package/template/{docs → .template-spec}/process/schemas/frontend-strategic-preflight-v2.schema.json +0 -0
  839. /package/template/{docs → .template-spec}/process/schemas/frontend-strategic-preflight.schema.json +0 -0
  840. /package/template/{docs → .template-spec}/process/schemas/implementation-repository-preparation-result.schema.json +0 -0
  841. /package/template/{docs → .template-spec}/process/schemas/openapi-draft-validation-record.schema.json +0 -0
  842. /package/template/{docs → .template-spec}/process/schemas/project-scaffold-contract.schema.json +0 -0
  843. /package/template/{docs → .template-spec}/process/schemas/review-bundle.schema.json +0 -0
  844. /package/template/{docs → .template-spec}/process/schemas/scaffold-architecture-decisions.schema.json +0 -0
  845. /package/template/{docs → .template-spec}/process/schemas/slice-implementation-contract-v3.schema.json +0 -0
  846. /package/template/{docs → .template-spec}/process/schemas/stage-tracking.schema.json +0 -0
  847. /package/template/{docs → .template-spec}/process/schemas/strategic-consumer-feedback.schema.json +0 -0
  848. /package/template/{docs → .template-spec}/process/schemas/strategic-feedback-adjudication.schema.json +0 -0
  849. /package/template/{docs → .template-spec}/process/schemas/strategic-handoff-delivery-verification.schema.json +0 -0
  850. /package/template/{docs → .template-spec}/process/schemas/strategic-handoff-delivery.schema.json +0 -0
  851. /package/template/{docs → .template-spec}/process/schemas/strategic-handoff-domain-strategy-v3.schema.json +0 -0
  852. /package/template/{docs → .template-spec}/process/schemas/strategic-handoff-domain-strategy.schema.json +0 -0
  853. /package/template/{docs → .template-spec}/process/schemas/strategic-handoff-export-v2.schema.json +0 -0
  854. /package/template/{docs → .template-spec}/process/schemas/strategic-handoff-export.schema.json +0 -0
  855. /package/template/{docs → .template-spec}/process/schemas/strategic-handoff-import-receipt-v3.schema.json +0 -0
  856. /package/template/{docs → .template-spec}/process/schemas/strategic-handoff-import-receipt.schema.json +0 -0
  857. /package/template/{docs → .template-spec}/process/schemas/strategic-handoff-package.schema.json +0 -0
  858. /package/template/{docs → .template-spec}/process/schemas/strategic-handoff-stage-decision-package-v3.schema.json +0 -0
  859. /package/template/{docs → .template-spec}/process/schemas/strategic-handoff-stage-decision-package.schema.json +0 -0
  860. /package/template/{docs → .template-spec}/process/schemas/subagent-task-package.schema.json +0 -0
  861. /package/template/{docs → .template-spec}/process/schemas/user-decision.schema.json +0 -0
  862. /package/template/{docs → .template-spec}/process/strategic-handoff-package.md +0 -0
  863. /package/template/{docs → .template-spec}/process/templates/context-reconciliation-template.yaml +0 -0
  864. /package/template/{docs → .template-spec}/process/templates/frontend-implementation-plan-template.yaml +0 -0
  865. /package/template/{docs → .template-spec}/process/templates/lifecycle-checkpoint-template.yaml +0 -0
  866. /package/template/{docs → .template-spec}/process/templates/scaffold-architecture-decisions-template.yaml +0 -0
  867. /package/template/{docs → .template-spec}/process/templates/stage-checkpoint-template.md +0 -0
  868. /package/template/{docs → .template-spec}/process/templates/user-decision-template.yaml +0 -0
  869. /package/template/{docs → .template-spec}/templates/adr-template.md +0 -0
  870. /package/template/{docs → .template-spec}/templates/approval-record-template.yaml +0 -0
  871. /package/template/{docs → .template-spec}/templates/cross-repo-slice-template.md +0 -0
  872. /package/template/{docs → .template-spec}/templates/examples/lifecycle-writing-examples.md +0 -0
  873. /package/template/{docs → .template-spec}/templates/implementation-repo-registry-template.md +0 -0
  874. /package/template/{docs → .template-spec}/templates/openapi-spec-template.yaml +0 -0
  875. /package/template/{docs → .template-spec}/templates/review-bundle-template.yaml +0 -0
  876. /package/template/{docs → .template-spec}/templates/review-report-template.md +0 -0
  877. /package/template/{docs → .template-spec}/templates/spec-delta-template.md +0 -0
  878. /package/template/{docs → .template-spec}/templates/spec-template.md +0 -0
  879. /package/template/{docs → .template-spec}/user-guide/yss-ui-mcp.md +0 -0
  880. /package/template/{docs → .template-spec}/user-guide//346/212/200/346/234/257/350/256/276/350/256/241/347/224/250/346/210/267/346/214/207/345/215/227.md" +0 -0
  881. /package/template/{docs → .template-spec}/user-guide//350/256/276/345/244/207/345/200/237/347/224/250/350/264/257/347/251/277/346/241/210/344/276/213.md" +0 -0
  882. /package/template/{docs → .template-spec}/user-guide//351/230/266/346/256/265/346/216/245/345/205/245/344/270/216/344/272/247/347/211/251/345/244/215/347/224/250/344/275/277/347/224/250/346/211/213/345/206/214.md" +0 -0
@@ -1,2379 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Convert an HTML analytics report into an editable PPTX deck.
3
-
4
- The script does not call Google APIs. It builds a local PPTX plus deterministic
5
- planning and preflight artifacts that an agent can import into native Google
6
- Slides with the Google Drive connector.
7
- """
8
-
9
- from __future__ import annotations
10
-
11
- import argparse
12
- import json
13
- import math
14
- import re
15
- import sys
16
- import zipfile
17
- from dataclasses import asdict, dataclass, field
18
- from pathlib import Path
19
- from typing import Any
20
-
21
- try:
22
- from bs4 import BeautifulSoup, NavigableString, Tag
23
- except ImportError as exc: # pragma: no cover - dependency failure path
24
- raise SystemExit(
25
- "This helper requires beautifulsoup4. Install it in the active Python "
26
- "environment before running report_to_google_slides.py."
27
- ) from exc
28
-
29
- try:
30
- from PIL import Image, ImageChops, ImageFont
31
- except ImportError as exc: # pragma: no cover - dependency failure path
32
- raise SystemExit(
33
- "This helper requires pillow. Install it in the active Python "
34
- "environment before running report_to_google_slides.py."
35
- ) from exc
36
-
37
- try:
38
- from pptx import Presentation
39
- from pptx.dml.color import RGBColor
40
- from pptx.enum.shapes import MSO_SHAPE
41
- from pptx.enum.text import MSO_ANCHOR, PP_ALIGN
42
- from pptx.oxml.xmlchemy import OxmlElement
43
- from pptx.util import Inches, Pt
44
- except ImportError as exc: # pragma: no cover - dependency failure path
45
- raise SystemExit(
46
- "This helper requires python-pptx. Install it in the active Python "
47
- "environment before running report_to_google_slides.py."
48
- ) from exc
49
-
50
-
51
- SLIDE_W = 13.333333
52
- SLIDE_H = 7.5
53
- PX_PER_PT = 96 / 72
54
- DEFAULT_COLORS = {
55
- "surface": "#FCFCFD",
56
- "ink": "#1F2430",
57
- "muted": "#6F768A",
58
- "grid": "#E6E8F0",
59
- "axis": "#D7DBE7",
60
- "blue": "#2E4780",
61
- "blue_light": "#CEDFFE",
62
- "gold": "#B8A037",
63
- "neutral": "#C5CAD3",
64
- "neutral_dark": "#464C55",
65
- }
66
-
67
- EXECUTIVE_SECTION_CANDIDATES = ["Executive Summary", "Summary", "Overview"]
68
- CONCLUSION_SECTION_CANDIDATES = ["Conclusions", "Conclusion", "Key Takeaways", "Takeaways"]
69
- IMPLICATION_SECTION_CANDIDATES = [
70
- "Implications",
71
- "Recommendations",
72
- "Recommended Actions",
73
- "Next Steps",
74
- ]
75
- CAVEAT_SECTION_CANDIDATES = [
76
- "Caveats And Assumptions",
77
- "Caveats",
78
- "Assumptions",
79
- "Limitations",
80
- "Methodology Notes",
81
- ]
82
- SOURCE_SECTION_CANDIDATES = [
83
- "Sources And Reproducibility",
84
- "Sources",
85
- "Reproducibility",
86
- "Methodology",
87
- "Appendix",
88
- ]
89
- BLOCKED_SVG_TAGS = {
90
- "audio",
91
- "base",
92
- "canvas",
93
- "embed",
94
- "foreignobject",
95
- "iframe",
96
- "link",
97
- "meta",
98
- "object",
99
- "script",
100
- "video",
101
- }
102
-
103
-
104
- @dataclass
105
- class MetricCard:
106
- label: str
107
- value: str
108
- note: str = ""
109
-
110
-
111
- @dataclass
112
- class ChartItem:
113
- index: int
114
- title: str
115
- takeaway: str
116
- caveat: str
117
- svg_markup: str
118
- css_text: str = ""
119
- image_path: str = ""
120
- width_px: int = 0
121
- height_px: int = 0
122
-
123
-
124
- @dataclass
125
- class TableItem:
126
- headers: list[str]
127
- rows: list[list[str]]
128
- title: str = ""
129
- note: str = ""
130
-
131
-
132
- @dataclass
133
- class LinkItem:
134
- text: str
135
- href: str
136
-
137
-
138
- @dataclass
139
- class Section:
140
- title: str
141
- paragraphs: list[str] = field(default_factory=list)
142
- bullets: list[str] = field(default_factory=list)
143
- links: list[LinkItem] = field(default_factory=list)
144
-
145
-
146
- @dataclass
147
- class Report:
148
- source: str
149
- title: str
150
- eyebrow: str
151
- lede: str
152
- metrics: list[MetricCard]
153
- sections: dict[str, Section]
154
- charts: list[ChartItem]
155
- tables: list[TableItem]
156
- colors: dict[str, str]
157
-
158
-
159
- @dataclass
160
- class Check:
161
- name: str
162
- status: str
163
- message: str
164
- details: dict[str, Any] = field(default_factory=dict)
165
-
166
-
167
- @dataclass
168
- class Preflight:
169
- checks: list[Check] = field(default_factory=list)
170
-
171
- def add(self, name: str, ok: bool, message: str, details: dict[str, Any] | None = None) -> None:
172
- self.checks.append(
173
- Check(
174
- name=name,
175
- status="passed" if ok else "failed",
176
- message=message,
177
- details=details or {},
178
- )
179
- )
180
-
181
- @property
182
- def passed(self) -> bool:
183
- return all(check.status == "passed" for check in self.checks)
184
-
185
- def to_json(self) -> dict[str, Any]:
186
- failed = [check for check in self.checks if check.status != "passed"]
187
- return {
188
- "status": "passed" if not failed else "failed",
189
- "summary": {
190
- "checks": len(self.checks),
191
- "passed": len(self.checks) - len(failed),
192
- "failed": len(failed),
193
- },
194
- "checks": [asdict(check) for check in self.checks],
195
- }
196
-
197
-
198
- def collapse_ws(text: str) -> str:
199
- return re.sub(r"\s+", " ", text or "").strip()
200
-
201
-
202
- def first_sentence(text: str, max_chars: int = 140) -> str:
203
- text = collapse_ws(text)
204
- if not text:
205
- return ""
206
- match = re.search(r"(?<=[.!?])\s+", text)
207
- sentence = text[: match.start()].strip() if match else text
208
- if len(sentence) <= max_chars:
209
- return sentence
210
- return sentence[: max_chars - 3].rsplit(" ", 1)[0] + "..."
211
-
212
-
213
- def normalized_label(text: str) -> str:
214
- text = collapse_ws(text).lower().replace("&", " and ")
215
- return re.sub(r"[^a-z0-9]+", " ", text).strip()
216
-
217
-
218
- def find_section(report: Report, candidates: list[str]) -> Section | None:
219
- by_label = {normalized_label(title): section for title, section in report.sections.items()}
220
- for candidate in candidates:
221
- section = by_label.get(normalized_label(candidate))
222
- if section:
223
- return section
224
-
225
- candidate_words = [set(normalized_label(candidate).split()) for candidate in candidates]
226
- for title, section in report.sections.items():
227
- title_words = set(normalized_label(title).split())
228
- if any(words and words.issubset(title_words) for words in candidate_words):
229
- return section
230
- return None
231
-
232
-
233
- def section_items_by_candidates(report: Report, candidates: list[str]) -> list[str]:
234
- section = find_section(report, candidates)
235
- if not section:
236
- return []
237
- return section.paragraphs + section.bullets
238
-
239
-
240
- def section_title_by_candidates(report: Report, candidates: list[str], fallback: str) -> str:
241
- section = find_section(report, candidates)
242
- return section.title if section and section.title else fallback
243
-
244
-
245
- def has_section_content(report: Report, candidates: list[str]) -> bool:
246
- section = find_section(report, candidates)
247
- return bool(section and (section.paragraphs or section.bullets or section.links))
248
-
249
-
250
- def resolve_source(source: str) -> Path:
251
- candidate = Path(source).expanduser()
252
- if candidate.is_file():
253
- return candidate.resolve()
254
-
255
- if source.startswith(("http://", "https://")):
256
- raise SystemExit(
257
- "This helper only accepts a local HTML file path. Retrieve the report "
258
- "using the user's environment-specific instructions, then rerun with "
259
- "the local file path."
260
- )
261
-
262
- raise SystemExit(f"Source does not exist or is not a file: {source}")
263
-
264
-
265
- def read_html(path: Path) -> str:
266
- raw = path.read_text(encoding="utf-8", errors="replace")
267
- if len(raw) < 1000:
268
- raise SystemExit(f"HTML file is too small to be a report: {path}")
269
- return raw
270
-
271
-
272
- def validate_report_html(soup: BeautifulSoup, path: Path) -> None:
273
- title = collapse_ws(soup.title.get_text(" ", strip=True) if soup.title else "")
274
- body_text = collapse_ws(soup.get_text(" ", strip=True))
275
- has_report_shape = bool(soup.find("h1") and soup.find("h2")) or bool(
276
- soup.find_all(["svg", "table"])
277
- )
278
- auth_markers = [
279
- "sign in to your account",
280
- "login.microsoftonline.com",
281
- "convergedsignin",
282
- "jsdisabled",
283
- ]
284
- lowered = f"{title} {body_text[:1000]}".lower()
285
- if any(marker in lowered for marker in auth_markers) or not has_report_shape:
286
- raise SystemExit(
287
- f"{path} does not look like the report HTML. It appears to be an auth "
288
- "or non-report page; provide the actual local HTML report file and rerun."
289
- )
290
-
291
-
292
- def extract_report_css(soup: BeautifulSoup) -> str:
293
- return "\n".join(style.get_text() for style in soup.find_all("style"))
294
-
295
-
296
- def parse_css_vars(soup: BeautifulSoup, style_text: str | None = None) -> dict[str, str]:
297
- colors = DEFAULT_COLORS.copy()
298
- style_text = style_text if style_text is not None else extract_report_css(soup)
299
- for name, value in re.findall(r"--([a-zA-Z0-9_-]+)\s*:\s*(#[0-9a-fA-F]{3,6})", style_text):
300
- key = name.replace("-", "_")
301
- if key in colors or key in {
302
- "surface",
303
- "ink",
304
- "muted",
305
- "grid",
306
- "axis",
307
- "blue",
308
- "gold",
309
- "neutral",
310
- }:
311
- colors[key] = normalize_hex(value)
312
- return colors
313
-
314
-
315
- def normalize_hex(value: str) -> str:
316
- value = value.strip()
317
- if re.fullmatch(r"#[0-9a-fA-F]{3}", value):
318
- return "#" + "".join(ch * 2 for ch in value[1:]).upper()
319
- if re.fullmatch(r"#[0-9a-fA-F]{6}", value):
320
- return value.upper()
321
- return "#000000"
322
-
323
-
324
- def text_of(node: Tag | None) -> str:
325
- if not node:
326
- return ""
327
- return collapse_ws(node.get_text(" ", strip=True))
328
-
329
-
330
- def links_of(node: Tag) -> list[LinkItem]:
331
- links: list[LinkItem] = []
332
- for anchor in node.find_all("a", href=True):
333
- href = collapse_ws(str(anchor.get("href") or ""))
334
- if not href or href.startswith("#"):
335
- continue
336
- label = text_of(anchor) or href
337
- links.append(LinkItem(text=label, href=href))
338
- return links
339
-
340
-
341
- def add_section_links(section: Section, links: list[LinkItem]) -> None:
342
- seen = {(link.text, link.href) for link in section.links}
343
- for link in links:
344
- key = (link.text, link.href)
345
- if key not in seen:
346
- section.links.append(link)
347
- seen.add(key)
348
-
349
-
350
- def parse_metrics(soup: BeautifulSoup) -> list[MetricCard]:
351
- metrics: list[MetricCard] = []
352
- for metric in soup.select(".metric"):
353
- label = text_of(metric.select_one(".label")) or text_of(metric.find("span"))
354
- value = text_of(metric.select_one(".value")) or text_of(metric.find("strong"))
355
- note = text_of(metric.select_one(".note"))
356
- if not note:
357
- value_node = metric.select_one(".value") or metric.find("strong")
358
- label_node = metric.select_one(".label") or metric.find("span")
359
- excluded = {id(node) for node in (value_node, label_node) if node}
360
- note_parts = [
361
- collapse_ws(str(part))
362
- for part in metric.stripped_strings
363
- if collapse_ws(str(part))
364
- and collapse_ws(str(part)) not in {label, value}
365
- and id(getattr(part, "parent", None)) not in excluded
366
- ]
367
- note = " ".join(note_parts)
368
- metrics.append(
369
- MetricCard(
370
- label=label,
371
- value=value,
372
- note=note,
373
- )
374
- )
375
- return [m for m in metrics if m.label or m.value]
376
-
377
-
378
- def parse_table(table: Tag) -> TableItem:
379
- parsed_rows: list[tuple[list[str], bool]] = []
380
- for tr in table.find_all("tr"):
381
- cell_nodes = tr.find_all(["th", "td"])
382
- cells = [text_of(cell) for cell in cell_nodes]
383
- if cells:
384
- parsed_rows.append((cells, any(cell.name == "th" for cell in cell_nodes)))
385
- if not parsed_rows:
386
- return TableItem(headers=[], rows=[])
387
- width = max(len(row) for row, _ in parsed_rows)
388
- header_idx = next((idx for idx, (_, has_th) in enumerate(parsed_rows) if has_th), None)
389
- if header_idx is None:
390
- headers = [f"Column {idx}" for idx in range(1, width + 1)]
391
- rows = [row + [""] * (width - len(row)) for row, _ in parsed_rows]
392
- return TableItem(headers=headers, rows=rows)
393
- headers = parsed_rows[header_idx][0]
394
- headers = headers + [f"Column {idx}" for idx in range(len(headers) + 1, width + 1)]
395
- rows = [
396
- row + [""] * (width - len(row))
397
- for idx, (row, _) in enumerate(parsed_rows)
398
- if idx != header_idx
399
- ]
400
- return TableItem(headers=headers, rows=rows)
401
-
402
-
403
- def table_context_title(table: Tag, fallback: str = "") -> str:
404
- heading = table.find_previous(["h4", "h3", "h2"])
405
- return text_of(heading) or fallback
406
-
407
-
408
- def table_context_note(table: Tag) -> str:
409
- for sibling in table.next_siblings:
410
- if isinstance(sibling, NavigableString):
411
- continue
412
- if not isinstance(sibling, Tag):
413
- continue
414
- if sibling.name in {"h2", "h3", "h4", "table"}:
415
- return ""
416
- if sibling.name in {"p", "figcaption"} or "caption" in set(sibling.get("class") or []):
417
- return text_of(sibling)
418
- return ""
419
-
420
-
421
- def find_matching_table(tables: list[TableItem], parsed: TableItem) -> TableItem | None:
422
- for table in tables:
423
- if table.headers == parsed.headers and table.rows == parsed.rows:
424
- return table
425
- return None
426
-
427
-
428
- def parse_svg_length(value: Any, fallback: float) -> float:
429
- match = re.search(r"\d+(?:\.\d+)?", str(value or ""))
430
- if not match:
431
- return fallback
432
- try:
433
- return float(match.group(0))
434
- except ValueError:
435
- return fallback
436
-
437
-
438
- def chart_dimensions(svg: Tag) -> tuple[int, int]:
439
- view_box = svg.get("viewBox") or svg.get("viewbox")
440
- if view_box:
441
- raw_parts = [part for part in re.split(r"[,\s]+", str(view_box).strip()) if part]
442
- try:
443
- parts = [float(part) for part in raw_parts]
444
- except ValueError:
445
- parts = []
446
- if len(parts) == 4 and parts[2] > 0 and parts[3] > 0:
447
- return int(math.ceil(parts[2])), int(math.ceil(parts[3]))
448
- width = parse_svg_length(svg.get("width"), 900)
449
- height = parse_svg_length(svg.get("height"), 520)
450
- return int(math.ceil(width)), int(math.ceil(height))
451
-
452
-
453
- def looks_like_chart_title_candidate(text: str) -> bool:
454
- text = collapse_ws(text)
455
- if not 8 <= len(text) <= 180:
456
- return False
457
- if re.fullmatch(r"[a-z]+/[a-z0-9.+-]+", text.lower()):
458
- return False
459
- lowered = normalized_label(text)
460
- if "matplotlib" in lowered or "http" in text.lower():
461
- return False
462
- if len(re.findall(r"[A-Za-z]", text)) < 4:
463
- return False
464
- words = re.findall(r"[A-Za-z][A-Za-z0-9_-]*", text)
465
- if len(words) < 2:
466
- return False
467
- numeric_ratio = len(re.findall(r"[0-9]", text)) / max(1, len(text))
468
- if numeric_ratio > 0.45:
469
- return False
470
- axis_or_legend_labels = {
471
- "source",
472
- "notes",
473
- "legend",
474
- "date",
475
- "day",
476
- "week",
477
- "month",
478
- "year",
479
- "image svg xml",
480
- }
481
- return lowered not in axis_or_legend_labels
482
-
483
-
484
- def chart_text_candidates(svg: Tag) -> list[str]:
485
- candidates: list[str] = []
486
- for value in svg.stripped_strings:
487
- text = collapse_ws(str(value))
488
- if looks_like_chart_title_candidate(text):
489
- candidates.append(text)
490
-
491
- deduped: list[str] = []
492
- seen: set[str] = set()
493
- for candidate in candidates:
494
- key = normalized_label(candidate)
495
- if key and key not in seen:
496
- deduped.append(candidate)
497
- seen.add(key)
498
- return deduped
499
-
500
-
501
- def chart_semantic_label(svg: Tag) -> str:
502
- for attr in ("aria-label", "data-title", "title"):
503
- value = collapse_ws(str(svg.get(attr) or ""))
504
- if value and looks_like_chart_title_candidate(value):
505
- return value
506
- for tag_name in ("title", "desc"):
507
- for node in svg.find_all(tag_name):
508
- value = text_of(node)
509
- if value and looks_like_chart_title_candidate(value):
510
- return value
511
- return ""
512
-
513
-
514
- def chart_title_score(text: str) -> float:
515
- words = re.findall(r"[A-Za-z][A-Za-z0-9_-]*", text)
516
- score = min(len(words), 14) * 0.8
517
- score += min(len(text), 120) / 45
518
- if re.search(r"[.!?]$", text.strip()):
519
- score += 2.0
520
- if "," in text or ":" in text or ";" in text:
521
- score += 0.8
522
- if len(words) <= 4 and not re.search(r"[.!?]$", text.strip()):
523
- score -= 4.0
524
- if len(text) < 25:
525
- score -= 2.0
526
- return score
527
-
528
-
529
- def parse_chart_title(svg: Tag) -> str:
530
- semantic_label = chart_semantic_label(svg)
531
- if semantic_label:
532
- return first_sentence(semantic_label, 120)
533
- candidates = chart_text_candidates(svg)
534
- if candidates:
535
- return first_sentence(max(candidates, key=chart_title_score), 120)
536
- text = collapse_ws(svg.get_text(" ", strip=True))
537
- return first_sentence(text, 100) or "Source chart from report"
538
-
539
-
540
- def is_material_chart_svg(svg: Tag, width_px: int, height_px: int) -> bool:
541
- if width_px < 180 or height_px < 110:
542
- return False
543
- if chart_semantic_label(svg) or chart_text_candidates(svg):
544
- return True
545
- visual_marks = svg.find_all(
546
- ["circle", "ellipse", "line", "path", "polygon", "polyline", "rect"]
547
- )
548
- if width_px >= 280 and height_px >= 170 and len(visual_marks) >= 3:
549
- return True
550
- return width_px >= 420 and height_px >= 240 and bool(visual_marks)
551
-
552
-
553
- def is_chart_note_text(text: str) -> bool:
554
- normalized = normalized_label(text)
555
- note_prefixes = [
556
- "note",
557
- "notes",
558
- "source",
559
- "definition",
560
- "current window",
561
- "previous window",
562
- "bars are",
563
- "bars show",
564
- "values are",
565
- ]
566
- return any(normalized.startswith(prefix) for prefix in note_prefixes)
567
-
568
-
569
- def chart_takeaway(chart_title: str, next_para: str) -> str:
570
- candidate = first_sentence(next_para, 145)
571
- if not candidate or is_chart_note_text(candidate):
572
- return chart_title
573
- if normalized_label(candidate).startswith(("implication", "recommendation")):
574
- return chart_title
575
- return candidate
576
-
577
-
578
- def chart_context_text(
579
- section_title: str,
580
- chart_title: str,
581
- next_para: str,
582
- prior_items: list[str],
583
- ) -> str:
584
- candidates: list[str] = []
585
- if next_para:
586
- candidates.append(next_para)
587
- candidates.extend(reversed(prior_items))
588
- chart_key = normalize_sentence_key(chart_title)
589
-
590
- for allow_note_text in (False, True):
591
- for candidate in candidates:
592
- raw_candidate = collapse_ws(candidate)
593
- sentence = first_sentence(raw_candidate, 230)
594
- sentence_key = normalize_sentence_key(sentence)
595
- if not sentence or sentence_key == chart_key:
596
- continue
597
- if not allow_note_text and is_chart_note_text(sentence):
598
- continue
599
- return shorten_for_bullet(raw_candidate, 360)
600
- return f"This chart supports the report section: {section_title}."
601
-
602
-
603
- def iter_content_blocks(node: Tag) -> list[Tag]:
604
- if node.name in {"h2", "h3", "h4"}:
605
- return []
606
- classes = set(node.get("class") or [])
607
- if node.name in {"p", "ul", "ol", "table", "svg"}:
608
- return [node]
609
- if "figure" in classes and node.find("svg"):
610
- return [node]
611
- direct_svg = node.find("svg", recursive=False)
612
- direct_text_blocks = node.find(["p", "ul", "ol", "table"], recursive=False)
613
- if direct_svg and not direct_text_blocks:
614
- return [node]
615
-
616
- blocks: list[Tag] = []
617
- for child in node.children:
618
- if isinstance(child, Tag):
619
- blocks.extend(iter_content_blocks(child))
620
- return blocks
621
-
622
-
623
- def heading_level(node: Tag) -> int:
624
- if node.name and re.fullmatch(r"h[1-6]", node.name):
625
- return int(node.name[1])
626
- return 7
627
-
628
-
629
- def section_content_blocks(heading: Tag) -> list[Tag]:
630
- blocks: list[Tag] = []
631
- current_level = heading_level(heading)
632
- for sibling in heading.next_siblings:
633
- if isinstance(sibling, NavigableString):
634
- continue
635
- if not isinstance(sibling, Tag):
636
- continue
637
- if re.fullmatch(r"h[1-6]", sibling.name or "") and heading_level(sibling) <= current_level:
638
- break
639
- blocks.extend(iter_content_blocks(sibling))
640
- return blocks
641
-
642
-
643
- def context_text_from_block(block: Tag) -> str:
644
- if block.name == "p":
645
- return text_of(block)
646
- if block.name in {"ul", "ol"}:
647
- return " ".join(text_of(li) for li in block.find_all("li", recursive=False))
648
- return ""
649
-
650
-
651
- def following_context(blocks: list[Tag], start_idx: int) -> str:
652
- for block in blocks[start_idx + 1 :]:
653
- text = context_text_from_block(block)
654
- if text:
655
- return text
656
- return ""
657
-
658
-
659
- def nearest_heading_title(node: Tag, fallback: str) -> str:
660
- heading = node.find_previous(["h4", "h3", "h2", "h1"])
661
- heading_text = text_of(heading) if isinstance(heading, Tag) else ""
662
- return heading_text or fallback
663
-
664
-
665
- def nearby_prior_items(node: Tag, limit: int = 4) -> list[str]:
666
- items: list[str] = []
667
- for prev in node.find_all_previous(["p", "li"], limit=limit):
668
- text = text_of(prev)
669
- if text:
670
- items.append(text)
671
- return list(reversed(items))
672
-
673
-
674
- def nearby_next_context(node: Tag, limit: int = 4) -> str:
675
- for nxt in node.find_all_next(["p", "li"], limit=limit):
676
- text = text_of(nxt)
677
- if text:
678
- return text
679
- return ""
680
-
681
-
682
- def add_chart_item(
683
- charts: list[ChartItem],
684
- svg_container: Tag,
685
- section_title: str,
686
- next_para: str,
687
- prior_items: list[str],
688
- style_text: str,
689
- parsed_svg_ids: set[int],
690
- ) -> None:
691
- svg = svg_container if svg_container.name == "svg" else svg_container.find("svg")
692
- if not isinstance(svg, Tag) or id(svg) in parsed_svg_ids:
693
- return
694
- width_px, height_px = chart_dimensions(svg)
695
- if not is_material_chart_svg(svg, width_px, height_px):
696
- parsed_svg_ids.add(id(svg))
697
- return
698
- parsed_svg_ids.add(id(svg))
699
- chart_title = parse_chart_title(svg)
700
- if chart_title == "Source chart from report":
701
- chart_title = section_title
702
- context_text = chart_context_text(section_title, chart_title, next_para, prior_items)
703
- charts.append(
704
- ChartItem(
705
- index=len(charts) + 1,
706
- title=chart_title,
707
- takeaway=chart_takeaway(chart_title, context_text),
708
- caveat=context_text,
709
- svg_markup=str(svg),
710
- css_text=style_text,
711
- width_px=width_px,
712
- height_px=height_px,
713
- )
714
- )
715
-
716
-
717
- def parse_sections_and_charts(
718
- soup: BeautifulSoup,
719
- style_text: str,
720
- ) -> tuple[dict[str, Section], list[ChartItem], list[TableItem]]:
721
- sections: dict[str, Section] = {}
722
- charts: list[ChartItem] = []
723
- tables: list[TableItem] = []
724
- parsed_svg_ids: set[int] = set()
725
- parsed_table_ids: set[int] = set()
726
-
727
- for heading in soup.find_all(["h2", "h3"]):
728
- title = text_of(heading)
729
- if not title:
730
- continue
731
- section = Section(title=title)
732
- blocks = section_content_blocks(heading)
733
- for block_idx, block in enumerate(blocks):
734
- if block.name == "p":
735
- paragraph = text_of(block)
736
- if paragraph:
737
- section.paragraphs.append(paragraph)
738
- add_section_links(section, links_of(block))
739
- elif block.name in {"ul", "ol"}:
740
- for li in block.find_all("li", recursive=False):
741
- item = text_of(li)
742
- if item:
743
- section.bullets.append(item)
744
- add_section_links(section, links_of(li))
745
- elif block.name == "table":
746
- parsed_table_ids.add(id(block))
747
- table = parse_table(block)
748
- if table.headers or table.rows:
749
- table.title = table_context_title(block, title)
750
- table.note = table_context_note(block)
751
- tables.append(table)
752
- elif block.name == "svg" or block.find("svg"):
753
- add_chart_item(
754
- charts,
755
- block,
756
- title,
757
- following_context(blocks, block_idx),
758
- section.paragraphs + section.bullets,
759
- style_text,
760
- parsed_svg_ids,
761
- )
762
- sections[title] = section
763
-
764
- fallback_title = text_of(soup.find("h1")) or text_of(soup.title) or "Chart evidence"
765
- for svg in soup.find_all("svg"):
766
- if id(svg) in parsed_svg_ids:
767
- continue
768
- add_chart_item(
769
- charts,
770
- svg,
771
- nearest_heading_title(svg, fallback_title),
772
- nearby_next_context(svg),
773
- nearby_prior_items(svg),
774
- style_text,
775
- parsed_svg_ids,
776
- )
777
-
778
- for table in soup.find_all("table"):
779
- if id(table) in parsed_table_ids:
780
- continue
781
- parsed = parse_table(table)
782
- if not (parsed.headers or parsed.rows):
783
- continue
784
- parsed.title = table_context_title(table)
785
- parsed.note = table_context_note(table)
786
- match = find_matching_table(tables, parsed)
787
- if match:
788
- if parsed.title and not match.title:
789
- match.title = parsed.title
790
- if parsed.note and not match.note:
791
- match.note = parsed.note
792
- else:
793
- tables.append(parsed)
794
-
795
- return sections, charts, tables
796
-
797
-
798
- def parse_report(path: Path) -> Report:
799
- raw = read_html(path)
800
- soup = BeautifulSoup(raw, "html.parser")
801
- validate_report_html(soup, path)
802
- style_text = extract_report_css(soup)
803
- colors = parse_css_vars(soup, style_text)
804
- title = text_of(soup.find("h1")) or text_of(soup.title) or "Analytics Report"
805
- eyebrow = text_of(soup.select_one(".eyebrow"))
806
- lede = text_of(soup.select_one(".lede"))
807
- metrics = parse_metrics(soup)
808
- sections, charts, tables = parse_sections_and_charts(soup, style_text)
809
- if not lede:
810
- executive_labels = {normalized_label(label) for label in EXECUTIVE_SECTION_CANDIDATES}
811
- for section_title, section in sections.items():
812
- if normalized_label(section_title) in executive_labels and section.paragraphs:
813
- lede = first_sentence(section.paragraphs[0], 220)
814
- break
815
- return Report(
816
- source=str(path),
817
- title=title,
818
- eyebrow=eyebrow,
819
- lede=lede,
820
- metrics=metrics,
821
- sections=sections,
822
- charts=charts,
823
- tables=tables,
824
- colors=colors,
825
- )
826
-
827
-
828
- def write_json(path: Path, data: Any) -> None:
829
- path.write_text(json.dumps(data, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
830
-
831
-
832
- def rgb(value: str) -> RGBColor:
833
- value = normalize_hex(value)
834
- return RGBColor(int(value[1:3], 16), int(value[3:5], 16), int(value[5:7], 16))
835
-
836
-
837
- def find_font_path(bold: bool = False) -> str | None:
838
- candidates = [
839
- "/System/Library/Fonts/Supplemental/Arial Bold.ttf"
840
- if bold
841
- else "/System/Library/Fonts/Supplemental/Arial.ttf",
842
- "/Library/Fonts/Arial Bold.ttf" if bold else "/Library/Fonts/Arial.ttf",
843
- "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf"
844
- if bold
845
- else "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
846
- ]
847
- for candidate in candidates:
848
- if candidate and Path(candidate).exists():
849
- return candidate
850
- return None
851
-
852
-
853
- def pil_font(
854
- font_size_pt: float, bold: bool = False
855
- ) -> ImageFont.FreeTypeFont | ImageFont.ImageFont:
856
- path = find_font_path(bold)
857
- size_px = max(8, round(font_size_pt * PX_PER_PT))
858
- if path:
859
- return ImageFont.truetype(path, size_px)
860
- return ImageFont.load_default()
861
-
862
-
863
- def line_width_px(text: str, font: ImageFont.FreeTypeFont | ImageFont.ImageFont) -> float:
864
- bbox = font.getbbox(text)
865
- return max(0, bbox[2] - bbox[0])
866
-
867
-
868
- def wrap_lines(
869
- text: str, font_size_pt: float, max_width_pt: float, bold: bool = False
870
- ) -> list[str]:
871
- font = pil_font(font_size_pt, bold=bold)
872
- max_width_px = max_width_pt * PX_PER_PT
873
- lines: list[str] = []
874
- for paragraph in (text or "").splitlines() or [""]:
875
- words = paragraph.split()
876
- if not words:
877
- lines.append("")
878
- continue
879
- line = words[0]
880
- for word in words[1:]:
881
- candidate = f"{line} {word}"
882
- if line_width_px(candidate, font) <= max_width_px:
883
- line = candidate
884
- else:
885
- lines.append(line)
886
- line = word
887
- lines.append(line)
888
- return lines
889
-
890
-
891
- def measured_height_pt(
892
- text: str,
893
- font_size_pt: float,
894
- max_width_pt: float,
895
- bold: bool = False,
896
- line_spacing: float = 1.18,
897
- paragraph_gap_pt: float = 2.5,
898
- ) -> float:
899
- paragraphs = (text or "").splitlines() or [""]
900
- total = 0.0
901
- for paragraph in paragraphs:
902
- lines = wrap_lines(paragraph, font_size_pt, max_width_pt, bold=bold)
903
- total += max(1, len(lines)) * font_size_pt * line_spacing + paragraph_gap_pt
904
- return total
905
-
906
-
907
- def best_font_size(
908
- text: str,
909
- max_width_pt: float,
910
- max_height_pt: float,
911
- start_pt: float,
912
- min_pt: float,
913
- bold: bool = False,
914
- ) -> tuple[float, bool]:
915
- size = start_pt
916
- while size >= min_pt:
917
- if measured_height_pt(text, size, max_width_pt, bold=bold) <= max_height_pt:
918
- return size, True
919
- size -= 1
920
- return min_pt, measured_height_pt(text, min_pt, max_width_pt, bold=bold) <= max_height_pt
921
-
922
-
923
- def set_shape_fill(shape: Any, color: str, transparency: float = 0.0) -> None:
924
- shape.fill.solid()
925
- shape.fill.fore_color.rgb = rgb(color)
926
- if transparency:
927
- shape.fill.transparency = transparency
928
-
929
-
930
- def set_shape_line(
931
- shape: Any, color: str, width_pt: float = 1.0, transparency: float = 0.0
932
- ) -> None:
933
- shape.line.color.rgb = rgb(color)
934
- shape.line.width = Pt(width_pt)
935
- if transparency:
936
- shape.line.transparency = transparency
937
-
938
-
939
- def add_box(
940
- slide: Any,
941
- name: str,
942
- x: float,
943
- y: float,
944
- w: float,
945
- h: float,
946
- fill: str,
947
- line: str | None = None,
948
- radius: bool = False,
949
- ) -> Any:
950
- shape_type = MSO_SHAPE.ROUNDED_RECTANGLE if radius else MSO_SHAPE.RECTANGLE
951
- shape = slide.shapes.add_shape(shape_type, Inches(x), Inches(y), Inches(w), Inches(h))
952
- shape.name = name
953
- set_shape_fill(shape, fill)
954
- set_shape_line(shape, line or fill, width_pt=0.75)
955
- return shape
956
-
957
-
958
- def add_text(
959
- slide: Any,
960
- name: str,
961
- x: float,
962
- y: float,
963
- w: float,
964
- h: float,
965
- text: str,
966
- font_size: float,
967
- color: str,
968
- preflight: Preflight,
969
- *,
970
- bold: bool = False,
971
- min_font: float = 8,
972
- align: PP_ALIGN = PP_ALIGN.LEFT,
973
- valign: MSO_ANCHOR = MSO_ANCHOR.TOP,
974
- ) -> Any:
975
- text = collapse_ws(text)
976
- margin_pt = 4
977
- chosen, fits = best_font_size(
978
- text,
979
- max_width_pt=w * 72 - margin_pt * 2,
980
- max_height_pt=h * 72 - margin_pt * 2,
981
- start_pt=font_size,
982
- min_pt=min_font,
983
- bold=bold,
984
- )
985
- preflight.add(
986
- f"text_fit:{name}",
987
- fits,
988
- f"{name} text fits at {chosen:.0f}pt" if fits else f"{name} may overflow at minimum size",
989
- {"font_size_pt": chosen, "box": [x, y, w, h], "text": text[:180]},
990
- )
991
- shape = slide.shapes.add_textbox(Inches(x), Inches(y), Inches(w), Inches(h))
992
- shape.name = name
993
- tf = shape.text_frame
994
- tf.clear()
995
- tf.word_wrap = True
996
- tf.margin_left = Pt(margin_pt)
997
- tf.margin_right = Pt(margin_pt)
998
- tf.margin_top = Pt(1)
999
- tf.margin_bottom = Pt(1)
1000
- tf.vertical_anchor = valign
1001
- paragraph = tf.paragraphs[0]
1002
- paragraph.text = text
1003
- paragraph.alignment = align
1004
- run = paragraph.runs[0] if paragraph.runs else paragraph.add_run()
1005
- run.font.name = "Arial"
1006
- run.font.size = Pt(chosen)
1007
- run.font.bold = bold
1008
- run.font.color.rgb = rgb(color)
1009
- return shape
1010
-
1011
-
1012
- def apply_native_bullet(paragraph: Any) -> None:
1013
- p_pr = paragraph._p.get_or_add_pPr()
1014
- for child in list(p_pr):
1015
- if child.tag.endswith(("}buChar", "}buAutoNum", "}buBlip", "}buNone")):
1016
- p_pr.remove(child)
1017
- p_pr.set("marL", "285750")
1018
- p_pr.set("indent", "-142875")
1019
- bullet = OxmlElement("a:buChar")
1020
- bullet.set("char", "\u2022")
1021
- p_pr.insert(0, bullet)
1022
-
1023
-
1024
- def add_bullets(
1025
- slide: Any,
1026
- name: str,
1027
- x: float,
1028
- y: float,
1029
- w: float,
1030
- h: float,
1031
- bullets: list[str],
1032
- font_size: float,
1033
- color: str,
1034
- preflight: Preflight,
1035
- *,
1036
- max_items: int = 5,
1037
- min_font: float = 9,
1038
- links: list[str | None] | None = None,
1039
- link_color: str | None = None,
1040
- space_after_pt: float = 5.5,
1041
- ) -> Any:
1042
- clean = [collapse_ws(item) for item in bullets if collapse_ws(item)][:max_items]
1043
- clean_links: list[str | None] = []
1044
- if links:
1045
- clean_links = [links[idx] if idx < len(links) else None for idx in range(len(clean))]
1046
- else:
1047
- clean_links = [None] * len(clean)
1048
- bullet_text = "\n".join(clean)
1049
- chosen, fits = best_font_size(
1050
- bullet_text,
1051
- max_width_pt=w * 72 - 24,
1052
- max_height_pt=h * 72 - 8,
1053
- start_pt=font_size,
1054
- min_pt=min_font,
1055
- )
1056
- preflight.add(
1057
- f"text_fit:{name}",
1058
- fits,
1059
- f"{name} bullets fit at {chosen:.0f}pt" if fits else f"{name} bullets may overflow",
1060
- {"items": len(clean), "font_size_pt": chosen, "box": [x, y, w, h]},
1061
- )
1062
- shape = slide.shapes.add_textbox(Inches(x), Inches(y), Inches(w), Inches(h))
1063
- shape.name = name
1064
- tf = shape.text_frame
1065
- tf.clear()
1066
- tf.word_wrap = True
1067
- tf.margin_left = Pt(4)
1068
- tf.margin_right = Pt(4)
1069
- tf.margin_top = Pt(1)
1070
- tf.margin_bottom = Pt(1)
1071
- for idx, (item, href) in enumerate(zip(clean, clean_links, strict=False)):
1072
- p = tf.paragraphs[0] if idx == 0 else tf.add_paragraph()
1073
- p.text = ""
1074
- p.space_after = Pt(space_after_pt)
1075
- p.level = 0
1076
- apply_native_bullet(p)
1077
- run = p.add_run()
1078
- run.text = item
1079
- run.font.name = "Arial"
1080
- run.font.size = Pt(chosen)
1081
- run.font.color.rgb = rgb(link_color if href and link_color else color)
1082
- if href:
1083
- run.hyperlink.address = href
1084
- return shape
1085
-
1086
-
1087
- def add_source(slide: Any, text: str, colors: dict[str, str], preflight: Preflight) -> None:
1088
- add_text(
1089
- slide,
1090
- "source-note",
1091
- 0.62,
1092
- 7.02,
1093
- 12.1,
1094
- 0.34,
1095
- text,
1096
- 7.5,
1097
- colors.get("muted", DEFAULT_COLORS["muted"]),
1098
- preflight,
1099
- min_font=6,
1100
- )
1101
-
1102
-
1103
- def blank_slide(prs: Presentation, colors: dict[str, str]) -> Any:
1104
- slide = prs.slides.add_slide(prs.slide_layouts[6])
1105
- add_box(
1106
- slide,
1107
- "slide-background",
1108
- 0,
1109
- 0,
1110
- SLIDE_W,
1111
- SLIDE_H,
1112
- colors.get("surface", DEFAULT_COLORS["surface"]),
1113
- )
1114
- return slide
1115
-
1116
-
1117
- def sanitize_css(css_text: str) -> str:
1118
- css_text = re.sub(r"@import[^;]+;", "", css_text, flags=re.IGNORECASE)
1119
- css_text = re.sub(r"@font-face\s*\{[^}]*\}", "", css_text, flags=re.IGNORECASE | re.DOTALL)
1120
- css_text = re.sub(r"expression\s*\([^)]*\)", "none", css_text, flags=re.IGNORECASE)
1121
-
1122
- def replace_url(match: re.Match[str]) -> str:
1123
- target = match.group(1).strip().strip("\"'")
1124
- return match.group(0) if target.startswith("#") else "none"
1125
-
1126
- css_text = re.sub(r"url\(\s*([^)]*?)\s*\)", replace_url, css_text, flags=re.IGNORECASE)
1127
- return css_text.replace("</style", "<\\/style")
1128
-
1129
-
1130
- def sanitize_svg_markup(svg_markup: str) -> str:
1131
- soup = BeautifulSoup(svg_markup, "html.parser")
1132
- for tag in soup.find_all(BLOCKED_SVG_TAGS):
1133
- tag.decompose()
1134
- for tag in soup.find_all(True):
1135
- if tag.name == "style":
1136
- tag.string = sanitize_css(tag.get_text())
1137
- for attr in list(tag.attrs):
1138
- attr_lower = attr.lower()
1139
- value = tag.attrs[attr]
1140
- value_text = " ".join(value) if isinstance(value, list) else str(value)
1141
- if attr_lower.startswith("on"):
1142
- del tag.attrs[attr]
1143
- elif attr_lower in {"href", "xlink:href", "src"}:
1144
- target = collapse_ws(value_text)
1145
- if target and not target.startswith("#"):
1146
- del tag.attrs[attr]
1147
- elif attr_lower == "style":
1148
- tag.attrs[attr] = sanitize_css(value_text)
1149
- elif "url(" in value_text.lower():
1150
- tag.attrs[attr] = sanitize_css(value_text)
1151
- svg = soup.find("svg")
1152
- return str(svg) if svg else ""
1153
-
1154
-
1155
- def svg_with_embedded_css(svg_markup: str, css_text: str) -> str:
1156
- css_text = css_text.strip()
1157
- if not css_text:
1158
- return svg_markup
1159
- soup = BeautifulSoup(svg_markup, "html.parser")
1160
- svg = soup.find("svg")
1161
- if svg is None:
1162
- return svg_markup
1163
- style = soup.new_tag("style")
1164
- style.string = css_text
1165
- svg.insert(0, style)
1166
- return str(svg)
1167
-
1168
-
1169
- def render_svg_chart(chart: ChartItem, out_dir: Path) -> tuple[Path | None, dict[str, Any]]:
1170
- asset_dir = out_dir / "assets" / "charts"
1171
- asset_dir.mkdir(parents=True, exist_ok=True)
1172
- image_path = asset_dir / f"chart_{chart.index:02d}.png"
1173
- safe_svg = sanitize_svg_markup(chart.svg_markup)
1174
- if not safe_svg:
1175
- return None, {"error": "chart SVG was empty after sanitization"}
1176
- report_css = sanitize_css(chart.css_text)
1177
- html = (
1178
- "<!doctype html><html><head><meta charset='utf-8'>"
1179
- "<style>"
1180
- "html,body{margin:0;padding:0;background:white;}svg{display:block;}"
1181
- f"{report_css}"
1182
- "</style>"
1183
- "</head><body>" + safe_svg + "</body></html>"
1184
- )
1185
- try:
1186
- import cairosvg # type: ignore[import-not-found]
1187
-
1188
- cairosvg.svg2png(
1189
- bytestring=svg_with_embedded_css(safe_svg, report_css).encode("utf-8"),
1190
- write_to=str(image_path),
1191
- )
1192
- return image_path, {"sanitized": True, "renderer": "cairosvg"}
1193
- except Exception as cairosvg_error:
1194
- cairo_details = {"cairosvg_error": str(cairosvg_error)[:500]}
1195
-
1196
- try:
1197
- doc_skill_scripts = Path(__file__).resolve().parents[2] / "report-to-google-doc" / "scripts"
1198
- if str(doc_skill_scripts) not in sys.path:
1199
- sys.path.insert(0, str(doc_skill_scripts))
1200
- from report_to_google_doc.rendering import render_simple_svg_with_pillow
1201
-
1202
- render_simple_svg_with_pillow(
1203
- svg_with_embedded_css(safe_svg, report_css),
1204
- image_path,
1205
- max(360, chart.width_px),
1206
- max(220, chart.height_px),
1207
- )
1208
- return image_path, {"sanitized": True, "renderer": "pillow_svg_subset", **cairo_details}
1209
- except Exception as pillow_error:
1210
- pillow_details = {"pillow_svg_error": str(pillow_error)[:500]}
1211
-
1212
- try:
1213
- from playwright.sync_api import sync_playwright
1214
-
1215
- with sync_playwright() as playwright:
1216
- browser = playwright.chromium.launch(headless=True)
1217
- context = browser.new_context(
1218
- viewport={
1219
- "width": max(360, min(1600, chart.width_px + 8)),
1220
- "height": max(220, min(1100, chart.height_px + 8)),
1221
- "device_scale_factor": 2,
1222
- },
1223
- java_script_enabled=False,
1224
- )
1225
- page = context.new_page()
1226
- page.route("**/*", lambda route: route.abort())
1227
- page.set_content(html)
1228
- page.locator("svg").screenshot(path=str(image_path))
1229
- context.close()
1230
- browser.close()
1231
- except Exception as exc:
1232
- svg_path = asset_dir / f"chart_{chart.index:02d}.svg"
1233
- svg_path.write_text(safe_svg, encoding="utf-8")
1234
- return None, {
1235
- "error": str(exc)[:500],
1236
- "sanitized_svg": str(svg_path),
1237
- **cairo_details,
1238
- **pillow_details,
1239
- }
1240
- return image_path, {
1241
- "sanitized": True,
1242
- "external_resources_blocked": True,
1243
- **cairo_details,
1244
- **pillow_details,
1245
- }
1246
-
1247
-
1248
- def image_is_nonblank(path: Path) -> tuple[bool, dict[str, Any]]:
1249
- with Image.open(path).convert("RGB") as image:
1250
- extrema = image.getextrema()
1251
- diff = ImageChops.difference(image, Image.new("RGB", image.size, (255, 255, 255)))
1252
- bbox = diff.getbbox()
1253
- changed_ratio = 0.0
1254
- if bbox:
1255
- gray = diff.convert("L")
1256
- histogram = gray.histogram()
1257
- changed = sum(count for value, count in enumerate(histogram) if value > 8)
1258
- changed_ratio = changed / (image.width * image.height)
1259
- return bool(bbox and changed_ratio > 0.01), {
1260
- "size": image.size,
1261
- "extrema": extrema,
1262
- "changed_ratio": round(changed_ratio, 4),
1263
- }
1264
-
1265
-
1266
- def render_charts(report: Report, out_dir: Path, preflight: Preflight) -> None:
1267
- for chart in report.charts:
1268
- path, details = render_svg_chart(chart, out_dir)
1269
- if path is None:
1270
- preflight.add(
1271
- f"chart_render:{chart.index}",
1272
- False,
1273
- f"chart {chart.index} could not be rendered",
1274
- details,
1275
- )
1276
- continue
1277
- try:
1278
- ok, image_details = image_is_nonblank(path)
1279
- except Exception as exc:
1280
- preflight.add(
1281
- f"chart_render:{chart.index}",
1282
- False,
1283
- f"chart {chart.index} rendered unreadable image",
1284
- {"path": str(path), "error": str(exc)[:500], **details},
1285
- )
1286
- continue
1287
- chart.image_path = str(path)
1288
- preflight.add(
1289
- f"chart_render:{chart.index}",
1290
- ok,
1291
- f"chart {chart.index} rendered to {path.name}"
1292
- if ok
1293
- else f"chart {chart.index} rendered blank",
1294
- {**image_details, **details},
1295
- )
1296
-
1297
-
1298
- def table_slide_title(table: TableItem) -> str:
1299
- title = collapse_ws(table.title)
1300
- if title:
1301
- return title if "table" in normalized_label(title) else f"{title} table"
1302
- if table.headers:
1303
- return "Report table: " + ", ".join(table.headers[:3])
1304
- return "Report table"
1305
-
1306
-
1307
- def conclusions_slide_title(report: Report) -> str:
1308
- has_conclusions = has_section_content(report, CONCLUSION_SECTION_CANDIDATES)
1309
- has_implications = has_section_content(report, IMPLICATION_SECTION_CANDIDATES)
1310
- if has_conclusions and has_implications:
1311
- return "Conclusions and implications"
1312
- if has_conclusions:
1313
- return section_title_by_candidates(report, CONCLUSION_SECTION_CANDIDATES, "Conclusions")
1314
- if has_implications:
1315
- return section_title_by_candidates(report, IMPLICATION_SECTION_CANDIDATES, "Implications")
1316
- return "Key takeaways"
1317
-
1318
-
1319
- def slide_plan(report: Report) -> list[dict[str, Any]]:
1320
- executive = section_items_by_candidates(report, EXECUTIVE_SECTION_CANDIDATES)
1321
- conclusions = section_items_by_candidates(report, CONCLUSION_SECTION_CANDIDATES)
1322
- implications = section_items_by_candidates(report, IMPLICATION_SECTION_CANDIDATES)
1323
- caveats = section_items_by_candidates(report, CAVEAT_SECTION_CANDIDATES)
1324
- sources_section = find_section(report, SOURCE_SECTION_CANDIDATES)
1325
- sources = (sources_section.paragraphs + sources_section.bullets) if sources_section else []
1326
- plan: list[dict[str, Any]] = [
1327
- {
1328
- "kind": "cover",
1329
- "title": report.title,
1330
- "elements": ["slide-title", "lede", "metric-1", "metric-2", "metric-3", "metric-4"],
1331
- },
1332
- {
1333
- "kind": "executive_summary",
1334
- "title": "Executive summary",
1335
- "elements": [
1336
- "slide-title",
1337
- "summary-bullets",
1338
- "metric-card-1",
1339
- "metric-card-2",
1340
- "metric-card-3",
1341
- "metric-card-4",
1342
- ],
1343
- "source_paragraphs": executive,
1344
- },
1345
- ]
1346
- for chart in report.charts:
1347
- plan.append(
1348
- {
1349
- "kind": "chart_evidence",
1350
- "title": chart.takeaway,
1351
- "chart_index": chart.index,
1352
- "elements": ["slide-title", "chart-main", "chart-callout", "source-note"],
1353
- }
1354
- )
1355
- for table_index, table in enumerate(report.tables, start=1):
1356
- plan.append(
1357
- {
1358
- "kind": "table",
1359
- "title": table_slide_title(table),
1360
- "table_index": table_index,
1361
- "elements": ["slide-title", "table-main", "table-note", "source-note"],
1362
- }
1363
- )
1364
- if conclusions or implications:
1365
- plan.append(
1366
- {
1367
- "kind": "conclusions_implications",
1368
- "title": conclusions_slide_title(report),
1369
- "elements": ["slide-title", "conclusions", "implications"],
1370
- "source_paragraphs": {"conclusions": conclusions, "implications": implications},
1371
- }
1372
- )
1373
- if caveats:
1374
- plan.append(
1375
- {
1376
- "kind": "caveats",
1377
- "title": section_title_by_candidates(
1378
- report, CAVEAT_SECTION_CANDIDATES, "Caveats and assumptions"
1379
- ),
1380
- "elements": ["slide-title", "caveats"],
1381
- "source_paragraphs": caveats,
1382
- }
1383
- )
1384
- if sources_section and (sources or sources_section.links):
1385
- source_bullets, source_links = source_bullets_with_links(report)
1386
- source_title = section_title_by_candidates(
1387
- report, SOURCE_SECTION_CANDIDATES, "Sources and reproducibility"
1388
- )
1389
- plan.append(
1390
- {
1391
- "kind": "sources_reproducibility",
1392
- "title": source_title,
1393
- "elements": ["slide-title", "sources"],
1394
- "source_paragraphs": sources,
1395
- "source_links": [
1396
- {"text": bullet, "href": link}
1397
- for bullet, link in zip(source_bullets, source_links)
1398
- if link
1399
- ],
1400
- }
1401
- )
1402
- return plan
1403
-
1404
-
1405
- def add_rule(slide: Any, name: str, x: float, y: float, w: float, color: str) -> None:
1406
- shape = slide.shapes.add_shape(
1407
- MSO_SHAPE.RECTANGLE, Inches(x), Inches(y), Inches(w), Inches(0.025)
1408
- )
1409
- shape.name = name
1410
- set_shape_fill(shape, color)
1411
- set_shape_line(shape, color, width_pt=0)
1412
-
1413
-
1414
- def add_vertical_rule(slide: Any, name: str, x: float, y: float, h: float, color: str) -> None:
1415
- shape = slide.shapes.add_shape(
1416
- MSO_SHAPE.RECTANGLE, Inches(x), Inches(y), Inches(0.015), Inches(h)
1417
- )
1418
- shape.name = name
1419
- set_shape_fill(shape, color)
1420
- set_shape_line(shape, color, width_pt=0)
1421
-
1422
-
1423
- def add_metric_card(
1424
- slide: Any,
1425
- metric: MetricCard,
1426
- idx: int,
1427
- x: float,
1428
- y: float,
1429
- w: float,
1430
- h: float,
1431
- colors: dict[str, str],
1432
- preflight: Preflight,
1433
- ) -> None:
1434
- add_box(
1435
- slide,
1436
- f"metric-{idx}",
1437
- x,
1438
- y,
1439
- w,
1440
- h,
1441
- "#FFFFFF",
1442
- colors.get("grid", DEFAULT_COLORS["grid"]),
1443
- radius=True,
1444
- )
1445
- add_text(
1446
- slide,
1447
- f"metric-label-{idx}",
1448
- x + 0.12,
1449
- y + 0.12,
1450
- w - 0.24,
1451
- 0.32,
1452
- metric.label,
1453
- 7.5,
1454
- colors["muted"],
1455
- preflight,
1456
- bold=True,
1457
- min_font=6,
1458
- )
1459
- add_text(
1460
- slide,
1461
- f"metric-value-{idx}",
1462
- x + 0.12,
1463
- y + 0.46,
1464
- w - 0.24,
1465
- 0.46,
1466
- metric.value,
1467
- 20,
1468
- colors["blue"],
1469
- preflight,
1470
- bold=True,
1471
- min_font=14,
1472
- )
1473
- add_text(
1474
- slide,
1475
- f"metric-note-{idx}",
1476
- x + 0.12,
1477
- y + 0.85,
1478
- w - 0.24,
1479
- 0.36,
1480
- metric.note,
1481
- 8.5,
1482
- colors["muted"],
1483
- preflight,
1484
- min_font=6.5,
1485
- )
1486
-
1487
-
1488
- def executive_bullets(report: Report) -> list[str]:
1489
- items = section_items_by_candidates(report, EXECUTIVE_SECTION_CANDIDATES)
1490
- if not items:
1491
- return [report.lede]
1492
- bullets = [first_sentence(item, 320) for item in items[:4]]
1493
- return [bullet for bullet in bullets if bullet]
1494
-
1495
-
1496
- def shorten_for_bullet(text: str, max_chars: int) -> str:
1497
- text = collapse_ws(text)
1498
- if len(text) <= max_chars:
1499
- return text
1500
- sentences = re.split(r"(?<=[.!?])\s+", text)
1501
- if sentences and len(sentences[0]) <= max_chars:
1502
- return sentences[0]
1503
- return text[: max_chars - 3].rsplit(" ", 1)[0] + "..."
1504
-
1505
-
1506
- def split_sentences(paragraphs: list[str], max_items: int = 6, max_chars: int = 170) -> list[str]:
1507
- output: list[str] = []
1508
- for paragraph in paragraphs:
1509
- paragraph = collapse_ws(paragraph)
1510
- if not paragraph:
1511
- continue
1512
- if len(paragraph) <= max_chars:
1513
- output.append(paragraph)
1514
- if len(output) >= max_items:
1515
- return output
1516
- continue
1517
- parts = re.split(r"(?<=[.!?])\s+", paragraph)
1518
- for part in parts:
1519
- part = collapse_ws(part)
1520
- if part:
1521
- output.append(shorten_for_bullet(part, max_chars))
1522
- if len(output) >= max_items:
1523
- return output
1524
- return output
1525
-
1526
-
1527
- def list_slide_items(items: list[str], max_items: int = 6, max_chars: int = 220) -> list[str]:
1528
- cleaned = [collapse_ws(item) for item in items if collapse_ws(item)]
1529
- if len(cleaned) <= max_items:
1530
- return [shorten_for_bullet(item, max_chars) for item in cleaned]
1531
- return split_sentences(cleaned, max_items=max_items, max_chars=max_chars)
1532
-
1533
-
1534
- def normalize_sentence_key(text: str) -> str:
1535
- return re.sub(r"[^a-z0-9]+", " ", collapse_ws(text).lower()).strip()
1536
-
1537
-
1538
- def chart_callout_text(chart: ChartItem) -> str:
1539
- text = collapse_ws(chart.caveat)
1540
- if not text:
1541
- return ""
1542
- sentences = [
1543
- collapse_ws(part) for part in re.split(r"(?<=[.!?])\s+", text) if collapse_ws(part)
1544
- ]
1545
- if not sentences:
1546
- return text
1547
-
1548
- first_key = normalize_sentence_key(sentences[0])
1549
- takeaway_key = normalize_sentence_key(chart.takeaway)
1550
- title_key = normalize_sentence_key(chart.title)
1551
- if (
1552
- first_key
1553
- and (takeaway_key or title_key)
1554
- and (
1555
- first_key == takeaway_key
1556
- or first_key.startswith((takeaway_key, title_key))
1557
- or takeaway_key.startswith(first_key)
1558
- or first_key == title_key
1559
- or title_key.startswith(first_key)
1560
- )
1561
- ):
1562
- trimmed = " ".join(sentences[1:]).strip()
1563
- if trimmed:
1564
- return trimmed
1565
- return text
1566
- return text
1567
-
1568
-
1569
- def build_cover(prs: Presentation, report: Report, preflight: Preflight) -> None:
1570
- colors = report.colors
1571
- slide = blank_slide(prs, colors)
1572
- add_text(
1573
- slide,
1574
- "eyebrow",
1575
- 0.72,
1576
- 0.46,
1577
- 5.2,
1578
- 0.36,
1579
- report.eyebrow or "Analytics report",
1580
- 9.5,
1581
- colors["muted"],
1582
- preflight,
1583
- bold=True,
1584
- min_font=7,
1585
- )
1586
- add_text(
1587
- slide,
1588
- "slide-title",
1589
- 0.68,
1590
- 0.95,
1591
- 11.7,
1592
- 1.55,
1593
- report.title,
1594
- 28,
1595
- colors["ink"],
1596
- preflight,
1597
- bold=True,
1598
- min_font=20,
1599
- )
1600
- add_rule(slide, "title-rule", 0.72, 2.7, 1.45, colors["gold"])
1601
- add_text(
1602
- slide,
1603
- "lede",
1604
- 0.72,
1605
- 3.02,
1606
- 10.7,
1607
- 1.0,
1608
- report.lede,
1609
- 14,
1610
- colors["neutral_dark"],
1611
- preflight,
1612
- min_font=10,
1613
- )
1614
- metric_count = min(4, len(report.metrics))
1615
- if metric_count:
1616
- gap = 0.18
1617
- card_w = (11.9 - gap * (metric_count - 1)) / metric_count
1618
- for idx, metric in enumerate(report.metrics[:metric_count], start=1):
1619
- add_metric_card(
1620
- slide,
1621
- metric,
1622
- idx,
1623
- 0.72 + (idx - 1) * (card_w + gap),
1624
- 5.45,
1625
- card_w,
1626
- 1.24,
1627
- colors,
1628
- preflight,
1629
- )
1630
- add_source(slide, "Source: HTML report conversion inventory.", colors, preflight)
1631
-
1632
-
1633
- def build_executive(prs: Presentation, report: Report, preflight: Preflight) -> None:
1634
- colors = report.colors
1635
- slide = blank_slide(prs, colors)
1636
- add_text(
1637
- slide,
1638
- "slide-title",
1639
- 0.65,
1640
- 0.45,
1641
- 12.0,
1642
- 0.6,
1643
- "Executive summary",
1644
- 25,
1645
- colors["ink"],
1646
- preflight,
1647
- bold=True,
1648
- min_font=18,
1649
- )
1650
- add_rule(slide, "summary-rule", 0.72, 1.17, 1.2, colors["gold"])
1651
- if not report.metrics:
1652
- add_bullets(
1653
- slide,
1654
- "summary-bullets",
1655
- 0.72,
1656
- 1.48,
1657
- 11.55,
1658
- 4.75,
1659
- executive_bullets(report),
1660
- 16.2,
1661
- colors["ink"],
1662
- preflight,
1663
- max_items=5,
1664
- min_font=11,
1665
- )
1666
- add_source(slide, "Source: report executive summary.", colors, preflight)
1667
- return
1668
- add_bullets(
1669
- slide,
1670
- "summary-bullets",
1671
- 0.72,
1672
- 1.48,
1673
- 7.08,
1674
- 4.55,
1675
- executive_bullets(report),
1676
- 15.7,
1677
- colors["ink"],
1678
- preflight,
1679
- max_items=4,
1680
- min_font=11,
1681
- )
1682
- add_text(
1683
- slide,
1684
- "metrics-heading",
1685
- 8.32,
1686
- 1.22,
1687
- 4.25,
1688
- 0.34,
1689
- "Key metrics",
1690
- 10.5,
1691
- colors["muted"],
1692
- preflight,
1693
- bold=True,
1694
- min_font=8,
1695
- )
1696
- metric_w = 2.02
1697
- metric_h = 1.38
1698
- gap_x = 0.2
1699
- gap_y = 0.26
1700
- for idx, metric in enumerate(report.metrics[:4], start=1):
1701
- col = (idx - 1) % 2
1702
- row = (idx - 1) // 2
1703
- x = 8.32 + col * (metric_w + gap_x)
1704
- y = 1.68 + row * (metric_h + gap_y)
1705
- add_box(
1706
- slide,
1707
- f"metric-card-{idx}",
1708
- x,
1709
- y,
1710
- metric_w,
1711
- metric_h,
1712
- "#FFFFFF",
1713
- colors["grid"],
1714
- radius=True,
1715
- )
1716
- add_text(
1717
- slide,
1718
- f"summary-metric-label-{idx}",
1719
- x + 0.14,
1720
- y + 0.12,
1721
- metric_w - 0.28,
1722
- 0.28,
1723
- metric.label,
1724
- 8.4,
1725
- colors["muted"],
1726
- preflight,
1727
- bold=True,
1728
- min_font=7,
1729
- )
1730
- add_text(
1731
- slide,
1732
- f"summary-metric-value-{idx}",
1733
- x + 0.14,
1734
- y + 0.46,
1735
- metric_w - 0.28,
1736
- 0.42,
1737
- metric.value,
1738
- 19,
1739
- colors["blue"],
1740
- preflight,
1741
- bold=True,
1742
- min_font=14,
1743
- )
1744
- add_text(
1745
- slide,
1746
- f"summary-metric-note-{idx}",
1747
- x + 0.14,
1748
- y + 0.93,
1749
- metric_w - 0.28,
1750
- 0.3,
1751
- metric.note,
1752
- 8.8,
1753
- colors["muted"],
1754
- preflight,
1755
- min_font=7,
1756
- )
1757
- add_source(slide, "Source: report executive summary and top metric cards.", colors, preflight)
1758
-
1759
-
1760
- def build_chart_slide(
1761
- prs: Presentation, report: Report, chart: ChartItem, preflight: Preflight
1762
- ) -> None:
1763
- colors = report.colors
1764
- slide = blank_slide(prs, colors)
1765
- add_text(
1766
- slide,
1767
- "slide-title",
1768
- 0.62,
1769
- 0.38,
1770
- 12.0,
1771
- 0.82,
1772
- chart.takeaway,
1773
- 20,
1774
- colors["ink"],
1775
- preflight,
1776
- bold=True,
1777
- min_font=13.5,
1778
- )
1779
- chart_box = add_box(
1780
- slide, "chart-frame", 0.72, 1.28, 8.0, 5.05, "#FFFFFF", colors["grid"], radius=True
1781
- )
1782
- chart_box.shadow.inherit = False
1783
- if not chart.image_path:
1784
- preflight.add(f"chart_image:{chart.index}", False, "chart image path missing")
1785
- return
1786
- try:
1787
- picture = slide.shapes.add_picture(
1788
- chart.image_path, Inches(0.95), Inches(1.55), width=Inches(7.55)
1789
- )
1790
- except Exception as exc:
1791
- preflight.add(
1792
- f"chart_image:{chart.index}",
1793
- False,
1794
- "chart image could not be inserted into PPTX",
1795
- {"path": chart.image_path, "error": str(exc)[:500]},
1796
- )
1797
- return
1798
- picture.name = "chart-main"
1799
- if picture.height > Inches(4.45):
1800
- picture.height = Inches(4.45)
1801
- picture.left = Inches(0.95 + (7.55 - picture.width / 914400) / 2)
1802
- picture.top = Inches(1.55 + (4.45 - picture.height / 914400) / 2)
1803
- add_box(
1804
- slide, "chart-callout-frame", 9.0, 1.28, 3.55, 5.05, "#FFFFFF", colors["grid"], radius=True
1805
- )
1806
- callout = chart_callout_text(chart)
1807
- add_text(
1808
- slide,
1809
- "chart-callout-heading",
1810
- 9.25,
1811
- 1.55,
1812
- 3.05,
1813
- 0.35,
1814
- "Context" if callout else "Source",
1815
- 10,
1816
- colors["muted"],
1817
- preflight,
1818
- bold=True,
1819
- min_font=7,
1820
- )
1821
- if callout:
1822
- add_text(
1823
- slide,
1824
- "chart-callout",
1825
- 9.25,
1826
- 1.95,
1827
- 3.05,
1828
- 2.2,
1829
- callout,
1830
- 12.5,
1831
- colors["ink"],
1832
- preflight,
1833
- min_font=8.5,
1834
- )
1835
- add_text(
1836
- slide,
1837
- "chart-source",
1838
- 9.25,
1839
- 4.68 if callout else 1.95,
1840
- 3.05,
1841
- 0.75 if callout else 1.4,
1842
- chart.title,
1843
- 8.5,
1844
- colors["muted"],
1845
- preflight,
1846
- min_font=6.5,
1847
- )
1848
- add_source(
1849
- slide,
1850
- f"Source chart {chart.index}: extracted from report SVG and rendered as chart evidence.",
1851
- colors,
1852
- preflight,
1853
- )
1854
-
1855
-
1856
- def build_table_slide(
1857
- prs: Presentation, report: Report, table: TableItem, preflight: Preflight
1858
- ) -> None:
1859
- colors = report.colors
1860
- slide = blank_slide(prs, colors)
1861
- add_text(
1862
- slide,
1863
- "slide-title",
1864
- 0.62,
1865
- 0.42,
1866
- 12.0,
1867
- 0.55,
1868
- table_slide_title(table),
1869
- 22,
1870
- colors["ink"],
1871
- preflight,
1872
- bold=True,
1873
- min_font=16,
1874
- )
1875
- cols = max([1, len(table.headers), *[len(row) for row in table.rows]])
1876
- headers = table.headers + [f"Column {idx}" for idx in range(len(table.headers) + 1, cols + 1)]
1877
- body_rows = [row + [""] * (cols - len(row)) for row in table.rows]
1878
- rows = max(1, len(body_rows) + 1)
1879
- table_h = min(4.25, max(3.05, 0.58 + max(1, len(table.rows)) * 0.42))
1880
- shape = slide.shapes.add_table(
1881
- rows, cols, Inches(0.62), Inches(1.35), Inches(12.1), Inches(table_h)
1882
- )
1883
- shape.name = "table-main"
1884
- ppt_table = shape.table
1885
- if cols >= 1:
1886
- ppt_table.columns[0].width = Inches(1.95)
1887
- for c in range(1, cols):
1888
- ppt_table.columns[c].width = Inches((12.1 - 1.95) / max(1, cols - 1))
1889
- ppt_table.rows[0].height = Inches(0.52)
1890
- body_h = table_h - 0.52
1891
- if rows > 1:
1892
- body_h = (table_h - 0.52) / (rows - 1)
1893
- for r in range(1, rows):
1894
- ppt_table.rows[r].height = Inches(body_h)
1895
- body_font = 11.4 if body_h >= 0.38 else 10.2 if body_h >= 0.31 else 9.2
1896
- for c, header in enumerate(headers):
1897
- cell = ppt_table.cell(0, c)
1898
- cell.text = header
1899
- set_shape_fill(cell, colors["blue_light"])
1900
- cell.margin_left = Inches(0.08)
1901
- cell.margin_right = Inches(0.08)
1902
- cell.margin_top = Inches(0.04)
1903
- cell.margin_bottom = Inches(0.04)
1904
- for paragraph in cell.text_frame.paragraphs:
1905
- paragraph.alignment = PP_ALIGN.CENTER if c else PP_ALIGN.LEFT
1906
- for run in paragraph.runs:
1907
- run.font.size = Pt(10.2)
1908
- run.font.bold = True
1909
- run.font.color.rgb = rgb(colors["ink"])
1910
- for r, row in enumerate(body_rows, start=1):
1911
- for c in range(cols):
1912
- cell = ppt_table.cell(r, c)
1913
- cell.text = row[c] if c < len(row) else ""
1914
- set_shape_fill(cell, "#FFFFFF")
1915
- cell.margin_left = Inches(0.08)
1916
- cell.margin_right = Inches(0.08)
1917
- cell.margin_top = Inches(0.04)
1918
- cell.margin_bottom = Inches(0.04)
1919
- for paragraph in cell.text_frame.paragraphs:
1920
- paragraph.alignment = PP_ALIGN.CENTER if c else PP_ALIGN.LEFT
1921
- for run in paragraph.runs:
1922
- run.font.size = Pt(body_font if c else body_font + 0.4)
1923
- run.font.bold = c == 0
1924
- run.font.color.rgb = rgb(colors["ink"])
1925
- note = table.note or "Source table extracted from the report."
1926
- add_text(
1927
- slide,
1928
- "table-note",
1929
- 0.78,
1930
- 1.35 + table_h + 0.32,
1931
- 11.7,
1932
- 0.55,
1933
- note,
1934
- 11.5,
1935
- colors["muted"],
1936
- preflight,
1937
- min_font=9,
1938
- )
1939
- preflight.add(
1940
- "table_coverage:main",
1941
- bool(headers and body_rows),
1942
- f"main table has {len(body_rows)} body rows and {cols} columns",
1943
- {"rows": len(body_rows), "columns": cols},
1944
- )
1945
- add_source(slide, "Source: report metric table.", colors, preflight)
1946
-
1947
-
1948
- def build_conclusions(prs: Presentation, report: Report, preflight: Preflight) -> None:
1949
- colors = report.colors
1950
- slide = blank_slide(prs, colors)
1951
- add_text(
1952
- slide,
1953
- "slide-title",
1954
- 0.62,
1955
- 0.42,
1956
- 12.0,
1957
- 0.72,
1958
- conclusions_slide_title(report),
1959
- 21,
1960
- colors["ink"],
1961
- preflight,
1962
- bold=True,
1963
- min_font=15,
1964
- )
1965
- conclusion_source_items = section_items_by_candidates(report, CONCLUSION_SECTION_CANDIDATES)
1966
- implication_source_items = section_items_by_candidates(report, IMPLICATION_SECTION_CANDIDATES)
1967
- conclusions = list_slide_items(
1968
- conclusion_source_items,
1969
- max_items=5 if not implication_source_items else 4,
1970
- max_chars=320 if not implication_source_items else 220,
1971
- )
1972
- implications = list_slide_items(
1973
- implication_source_items,
1974
- max_items=6 if not conclusion_source_items else 5,
1975
- max_chars=320 if not conclusion_source_items else 220,
1976
- )
1977
- conclusion_heading = section_title_by_candidates(
1978
- report, CONCLUSION_SECTION_CANDIDATES, "Conclusions"
1979
- )
1980
- implication_heading = section_title_by_candidates(
1981
- report, IMPLICATION_SECTION_CANDIDATES, "Implications"
1982
- )
1983
- if conclusions and not implications:
1984
- add_bullets(
1985
- slide,
1986
- "conclusions",
1987
- 0.75,
1988
- 1.48,
1989
- 11.4,
1990
- 4.75,
1991
- conclusions,
1992
- 14.6,
1993
- colors["ink"],
1994
- preflight,
1995
- max_items=5,
1996
- min_font=10.5,
1997
- )
1998
- add_source(slide, "Source: report conclusion section.", colors, preflight)
1999
- return
2000
- if implications and not conclusions:
2001
- add_bullets(
2002
- slide,
2003
- "implications",
2004
- 0.75,
2005
- 1.48,
2006
- 11.4,
2007
- 4.75,
2008
- implications,
2009
- 14.6,
2010
- colors["ink"],
2011
- preflight,
2012
- max_items=5,
2013
- min_font=10.5,
2014
- )
2015
- add_source(slide, "Source: report implications section.", colors, preflight)
2016
- return
2017
- add_text(
2018
- slide,
2019
- "conclusions-heading",
2020
- 0.78,
2021
- 1.38,
2022
- 5.2,
2023
- 0.35,
2024
- conclusion_heading,
2025
- 13,
2026
- colors["blue"],
2027
- preflight,
2028
- bold=True,
2029
- min_font=10,
2030
- )
2031
- add_bullets(
2032
- slide,
2033
- "conclusions",
2034
- 0.75,
2035
- 1.82,
2036
- 5.55,
2037
- 4.35,
2038
- conclusions,
2039
- 14.2,
2040
- colors["ink"],
2041
- preflight,
2042
- max_items=4,
2043
- min_font=10,
2044
- )
2045
- add_text(
2046
- slide,
2047
- "implications-heading",
2048
- 7.02,
2049
- 1.38,
2050
- 5.2,
2051
- 0.35,
2052
- implication_heading,
2053
- 13,
2054
- colors["blue"],
2055
- preflight,
2056
- bold=True,
2057
- min_font=10,
2058
- )
2059
- add_bullets(
2060
- slide,
2061
- "implications",
2062
- 7.0,
2063
- 1.82,
2064
- 5.55,
2065
- 4.35,
2066
- implications,
2067
- 14.2,
2068
- colors["ink"],
2069
- preflight,
2070
- max_items=4,
2071
- min_font=10,
2072
- )
2073
- add_vertical_rule(slide, "column-divider", 6.67, 1.35, 4.95, colors["grid"])
2074
- add_source(slide, "Source: report conclusions and implications sections.", colors, preflight)
2075
-
2076
-
2077
- def source_bullets_with_links(report: Report) -> tuple[list[str], list[str | None]]:
2078
- section = find_section(report, SOURCE_SECTION_CANDIDATES) or Section("Sources")
2079
- bullets: list[str] = []
2080
- links: list[str | None] = []
2081
- for link in section.links:
2082
- label = collapse_ws(link.text)
2083
- href = collapse_ws(link.href)
2084
- if not href:
2085
- continue
2086
- display = href if not label or label == href else f"{label}: {href}"
2087
- bullets.append(display)
2088
- links.append(href)
2089
-
2090
- remaining_text_slots = max(0, 6 - len(bullets))
2091
- source_text = (
2092
- split_sentences(
2093
- section.paragraphs + section.bullets,
2094
- max_items=remaining_text_slots,
2095
- max_chars=300,
2096
- )
2097
- if remaining_text_slots
2098
- else []
2099
- )
2100
- for item in source_text:
2101
- if any(item in existing or existing in item for existing in bullets):
2102
- continue
2103
- bullets.append(item)
2104
- links.append(None)
2105
- return bullets, links
2106
-
2107
-
2108
- def build_caveats_slide(prs: Presentation, report: Report, preflight: Preflight) -> None:
2109
- colors = report.colors
2110
- slide = blank_slide(prs, colors)
2111
- add_text(
2112
- slide,
2113
- "slide-title",
2114
- 0.62,
2115
- 0.42,
2116
- 12.0,
2117
- 0.55,
2118
- section_title_by_candidates(report, CAVEAT_SECTION_CANDIDATES, "Caveats and assumptions"),
2119
- 23,
2120
- colors["ink"],
2121
- preflight,
2122
- bold=True,
2123
- min_font=17,
2124
- )
2125
- add_rule(slide, "caveats-rule", 0.72, 1.18, 1.2, colors["gold"])
2126
- caveats = split_sentences(
2127
- section_items_by_candidates(report, CAVEAT_SECTION_CANDIDATES),
2128
- max_items=5,
2129
- max_chars=205,
2130
- )
2131
- add_bullets(
2132
- slide,
2133
- "caveats",
2134
- 0.92,
2135
- 1.52,
2136
- 11.15,
2137
- 4.85,
2138
- caveats,
2139
- 15.1,
2140
- colors["ink"],
2141
- preflight,
2142
- max_items=5,
2143
- min_font=11,
2144
- )
2145
- add_source(slide, "Source: report caveats and assumptions section.", colors, preflight)
2146
-
2147
-
2148
- def build_sources_slide(
2149
- prs: Presentation,
2150
- report: Report,
2151
- preflight: Preflight,
2152
- bullets: list[str],
2153
- links: list[str | None],
2154
- ) -> None:
2155
- colors = report.colors
2156
- slide = blank_slide(prs, colors)
2157
- base_title = section_title_by_candidates(
2158
- report, SOURCE_SECTION_CANDIDATES, "Sources and reproducibility"
2159
- )
2160
- add_text(
2161
- slide,
2162
- "slide-title",
2163
- 0.62,
2164
- 0.42,
2165
- 12.0,
2166
- 0.55,
2167
- base_title,
2168
- 23,
2169
- colors["ink"],
2170
- preflight,
2171
- bold=True,
2172
- min_font=17,
2173
- )
2174
- add_rule(slide, "sources-rule", 0.72, 1.18, 1.2, colors["gold"])
2175
- add_bullets(
2176
- slide,
2177
- "sources",
2178
- 0.92,
2179
- 1.38,
2180
- 11.15,
2181
- 5.45,
2182
- bullets,
2183
- 10.6,
2184
- colors["ink"],
2185
- preflight,
2186
- max_items=len(bullets),
2187
- min_font=6,
2188
- links=links,
2189
- link_color=colors["blue"],
2190
- space_after_pt=2.0,
2191
- )
2192
- all_links = [link for link in source_bullets_with_links(report)[1] if link]
2193
- preflight.add(
2194
- "coverage:source_links",
2195
- True,
2196
- f"preserved {len(all_links)} source links from the report",
2197
- {"links": all_links},
2198
- )
2199
- add_source(slide, "Source: report sources and reproducibility section.", colors, preflight)
2200
-
2201
-
2202
- def create_pptx(report: Report, out_dir: Path, preflight: Preflight) -> Path:
2203
- prs = Presentation()
2204
- prs.slide_width = Inches(SLIDE_W)
2205
- prs.slide_height = Inches(SLIDE_H)
2206
- build_cover(prs, report, preflight)
2207
- build_executive(prs, report, preflight)
2208
- for chart in report.charts:
2209
- build_chart_slide(prs, report, chart, preflight)
2210
- for table in report.tables:
2211
- build_table_slide(prs, report, table, preflight)
2212
- if has_section_content(report, CONCLUSION_SECTION_CANDIDATES) or has_section_content(
2213
- report, IMPLICATION_SECTION_CANDIDATES
2214
- ):
2215
- build_conclusions(prs, report, preflight)
2216
- if has_section_content(report, CAVEAT_SECTION_CANDIDATES):
2217
- build_caveats_slide(prs, report, preflight)
2218
- if has_section_content(report, SOURCE_SECTION_CANDIDATES):
2219
- source_bullets, source_links = source_bullets_with_links(report)
2220
- build_sources_slide(prs, report, preflight, source_bullets, source_links)
2221
- pptx_path = out_dir / "deck.pptx"
2222
- prs.save(pptx_path)
2223
- return pptx_path
2224
-
2225
-
2226
- def pptx_package_ok(path: Path) -> tuple[bool, dict[str, Any]]:
2227
- try:
2228
- with zipfile.ZipFile(path) as archive:
2229
- bad = archive.testzip()
2230
- slide_files = [
2231
- name for name in archive.namelist() if re.match(r"ppt/slides/slide\d+\.xml", name)
2232
- ]
2233
- media_files = [name for name in archive.namelist() if name.startswith("ppt/media/")]
2234
- return bad is None and bool(slide_files), {
2235
- "bad_member": bad,
2236
- "slide_count": len(slide_files),
2237
- "media_count": len(media_files),
2238
- }
2239
- except zipfile.BadZipFile:
2240
- return False, {"error": "bad zip file"}
2241
-
2242
-
2243
- def build_manifest(report: Report) -> dict[str, Any]:
2244
- return {
2245
- "source": report.source,
2246
- "title": report.title,
2247
- "eyebrow": report.eyebrow,
2248
- "counts": {
2249
- "metrics": len(report.metrics),
2250
- "sections": len(report.sections),
2251
- "charts": len(report.charts),
2252
- "tables": len(report.tables),
2253
- },
2254
- "metrics": [asdict(metric) for metric in report.metrics],
2255
- "sections": {title: asdict(section) for title, section in report.sections.items()},
2256
- "charts": [
2257
- {
2258
- "index": chart.index,
2259
- "title": chart.title,
2260
- "takeaway": chart.takeaway,
2261
- "caveat": chart.caveat,
2262
- "image_path": chart.image_path,
2263
- "width_px": chart.width_px,
2264
- "height_px": chart.height_px,
2265
- }
2266
- for chart in report.charts
2267
- ],
2268
- "tables": [asdict(table) for table in report.tables],
2269
- "colors": report.colors,
2270
- }
2271
-
2272
-
2273
- def add_coverage_checks(report: Report, preflight: Preflight) -> None:
2274
- preflight.add(
2275
- "source:title", bool(report.title), "source report title extracted", {"title": report.title}
2276
- )
2277
- has_content = bool(
2278
- report.sections or report.metrics or report.charts or report.tables or report.lede
2279
- )
2280
- preflight.add(
2281
- "source:content",
2282
- has_content,
2283
- "source report body content extracted",
2284
- {
2285
- "sections": len(report.sections),
2286
- "metrics": len(report.metrics),
2287
- "charts": len(report.charts),
2288
- "tables": len(report.tables),
2289
- },
2290
- )
2291
- preflight.add(
2292
- "source:metrics",
2293
- True,
2294
- f"extracted {len(report.metrics)} metric cards",
2295
- {"metrics": len(report.metrics)},
2296
- )
2297
- preflight.add(
2298
- "source:charts",
2299
- True,
2300
- f"extracted {len(report.charts)} charts",
2301
- {"charts": len(report.charts)},
2302
- )
2303
- preflight.add(
2304
- "source:tables",
2305
- True,
2306
- f"extracted {len(report.tables)} tables",
2307
- {"tables": len(report.tables)},
2308
- )
2309
- preflight.add(
2310
- "coverage:executive_section",
2311
- True,
2312
- "executive-style section discovery completed",
2313
- {"found": bool(find_section(report, EXECUTIVE_SECTION_CANDIDATES))},
2314
- )
2315
- preflight.add(
2316
- "coverage:caveats",
2317
- True,
2318
- "caveat-style section discovery completed",
2319
- {"found": bool(find_section(report, CAVEAT_SECTION_CANDIDATES))},
2320
- )
2321
- preflight.add(
2322
- "coverage:sources",
2323
- True,
2324
- "source-style section discovery completed",
2325
- {"found": bool(find_section(report, SOURCE_SECTION_CANDIDATES))},
2326
- )
2327
-
2328
-
2329
- def run(source: str, out_dir: Path) -> int:
2330
- out_dir.mkdir(parents=True, exist_ok=True)
2331
- preflight = Preflight()
2332
- source_path = resolve_source(source)
2333
- report = parse_report(source_path)
2334
- add_coverage_checks(report, preflight)
2335
- render_charts(report, out_dir, preflight)
2336
- plan = slide_plan(report)
2337
- write_json(out_dir / "deck_plan.json", plan)
2338
- pptx_path = create_pptx(report, out_dir, preflight)
2339
- ok, details = pptx_package_ok(pptx_path)
2340
- preflight.add("pptx:package", ok, "PPTX package is readable", details)
2341
- manifest = build_manifest(report)
2342
- manifest["outputs"] = {
2343
- "deck_pptx": str(pptx_path),
2344
- "deck_plan": str(out_dir / "deck_plan.json"),
2345
- "preflight": str(out_dir / "preflight_checks.json"),
2346
- }
2347
- write_json(out_dir / "manifest.json", manifest)
2348
- write_json(out_dir / "preflight_checks.json", preflight.to_json())
2349
- print(
2350
- json.dumps(
2351
- {
2352
- "status": "passed" if preflight.passed else "failed",
2353
- "pptx": str(pptx_path),
2354
- "out_dir": str(out_dir),
2355
- },
2356
- indent=2,
2357
- )
2358
- )
2359
- return 0 if preflight.passed else 2
2360
-
2361
-
2362
- def parse_args(argv: list[str]) -> argparse.Namespace:
2363
- parser = argparse.ArgumentParser(
2364
- description="Convert an HTML analytics report into a local PPTX deck."
2365
- )
2366
- parser.add_argument("source", help="Local path to the HTML report file.")
2367
- parser.add_argument(
2368
- "--out-dir", required=True, help="Directory for deck.pptx and verification artifacts."
2369
- )
2370
- return parser.parse_args(argv)
2371
-
2372
-
2373
- def main(argv: list[str] | None = None) -> int:
2374
- args = parse_args(argv or sys.argv[1:])
2375
- return run(args.source, Path(args.out_dir).expanduser().resolve())
2376
-
2377
-
2378
- if __name__ == "__main__":
2379
- raise SystemExit(main())