hyperweave 0.2.0__tar.gz → 0.2.1__tar.gz

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 (327) hide show
  1. {hyperweave-0.2.0 → hyperweave-0.2.1}/CHANGELOG.md +12 -0
  2. {hyperweave-0.2.0 → hyperweave-0.2.1}/PKG-INFO +5 -5
  3. {hyperweave-0.2.0 → hyperweave-0.2.1}/README.md +4 -4
  4. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/_version.py +2 -2
  5. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/config/genome_validator.py +1 -3
  6. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/connectors/github.py +11 -4
  7. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/render/chart_engine.py +7 -15
  8. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/templates/frames/badge/chrome-content.j2 +10 -10
  9. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/templates/frames/badge/chrome-defs.j2 +21 -0
  10. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/templates/frames/stats/chrome-content.j2 +15 -2
  11. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/templates/frames/stats/chrome-defs.j2 +7 -9
  12. hyperweave-0.2.1/src/hyperweave/templates/frames/strip/brutalist-defs.j2 +26 -0
  13. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/templates/frames/strip/chrome-defs.j2 +35 -0
  14. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/templates/frames/strip.svg.j2 +12 -13
  15. {hyperweave-0.2.0 → hyperweave-0.2.1}/tests/test_assembler.py +1 -2
  16. {hyperweave-0.2.0 → hyperweave-0.2.1}/tests/test_chart_engine.py +2 -2
  17. {hyperweave-0.2.0 → hyperweave-0.2.1}/tests/test_github_scrape.py +41 -0
  18. {hyperweave-0.2.0 → hyperweave-0.2.1}/tests/test_telemetry_integration.py +3 -6
  19. hyperweave-0.2.0/src/hyperweave/templates/frames/strip/brutalist-defs.j2 +0 -6
  20. {hyperweave-0.2.0 → hyperweave-0.2.1}/.dockerignore +0 -0
  21. {hyperweave-0.2.0 → hyperweave-0.2.1}/.github/workflows/ci.yml +0 -0
  22. {hyperweave-0.2.0 → hyperweave-0.2.1}/.github/workflows/deploy.yml +0 -0
  23. {hyperweave-0.2.0 → hyperweave-0.2.1}/.github/workflows/publish.yml +0 -0
  24. {hyperweave-0.2.0 → hyperweave-0.2.1}/.gitignore +0 -0
  25. {hyperweave-0.2.0 → hyperweave-0.2.1}/Dockerfile +0 -0
  26. {hyperweave-0.2.0 → hyperweave-0.2.1}/LICENSE +0 -0
  27. {hyperweave-0.2.0 → hyperweave-0.2.1}/assets/buttons/button-liquid.svg +0 -0
  28. {hyperweave-0.2.0 → hyperweave-0.2.1}/assets/cards/card-butterfly.svg +0 -0
  29. {hyperweave-0.2.0 → hyperweave-0.2.1}/assets/cards/card-python.svg +0 -0
  30. {hyperweave-0.2.0 → hyperweave-0.2.1}/assets/cards/card-sunflower.svg +0 -0
  31. {hyperweave-0.2.0 → hyperweave-0.2.1}/assets/cards/card-waves.svg +0 -0
  32. {hyperweave-0.2.0 → hyperweave-0.2.1}/assets/examples/brutalist-emerald/badge_critical.svg +0 -0
  33. {hyperweave-0.2.0 → hyperweave-0.2.1}/assets/examples/brutalist-emerald/badge_passing.svg +0 -0
  34. {hyperweave-0.2.0 → hyperweave-0.2.1}/assets/examples/brutalist-emerald/badge_warning.svg +0 -0
  35. {hyperweave-0.2.0 → hyperweave-0.2.1}/assets/examples/brutalist-emerald/banner.svg +0 -0
  36. {hyperweave-0.2.0 → hyperweave-0.2.1}/assets/examples/brutalist-emerald/icons/discord.svg +0 -0
  37. {hyperweave-0.2.0 → hyperweave-0.2.1}/assets/examples/brutalist-emerald/icons/notion.svg +0 -0
  38. {hyperweave-0.2.0 → hyperweave-0.2.1}/assets/examples/brutalist-emerald/icons/reddit.svg +0 -0
  39. {hyperweave-0.2.0 → hyperweave-0.2.1}/assets/examples/brutalist-emerald/icons/spotify.svg +0 -0
  40. {hyperweave-0.2.0 → hyperweave-0.2.1}/assets/examples/brutalist-emerald/icons/youtube.svg +0 -0
  41. {hyperweave-0.2.0 → hyperweave-0.2.1}/assets/examples/brutalist-emerald/marquee_counter.svg +0 -0
  42. {hyperweave-0.2.0 → hyperweave-0.2.1}/assets/examples/brutalist-emerald/marquee_horizontal.svg +0 -0
  43. {hyperweave-0.2.0 → hyperweave-0.2.1}/assets/examples/brutalist-emerald/marquee_vertical.svg +0 -0
  44. {hyperweave-0.2.0 → hyperweave-0.2.1}/assets/examples/brutalist-emerald/profile-cards/chart_stars_full.svg +0 -0
  45. {hyperweave-0.2.0 → hyperweave-0.2.1}/assets/examples/brutalist-emerald/profile-cards/stats.svg +0 -0
  46. {hyperweave-0.2.0 → hyperweave-0.2.1}/assets/examples/brutalist-emerald/strip.svg +0 -0
  47. {hyperweave-0.2.0 → hyperweave-0.2.1}/assets/examples/chrome-horizon/badge_critical.svg +0 -0
  48. {hyperweave-0.2.0 → hyperweave-0.2.1}/assets/examples/chrome-horizon/badge_passing.svg +0 -0
  49. {hyperweave-0.2.0 → hyperweave-0.2.1}/assets/examples/chrome-horizon/badge_warning.svg +0 -0
  50. {hyperweave-0.2.0 → hyperweave-0.2.1}/assets/examples/chrome-horizon/banner.svg +0 -0
  51. {hyperweave-0.2.0 → hyperweave-0.2.1}/assets/examples/chrome-horizon/icons/bluesky.svg +0 -0
  52. {hyperweave-0.2.0 → hyperweave-0.2.1}/assets/examples/chrome-horizon/icons/github.svg +0 -0
  53. {hyperweave-0.2.0 → hyperweave-0.2.1}/assets/examples/chrome-horizon/icons/instagram.svg +0 -0
  54. {hyperweave-0.2.0 → hyperweave-0.2.1}/assets/examples/chrome-horizon/icons/mastodon.svg +0 -0
  55. {hyperweave-0.2.0 → hyperweave-0.2.1}/assets/examples/chrome-horizon/icons/x.svg +0 -0
  56. {hyperweave-0.2.0 → hyperweave-0.2.1}/assets/examples/chrome-horizon/marquee_counter.svg +0 -0
  57. {hyperweave-0.2.0 → hyperweave-0.2.1}/assets/examples/chrome-horizon/marquee_horizontal.svg +0 -0
  58. {hyperweave-0.2.0 → hyperweave-0.2.1}/assets/examples/chrome-horizon/marquee_vertical.svg +0 -0
  59. {hyperweave-0.2.0 → hyperweave-0.2.1}/assets/examples/chrome-horizon/profile-cards/chart_stars_full.svg +0 -0
  60. {hyperweave-0.2.0 → hyperweave-0.2.1}/assets/examples/chrome-horizon/profile-cards/stats.svg +0 -0
  61. {hyperweave-0.2.0 → hyperweave-0.2.1}/assets/examples/chrome-horizon/strip.svg +0 -0
  62. {hyperweave-0.2.0 → hyperweave-0.2.1}/assets/examples/telemetry/master_card.svg +0 -0
  63. {hyperweave-0.2.0 → hyperweave-0.2.1}/assets/examples/telemetry/receipt.svg +0 -0
  64. {hyperweave-0.2.0 → hyperweave-0.2.1}/assets/examples/telemetry/rhythm_strip.svg +0 -0
  65. {hyperweave-0.2.0 → hyperweave-0.2.1}/assets/footers/inneraura-footer-liquid.svg +0 -0
  66. {hyperweave-0.2.0 → hyperweave-0.2.1}/assets/footers/inneraura-footer-purple.svg +0 -0
  67. {hyperweave-0.2.0 → hyperweave-0.2.1}/assets/footers/inneraura-footer.svg +0 -0
  68. {hyperweave-0.2.0 → hyperweave-0.2.1}/assets/hyperweave-banner.svg +0 -0
  69. {hyperweave-0.2.0 → hyperweave-0.2.1}/assets/icons/cobalt-sapphire-discord.svg +0 -0
  70. {hyperweave-0.2.0 → hyperweave-0.2.1}/assets/icons/cobalt-sapphire-docs.svg +0 -0
  71. {hyperweave-0.2.0 → hyperweave-0.2.1}/assets/icons/cobalt-sapphire-github.svg +0 -0
  72. {hyperweave-0.2.0 → hyperweave-0.2.1}/assets/icons/cobalt-sapphire-instagram.svg +0 -0
  73. {hyperweave-0.2.0 → hyperweave-0.2.1}/assets/icons/cobalt-sapphire-linkedin.svg +0 -0
  74. {hyperweave-0.2.0 → hyperweave-0.2.1}/assets/icons/cobalt-sapphire-tiktok.svg +0 -0
  75. {hyperweave-0.2.0 → hyperweave-0.2.1}/assets/icons/cobalt-sapphire-x.svg +0 -0
  76. {hyperweave-0.2.0 → hyperweave-0.2.1}/assets/icons/cobalt-sapphire-youtube.svg +0 -0
  77. {hyperweave-0.2.0 → hyperweave-0.2.1}/assets/marquees/badge-showcase-triple.svg +0 -0
  78. {hyperweave-0.2.0 → hyperweave-0.2.1}/assets/marquees/genome-marquee-triple.svg +0 -0
  79. {hyperweave-0.2.0 → hyperweave-0.2.1}/assets/marquees/sample-badges.svg +0 -0
  80. {hyperweave-0.2.0 → hyperweave-0.2.1}/assets/mintlify-assets/404.svg +0 -0
  81. {hyperweave-0.2.0 → hyperweave-0.2.1}/assets/mintlify-assets/callout-icons.svg +0 -0
  82. {hyperweave-0.2.0 → hyperweave-0.2.1}/assets/mintlify-assets/divider.svg +0 -0
  83. {hyperweave-0.2.0 → hyperweave-0.2.1}/assets/mintlify-assets/favicon.svg +0 -0
  84. {hyperweave-0.2.0 → hyperweave-0.2.1}/assets/mintlify-assets/hero-banner.svg +0 -0
  85. {hyperweave-0.2.0 → hyperweave-0.2.1}/assets/mintlify-assets/hyperweave-banner-v2.svg +0 -0
  86. {hyperweave-0.2.0 → hyperweave-0.2.1}/assets/mintlify-assets/hyperweave-navbar-logo.svg +0 -0
  87. {hyperweave-0.2.0 → hyperweave-0.2.1}/assets/mintlify-assets/loader.svg +0 -0
  88. {hyperweave-0.2.0 → hyperweave-0.2.1}/assets/mintlify-assets/og-image.svg +0 -0
  89. {hyperweave-0.2.0 → hyperweave-0.2.1}/assets/timelines/hyperweave-roadmap.svg +0 -0
  90. {hyperweave-0.2.0 → hyperweave-0.2.1}/docs/assets/favicon.svg +0 -0
  91. {hyperweave-0.2.0 → hyperweave-0.2.1}/docs/assets/hyperweave-banner.svg +0 -0
  92. {hyperweave-0.2.0 → hyperweave-0.2.1}/docs/assets/hyperweave-navbar-logo.svg +0 -0
  93. {hyperweave-0.2.0 → hyperweave-0.2.1}/docs/assets/og-image.svg +0 -0
  94. {hyperweave-0.2.0 → hyperweave-0.2.1}/docs/explanation/architecture.mdx +0 -0
  95. {hyperweave-0.2.0 → hyperweave-0.2.1}/docs/explanation/camo-compatibility.mdx +0 -0
  96. {hyperweave-0.2.0 → hyperweave-0.2.1}/docs/explanation/cim-compliance.mdx +0 -0
  97. {hyperweave-0.2.0 → hyperweave-0.2.1}/docs/explanation/genome-profile-system.mdx +0 -0
  98. {hyperweave-0.2.0 → hyperweave-0.2.1}/docs/explanation/metadata-tiers.mdx +0 -0
  99. {hyperweave-0.2.0 → hyperweave-0.2.1}/docs/how-to/add-motion-to-badges.mdx +0 -0
  100. {hyperweave-0.2.0 → hyperweave-0.2.1}/docs/how-to/create-session-receipts.mdx +0 -0
  101. {hyperweave-0.2.0 → hyperweave-0.2.1}/docs/how-to/set-up-claude-code-hooks.mdx +0 -0
  102. {hyperweave-0.2.0 → hyperweave-0.2.1}/docs/how-to/use-live-data-badges.mdx +0 -0
  103. {hyperweave-0.2.0 → hyperweave-0.2.1}/docs/introduction.mdx +0 -0
  104. {hyperweave-0.2.0 → hyperweave-0.2.1}/docs/mint.json +0 -0
  105. {hyperweave-0.2.0 → hyperweave-0.2.1}/docs/quickstart.mdx +0 -0
  106. {hyperweave-0.2.0 → hyperweave-0.2.1}/docs/reference/cli.mdx +0 -0
  107. {hyperweave-0.2.0 → hyperweave-0.2.1}/docs/reference/compose-spec.mdx +0 -0
  108. {hyperweave-0.2.0 → hyperweave-0.2.1}/docs/reference/genomes.mdx +0 -0
  109. {hyperweave-0.2.0 → hyperweave-0.2.1}/docs/reference/http-api.mdx +0 -0
  110. {hyperweave-0.2.0 → hyperweave-0.2.1}/docs/reference/mcp-tools.mdx +0 -0
  111. {hyperweave-0.2.0 → hyperweave-0.2.1}/docs/reference/motions.mdx +0 -0
  112. {hyperweave-0.2.0 → hyperweave-0.2.1}/docs/reference/telemetry-contract.mdx +0 -0
  113. {hyperweave-0.2.0 → hyperweave-0.2.1}/docs/spec/hyperweave-protocol.mdx +0 -0
  114. {hyperweave-0.2.0 → hyperweave-0.2.1}/docs/tutorials/build-an-artifact-kit.mdx +0 -0
  115. {hyperweave-0.2.0 → hyperweave-0.2.1}/docs/tutorials/compose-your-first-badge.mdx +0 -0
  116. {hyperweave-0.2.0 → hyperweave-0.2.1}/docs/tutorials/install-session-telemetry.mdx +0 -0
  117. {hyperweave-0.2.0 → hyperweave-0.2.1}/fly.toml +0 -0
  118. {hyperweave-0.2.0 → hyperweave-0.2.1}/hooks/install.py +0 -0
  119. {hyperweave-0.2.0 → hyperweave-0.2.1}/hooks/session_end.sh +0 -0
  120. {hyperweave-0.2.0 → hyperweave-0.2.1}/justfile +0 -0
  121. {hyperweave-0.2.0 → hyperweave-0.2.1}/pyproject.toml +0 -0
  122. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/__init__.py +0 -0
  123. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/__main__.py +0 -0
  124. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/cli.py +0 -0
  125. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/compose/__init__.py +0 -0
  126. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/compose/assembler.py +0 -0
  127. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/compose/context.py +0 -0
  128. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/compose/engine.py +0 -0
  129. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/compose/lanes.py +0 -0
  130. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/compose/resolver.py +0 -0
  131. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/compose/resolvers/__init__.py +0 -0
  132. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/compose/resolvers/chart.py +0 -0
  133. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/compose/resolvers/stats.py +0 -0
  134. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/compose/resolvers/timeline.py +0 -0
  135. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/config/__init__.py +0 -0
  136. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/config/loader.py +0 -0
  137. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/config/registry.py +0 -0
  138. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/config/settings.py +0 -0
  139. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/connectors/__init__.py +0 -0
  140. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/connectors/arxiv.py +0 -0
  141. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/connectors/base.py +0 -0
  142. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/connectors/cache.py +0 -0
  143. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/connectors/rest.py +0 -0
  144. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/core/__init__.py +0 -0
  145. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/core/color.py +0 -0
  146. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/core/contracts.py +0 -0
  147. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/core/enums.py +0 -0
  148. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/core/models.py +0 -0
  149. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/core/schema.py +0 -0
  150. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/core/text.py +0 -0
  151. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/core/thresholds.py +0 -0
  152. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/data/css/accessibility.css +0 -0
  153. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/data/css/bridge.css +0 -0
  154. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/data/css/expression.css +0 -0
  155. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/data/css/phase-colors.css +0 -0
  156. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/data/css/status.css +0 -0
  157. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/data/css/telemetry.css +0 -0
  158. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/data/font-metrics/inter.json +0 -0
  159. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/data/fonts/jetbrains-mono.b64 +0 -0
  160. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/data/fonts/jetbrains-mono.meta.json +0 -0
  161. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/data/fonts/orbitron.b64 +0 -0
  162. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/data/fonts/orbitron.meta.json +0 -0
  163. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/data/genomes/brutalist-emerald.json +0 -0
  164. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/data/genomes/chrome-horizon.json +0 -0
  165. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/data/genomes/telemetry-void.json +0 -0
  166. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/data/glyphs.json +0 -0
  167. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/data/motions/border/chromatic-pulse.yaml +0 -0
  168. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/data/motions/border/corner-trace.yaml +0 -0
  169. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/data/motions/border/dual-orbit.yaml +0 -0
  170. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/data/motions/border/entanglement.yaml +0 -0
  171. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/data/motions/border/rimrun.yaml +0 -0
  172. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/data/motions/kinetic/bars.yaml +0 -0
  173. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/data/motions/kinetic/breach.yaml +0 -0
  174. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/data/motions/kinetic/broadcast.yaml +0 -0
  175. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/data/motions/kinetic/cascade.yaml +0 -0
  176. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/data/motions/kinetic/collapse.yaml +0 -0
  177. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/data/motions/kinetic/converge.yaml +0 -0
  178. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/data/motions/kinetic/crash.yaml +0 -0
  179. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/data/motions/kinetic/drop.yaml +0 -0
  180. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/data/motions/kinetic/pulse.yaml +0 -0
  181. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/data/motions/static.yaml +0 -0
  182. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/data/policies/normal.json +0 -0
  183. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/data/policies/permissive.json +0 -0
  184. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/data/policies/ungoverned.json +0 -0
  185. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/data/profiles/brutalist.contract.json +0 -0
  186. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/data/profiles/brutalist.yaml +0 -0
  187. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/data/profiles/chrome.contract.json +0 -0
  188. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/data/profiles/chrome.yaml +0 -0
  189. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/data/specimens.yaml +0 -0
  190. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/data/telemetry/model-pricing.yaml +0 -0
  191. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/data/telemetry/stage-config.yaml +0 -0
  192. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/data/telemetry/stage-labels.yaml +0 -0
  193. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/data/telemetry/tool-classes.yaml +0 -0
  194. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/data/telemetry/tool-colors.yaml +0 -0
  195. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/data/terminals/arrow.json +0 -0
  196. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/data/terminals/aurora.json +0 -0
  197. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/data/terminals/beacon.json +0 -0
  198. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/data/terminals/bracket3d.json +0 -0
  199. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/data/terminals/chevron.json +0 -0
  200. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/data/terminals/crosshair.json +0 -0
  201. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/data/terminals/diamond.json +0 -0
  202. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/data/terminals/rocket.json +0 -0
  203. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/data/terminals/rules/bar.json +0 -0
  204. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/data/terminals/rules/dashed.json +0 -0
  205. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/data/terminals/rules/five-wave.json +0 -0
  206. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/data/terminals/rules/spectral.json +0 -0
  207. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/data/terminals/rules/straight.json +0 -0
  208. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/data/terminals/rules/wave.json +0 -0
  209. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/data/terminals/stijl.json +0 -0
  210. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/kit.py +0 -0
  211. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/mcp/__init__.py +0 -0
  212. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/mcp/__main__.py +0 -0
  213. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/mcp/server.py +0 -0
  214. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/py.typed +0 -0
  215. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/render/__init__.py +0 -0
  216. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/render/fonts.py +0 -0
  217. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/render/glyphs.py +0 -0
  218. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/render/motion.py +0 -0
  219. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/render/templates.py +0 -0
  220. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/serve/__init__.py +0 -0
  221. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/serve/app.py +0 -0
  222. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/telemetry/__init__.py +0 -0
  223. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/telemetry/capture.py +0 -0
  224. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/telemetry/contract.py +0 -0
  225. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/telemetry/corrections.py +0 -0
  226. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/telemetry/cost.py +0 -0
  227. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/telemetry/models.py +0 -0
  228. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/telemetry/parser.py +0 -0
  229. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/telemetry/stages.py +0 -0
  230. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/templates/components/filter.svg.j2 +0 -0
  231. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/templates/components/glyph-inline.svg.j2 +0 -0
  232. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/templates/components/glyph.svg.j2 +0 -0
  233. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/templates/components/gradient.svg.j2 +0 -0
  234. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/templates/components/metadata.svg.j2 +0 -0
  235. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/templates/components/metric.svg.j2 +0 -0
  236. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/templates/components/phase-segment.svg.j2 +0 -0
  237. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/templates/components/rule.svg.j2 +0 -0
  238. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/templates/components/status.svg.j2 +0 -0
  239. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/templates/components/terminal.svg.j2 +0 -0
  240. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/templates/components/treemap.svg.j2 +0 -0
  241. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/templates/document.svg.j2 +0 -0
  242. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/templates/error-badge.svg.j2 +0 -0
  243. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/templates/frames/badge/brutalist-content.j2 +0 -0
  244. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/templates/frames/badge/brutalist-defs.j2 +0 -0
  245. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/templates/frames/badge/default-content.j2 +0 -0
  246. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/templates/frames/badge/default-defs.j2 +0 -0
  247. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/templates/frames/badge.svg.j2 +0 -0
  248. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/templates/frames/banner/brutalist-defs.j2 +0 -0
  249. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/templates/frames/banner/chrome-defs.j2 +0 -0
  250. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/templates/frames/banner/default-defs.j2 +0 -0
  251. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/templates/frames/banner.svg.j2 +0 -0
  252. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/templates/frames/catalog.svg.j2 +0 -0
  253. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/templates/frames/chart/brutalist-content.j2 +0 -0
  254. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/templates/frames/chart/brutalist-defs.j2 +0 -0
  255. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/templates/frames/chart/chrome-content.j2 +0 -0
  256. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/templates/frames/chart/chrome-defs.j2 +0 -0
  257. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/templates/frames/chart.svg.j2 +0 -0
  258. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/templates/frames/divider.svg.j2 +0 -0
  259. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/templates/frames/icon/brutalist-content.j2 +0 -0
  260. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/templates/frames/icon/brutalist-defs.j2 +0 -0
  261. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/templates/frames/icon/chrome-content.j2 +0 -0
  262. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/templates/frames/icon/chrome-defs.j2 +0 -0
  263. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/templates/frames/icon/default-content.j2 +0 -0
  264. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/templates/frames/icon/default-defs.j2 +0 -0
  265. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/templates/frames/icon.svg.j2 +0 -0
  266. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/templates/frames/marquee-counter/brutalist-content.j2 +0 -0
  267. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/templates/frames/marquee-counter/brutalist-defs.j2 +0 -0
  268. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/templates/frames/marquee-counter/chrome-content.j2 +0 -0
  269. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/templates/frames/marquee-counter/chrome-defs.j2 +0 -0
  270. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/templates/frames/marquee-counter.svg.j2 +0 -0
  271. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/templates/frames/marquee-horizontal/brutalist-content.j2 +0 -0
  272. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/templates/frames/marquee-horizontal/brutalist-defs.j2 +0 -0
  273. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/templates/frames/marquee-horizontal/chrome-content.j2 +0 -0
  274. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/templates/frames/marquee-horizontal/chrome-defs.j2 +0 -0
  275. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/templates/frames/marquee-horizontal.svg.j2 +0 -0
  276. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/templates/frames/marquee-vertical/brutalist-content.j2 +0 -0
  277. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/templates/frames/marquee-vertical/brutalist-defs.j2 +0 -0
  278. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/templates/frames/marquee-vertical/chrome-content.j2 +0 -0
  279. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/templates/frames/marquee-vertical/chrome-defs.j2 +0 -0
  280. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/templates/frames/marquee-vertical.svg.j2 +0 -0
  281. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/templates/frames/master-card.svg.j2 +0 -0
  282. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/templates/frames/receipt.svg.j2 +0 -0
  283. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/templates/frames/rhythm-strip.svg.j2 +0 -0
  284. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/templates/frames/stats/brutalist-content.j2 +0 -0
  285. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/templates/frames/stats/brutalist-defs.j2 +0 -0
  286. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/templates/frames/stats.svg.j2 +0 -0
  287. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/templates/frames/strip/brutalist-content.j2 +0 -0
  288. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/templates/frames/strip/brutalist-status.j2 +0 -0
  289. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/templates/frames/strip/chrome-content.j2 +0 -0
  290. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/templates/frames/strip/chrome-status.j2 +0 -0
  291. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/templates/frames/strip/default-content.j2 +0 -0
  292. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/templates/frames/strip/default-defs.j2 +0 -0
  293. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/templates/frames/strip/default-status.j2 +0 -0
  294. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/templates/frames/timeline/default-content.j2 +0 -0
  295. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/templates/frames/timeline.svg.j2 +0 -0
  296. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/templates/motions/border/chromatic-pulse.svg.j2 +0 -0
  297. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/templates/motions/border/corner-trace.svg.j2 +0 -0
  298. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/templates/motions/border/dual-orbit.svg.j2 +0 -0
  299. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/templates/motions/border/entanglement.svg.j2 +0 -0
  300. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/templates/motions/border/rimrun.svg.j2 +0 -0
  301. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/templates/motions/kinetic/bars.svg.j2 +0 -0
  302. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/templates/motions/kinetic/breach.svg.j2 +0 -0
  303. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/templates/motions/kinetic/broadcast.svg.j2 +0 -0
  304. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/templates/motions/kinetic/cascade.svg.j2 +0 -0
  305. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/templates/motions/kinetic/collapse.svg.j2 +0 -0
  306. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/templates/motions/kinetic/converge.svg.j2 +0 -0
  307. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/templates/motions/kinetic/crash.svg.j2 +0 -0
  308. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/templates/motions/kinetic/drop.svg.j2 +0 -0
  309. {hyperweave-0.2.0 → hyperweave-0.2.1}/src/hyperweave/templates/motions/kinetic/pulse.svg.j2 +0 -0
  310. {hyperweave-0.2.0 → hyperweave-0.2.1}/tests/__init__.py +0 -0
  311. {hyperweave-0.2.0 → hyperweave-0.2.1}/tests/conftest.py +0 -0
  312. {hyperweave-0.2.0 → hyperweave-0.2.1}/tests/fixtures/github_contributions/synthetic.html +0 -0
  313. {hyperweave-0.2.0 → hyperweave-0.2.1}/tests/fixtures/session.json +0 -0
  314. {hyperweave-0.2.0 → hyperweave-0.2.1}/tests/fixtures/session.jsonl +0 -0
  315. {hyperweave-0.2.0 → hyperweave-0.2.1}/tests/test_chart_frame.py +0 -0
  316. {hyperweave-0.2.0 → hyperweave-0.2.1}/tests/test_connectors.py +0 -0
  317. {hyperweave-0.2.0 → hyperweave-0.2.1}/tests/test_core.py +0 -0
  318. {hyperweave-0.2.0 → hyperweave-0.2.1}/tests/test_kit.py +0 -0
  319. {hyperweave-0.2.0 → hyperweave-0.2.1}/tests/test_mcp.py +0 -0
  320. {hyperweave-0.2.0 → hyperweave-0.2.1}/tests/test_paradigm_dispatch.py +0 -0
  321. {hyperweave-0.2.0 → hyperweave-0.2.1}/tests/test_proofset.py +0 -0
  322. {hyperweave-0.2.0 → hyperweave-0.2.1}/tests/test_render.py +0 -0
  323. {hyperweave-0.2.0 → hyperweave-0.2.1}/tests/test_serve.py +0 -0
  324. {hyperweave-0.2.0 → hyperweave-0.2.1}/tests/test_stats_card.py +0 -0
  325. {hyperweave-0.2.0 → hyperweave-0.2.1}/tests/test_telemetry.py +0 -0
  326. {hyperweave-0.2.0 → hyperweave-0.2.1}/tests/test_timeline.py +0 -0
  327. {hyperweave-0.2.0 → hyperweave-0.2.1}/uv.lock +0 -0
@@ -5,6 +5,18 @@ All notable changes to HyperWeave are documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.2.1] - 2026-04-13
9
+
10
+ Post-v0.2.0 stabilization: typography alignment, mobile rendering fix, and a streak computation correction.
11
+
12
+ ### Fixed
13
+
14
+ - **Stats card "streak" reports 0d for active contributors.** The contribution-calendar parser walked backwards from the latest cell and broke on the first zero, which is always today's empty cell before the user has committed. The streak calculator now treats the most-recent cell as a single grace day — if today hasn't happened yet, the streak continues from yesterday. Any zero day after the first one still breaks the streak as before.
15
+ - **Activity bars blur on mobile.** The `barglow` filter on the stats card's 52-week activity bars used `feGaussianBlur`, which rasterizes to a pixel buffer and gets downsampled when the SVG is scaled to smaller mobile viewports — producing soft, fuzzy bars. Replaced with a pure-vector 2-layer halo (sibling rects at decreasing opacity). Same cyan halo aesthetic from the chrome-horizon magazine specimen, but crisp at every scale. The `{uid}-barglow` filter definition was removed from `stats/chrome-defs.j2`.
16
+ - **chrome-horizon badge and strip typography** now match the stats and chart cards introduced in v0.2.0. Badge values render in Orbitron (the bundled display font) instead of the prior Impact+skew treatment; strip metric values keep the shields.io-style Impact+skew but gain the silver `ct-hero` gradient fill, tying them visually to the hero numbers on the stats and chart frames. Identity and metric labels render in JetBrains Mono at the sizes and letter-spacing used by the chrome-horizon magazine specimen.
17
+ - Badge and strip chrome templates migrated to the same class-based `<style>` pattern stats and chart already use (`.{uid}-label`, `.{uid}-value`, `.{uid}-identity`, `.{uid}-metric-label`, `.{uid}-metric-value`). Inline `font-family` / `font-size` / `font-weight` attributes removed from `strip.svg.j2`, `badge/chrome-content.j2`, and replaced with class references defined in each paradigm's `*-defs.j2` file. This is chrome-paradigm-only — brutalist-emerald output is unchanged.
18
+ - Applied `ruff format` to three files added in v0.2.0 (`config/genome_validator.py`, `connectors/github.py`, `render/chart_engine.py`). No behavior change — CI's `ruff format --check` job runs separately from `ruff check` and was the only thing red on the v0.2.0 push.
19
+
8
20
  ## [0.2.0] - 2026-04-12
9
21
 
10
22
  Live-data profile artifacts. HyperWeave can now render GitHub profile cards, star-history charts, and milestone timelines directly from a single API call, the CLI, or an MCP tool. Genomes gain a per-frame paradigm layer so two genomes on the same profile can diverge structurally, not just chromatically. Custom genomes can be loaded from a local JSON file and validated against a profile contract. Fonts are bundled as base64 WOFF2 for fully self-contained SVGs. Test suite: 435 passing.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hyperweave
3
- Version: 0.2.0
3
+ Version: 0.2.1
4
4
  Summary: Headless visual output layer for AI agents. One API call → self-contained SVG.
5
5
  Project-URL: Repository, https://github.com/InnerAura/hyperweave
6
6
  Project-URL: Issues, https://github.com/InnerAura/hyperweave/issues
@@ -120,12 +120,12 @@ Why genome and not theme? Because brand isn't a design problem, it's an infrastr
120
120
  </tr>
121
121
  <tr>
122
122
  <td rowspan="2" align="center"><strong>Profile&nbsp;cards</strong></td>
123
- <td><img src="assets/examples/brutalist-emerald/profile-cards/stats.svg" alt="stats" width="100%"/></td>
124
- <td><img src="assets/examples/chrome-horizon/profile-cards/stats.svg" alt="stats" width="100%"/></td>
123
+ <td><img src="https://hyperweave.app/v1/stats/eli64s/brutalist-emerald.static" alt="stats" width="100%"/></td>
124
+ <td><img src="https://hyperweave.app/v1/stats/eli64s/chrome-horizon.static" alt="stats" width="100%"/></td>
125
125
  </tr>
126
126
  <tr>
127
- <td><img src="assets/examples/brutalist-emerald/profile-cards/chart_stars_full.svg" alt="star chart" width="100%"/></td>
128
- <td><img src="assets/examples/chrome-horizon/profile-cards/chart_stars_full.svg" alt="star chart" width="100%"/></td>
127
+ <td><img src="https://hyperweave.app/v1/chart/stars/eli64s/readme-ai/brutalist-emerald.static" alt="star chart" width="100%"/></td>
128
+ <td><img src="https://hyperweave.app/v1/chart/stars/eli64s/readme-ai/chrome-horizon.static" alt="star chart" width="100%"/></td>
129
129
  </tr>
130
130
  <tr>
131
131
  <td rowspan="3" align="center"><strong>Marquee</strong></td>
@@ -88,12 +88,12 @@ Why genome and not theme? Because brand isn't a design problem, it's an infrastr
88
88
  </tr>
89
89
  <tr>
90
90
  <td rowspan="2" align="center"><strong>Profile&nbsp;cards</strong></td>
91
- <td><img src="assets/examples/brutalist-emerald/profile-cards/stats.svg" alt="stats" width="100%"/></td>
92
- <td><img src="assets/examples/chrome-horizon/profile-cards/stats.svg" alt="stats" width="100%"/></td>
91
+ <td><img src="https://hyperweave.app/v1/stats/eli64s/brutalist-emerald.static" alt="stats" width="100%"/></td>
92
+ <td><img src="https://hyperweave.app/v1/stats/eli64s/chrome-horizon.static" alt="stats" width="100%"/></td>
93
93
  </tr>
94
94
  <tr>
95
- <td><img src="assets/examples/brutalist-emerald/profile-cards/chart_stars_full.svg" alt="star chart" width="100%"/></td>
96
- <td><img src="assets/examples/chrome-horizon/profile-cards/chart_stars_full.svg" alt="star chart" width="100%"/></td>
95
+ <td><img src="https://hyperweave.app/v1/chart/stars/eli64s/readme-ai/brutalist-emerald.static" alt="star chart" width="100%"/></td>
96
+ <td><img src="https://hyperweave.app/v1/chart/stars/eli64s/readme-ai/chrome-horizon.static" alt="star chart" width="100%"/></td>
97
97
  </tr>
98
98
  <tr>
99
99
  <td rowspan="3" align="center"><strong>Marquee</strong></td>
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '0.2.0'
22
- __version_tuple__ = version_tuple = (0, 2, 0)
21
+ __version__ = version = '0.2.1'
22
+ __version_tuple__ = version_tuple = (0, 2, 1)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -69,9 +69,7 @@ def load_and_validate_genome_file(
69
69
  ratio = contrast_ratio(fg, bg)
70
70
  min_ratio = pair["min_ratio"]
71
71
  if ratio < min_ratio:
72
- errors.append(
73
- f"WCAG FAIL: {pair['label']} — {ratio:.1f}:1 < {min_ratio}:1 ({fg} on {bg})"
74
- )
72
+ errors.append(f"WCAG FAIL: {pair['label']} — {ratio:.1f}:1 < {min_ratio}:1 ({fg} on {bg})")
75
73
  except (ValueError, TypeError):
76
74
  errors.append(f"INVALID COLOR: {pair['label']} — cannot parse {fg} or {bg}")
77
75
 
@@ -247,7 +247,7 @@ _CELL_RE = re.compile(
247
247
  r'<td(?=[^>]*\bclass="[^"]*ContributionCalendar-day[^"]*")'
248
248
  r'(?=[^>]*\bdata-date="(?P<date>\d{4}-\d{2}-\d{2})")'
249
249
  r'(?=[^>]*\bdata-level="(?P<level>\d+)")'
250
- r'[^>]*>',
250
+ r"[^>]*>",
251
251
  re.IGNORECASE,
252
252
  )
253
253
 
@@ -257,7 +257,7 @@ _CELL_RE = re.compile(
257
257
  # format, not ISO, so we do NOT try to extract the date from the tooltip —
258
258
  # instead we pair tooltips and cells positionally in document order.
259
259
  _COUNT_TOOLTIP_RE = re.compile(
260
- r'<tool-tip[^>]*>\s*(?P<count>\d+|No)\s+contributions?',
260
+ r"<tool-tip[^>]*>\s*(?P<count>\d+|No)\s+contributions?",
261
261
  re.IGNORECASE,
262
262
  )
263
263
 
@@ -298,11 +298,18 @@ def parse_contribution_html(html: str) -> dict[str, Any]:
298
298
 
299
299
  contrib_total = sum(c["count"] for c in cells)
300
300
 
301
- # Streak: walk backwards from the latest cell, counting consecutive non-zero days.
301
+ # Streak: walk backwards from the latest cell, counting consecutive
302
+ # non-zero days. The most recent cell (today) is allowed to be zero
303
+ # as a grace day — GitHub renders today's empty cell before the user
304
+ # has committed today, and we don't want a morning stats check to
305
+ # report a false 0d streak for an otherwise active contributor.
306
+ # Any zero day AFTER the first one still breaks the streak.
302
307
  streak = 0
303
- for cell in reversed(cells):
308
+ for i, cell in enumerate(reversed(cells)):
304
309
  if cell["count"] > 0:
305
310
  streak += 1
311
+ elif i == 0:
312
+ continue
306
313
  else:
307
314
  break
308
315
 
@@ -198,7 +198,7 @@ def _marker_rect(x: int, y: int, size: int) -> str:
198
198
  f'stroke="var(--dna-signal-dim,var(--dna-signal))" stroke-width="1"/>'
199
199
  f'<line x1="0" y1="-{cross}" x2="0" y2="{cross}" '
200
200
  f'stroke="var(--dna-signal-dim,var(--dna-signal))" stroke-width="1"/>'
201
- f'</g>'
201
+ f"</g>"
202
202
  )
203
203
 
204
204
 
@@ -210,10 +210,7 @@ def _marker_circle(x: int, y: int, size: int) -> str:
210
210
  def _marker_diamond(x: int, y: int, size: int) -> str:
211
211
  """Chrome diamond marker: white-filled rotated rect with dark stroke."""
212
212
  half = size // 2
213
- return (
214
- f'<rect x="-{half}" y="-{half}" width="{size}" height="{size}" '
215
- f'transform="translate({x} {y}) rotate(45)"/>'
216
- )
213
+ return f'<rect x="-{half}" y="-{half}" width="{size}" height="{size}" transform="translate({x} {y}) rotate(45)"/>'
217
214
 
218
215
 
219
216
  _MARKER_BUILDERS = {
@@ -238,7 +235,7 @@ def _marker_endpoint_rect(x: int, y: int, size: int) -> str:
238
235
  f'fill="var(--dna-surface)"/>'
239
236
  f'<rect x="{x - h3}" y="{y - h3}" width="{s3}" height="{s3}" '
240
237
  f'fill="var(--dna-ink-primary,#D1FAE5)"/>'
241
- f'</g>'
238
+ f"</g>"
242
239
  )
243
240
 
244
241
 
@@ -254,7 +251,7 @@ def _marker_endpoint_diamond(x: int, y: int, size: int) -> str:
254
251
  f'fill="#38BDF8"/>'
255
252
  f'<rect x="-{h2}" y="-{h2}" width="{s2}" height="{s2}" rx="0.4" '
256
253
  f'transform="rotate(45)" fill="#FFFFFF" stroke="#000A14" stroke-width="1"/>'
257
- f'</g>'
254
+ f"</g>"
258
255
  )
259
256
 
260
257
 
@@ -308,10 +305,7 @@ def _build_gridlines(vp: Viewport, rows: int = 4) -> str:
308
305
  lines: list[str] = []
309
306
  for i in range(1, rows + 1):
310
307
  y = vp.y + round(vp.h * i / (rows + 1))
311
- lines.append(
312
- f'<line x1="{vp.x}" y1="{y}" x2="{vp.x + vp.w}" y2="{y}" '
313
- f'class="hw-chart-gridline"/>'
314
- )
308
+ lines.append(f'<line x1="{vp.x}" y1="{y}" x2="{vp.x + vp.w}" y2="{y}" class="hw-chart-gridline"/>')
315
309
  return "".join(lines)
316
310
 
317
311
 
@@ -415,15 +409,13 @@ def build_chart_svg(
415
409
  pts = _build_area_polygon_points(projected, baseline_y)
416
410
  if pts:
417
411
  area_fragment = (
418
- f'<polygon points="{pts}" fill="var(--dna-signal)" '
419
- f'fill-opacity="0.65" class="hw-chart-area"/>'
412
+ f'<polygon points="{pts}" fill="var(--dna-signal)" fill-opacity="0.65" class="hw-chart-area"/>'
420
413
  )
421
414
  elif fill_density == "bezier-smooth":
422
415
  path_d = _build_area_path(projected, baseline_y)
423
416
  if path_d:
424
417
  area_fragment = (
425
- f'<path d="{path_d}" fill="var(--dna-signal)" '
426
- f'fill-opacity="0.12" class="hw-chart-area-smooth"/>'
418
+ f'<path d="{path_d}" fill="var(--dna-signal)" fill-opacity="0.12" class="hw-chart-area-smooth"/>'
427
419
  )
428
420
 
429
421
  markers_fragment = _build_markers(projected, shape, point_size)
@@ -24,20 +24,20 @@
24
24
  </g>
25
25
  {% endif %}
26
26
 
27
- {# Label #}
27
+ {# Label — class-styled for chrome paradigm (matches stats/chart typography) #}
28
28
  <text data-hw-zone="label"
29
29
  x="{{ label_x }}" y="{{ (h / 2) + 1 }}"
30
30
  text-anchor="middle" dominant-baseline="central"
31
- font-family="var(--dna-font-mono)" font-size="8" font-weight="600"
32
- letter-spacing="0.10em" fill="var(--dna-signal)">{{ label_display | default(label) | upper }}</text>
31
+ class="{{ uid }}-label"
32
+ fill="var(--dna-signal)">{{ label_display | default(label) | upper }}</text>
33
33
 
34
- {# Value #}
35
- <g transform="translate({{ value_x }},{{ h / 2 }}) skewX(-8)">
36
- <text data-hw-zone="value" x="0" y="0"
37
- text-anchor="middle" dominant-baseline="central"
38
- font-family="'Impact','Arial Black',system-ui,sans-serif" font-size="11" font-weight="700"
39
- fill="{{ glyph_fill | default('var(--dna-ink-primary)') }}" letter-spacing=".4">{{ value }}</text>
40
- </g>
34
+ {# Value — class-styled, no skew. Magazine specimen uses Orbitron here for
35
+ geometric clarity at small sizes; Impact+skew was the pre-v0.2.2 treatment. #}
36
+ <text data-hw-zone="value"
37
+ x="{{ value_x }}" y="{{ h / 2 }}"
38
+ text-anchor="middle" dominant-baseline="central"
39
+ class="{{ uid }}-value"
40
+ fill="{{ glyph_fill | default('var(--dna-ink-primary)') }}">{{ value }}</text>
41
41
 
42
42
  {# Diamond status indicator #}
43
43
  {% if show_indicator | default(true) %}
@@ -35,4 +35,25 @@
35
35
  <stop offset="70%" stop-color="var(--dna-signal)" stop-opacity="0.35"/>
36
36
  <stop offset="100%" stop-color="var(--dna-signal)" stop-opacity="0"/>
37
37
  </linearGradient>
38
+ {# ── Hero text gradient (silver, matches stats/chart hero values) ── #}
39
+ <linearGradient id="{{ uid }}-ct-hero" x1="0" y1="0" x2="1" y2="0">
40
+ {% for stop in hero_text_gradient | default([
41
+ {"offset":"0%","color":"#94A3B8"},
42
+ {"offset":"50%","color":"#FFFFFF"},
43
+ {"offset":"100%","color":"#94A3B8"}
44
+ ]) %}
45
+ <stop offset="{{ stop.offset }}" stop-color="{{ stop.color }}"/>
46
+ {% endfor %}
47
+ </linearGradient>
48
+ {# ── Typography (class-based, matches stats/chart pattern) ── #}
49
+ <style>
50
+ .{{ uid }}-label {
51
+ font-family: var(--dna-font-mono, 'JetBrains Mono', 'SF Mono', monospace);
52
+ font-size: 11px; font-weight: 600; letter-spacing: 0.10em;
53
+ }
54
+ .{{ uid }}-value {
55
+ font-family: var(--dna-font-display, 'Orbitron', 'Space Grotesk', sans-serif);
56
+ font-size: 17px; font-weight: 900; letter-spacing: 0.02em;
57
+ }
58
+ </style>
38
59
  <clipPath id="{{ uid }}-clip"><rect width="{{ width }}" height="{{ height }}" rx="{{ chrome_corner | default('4') }}"/></clipPath>
@@ -78,7 +78,10 @@
78
78
  <line x1="{{ (ch_start_x + q * ch_stride) | round(1) }}" y1="{{ ch_baseline + 1.5 }}" x2="{{ (ch_start_x + q * ch_stride) | round(1) }}" y2="{{ ch_baseline + 4 }}" stroke="#A8C0D0" stroke-width="0.4" opacity="0.5"/>
79
79
  {% endfor %}
80
80
 
81
- <g data-hw-zone="activity-bars" fill="url(#{{ uid }}-bar)" filter="url(#{{ uid }}-barglow)">
81
+ {# Activity bars 2-layer vector halo replaces feGaussianBlur so the bars
82
+ stay crisp when the SVG is scaled down on mobile (iOS Safari downsamples
83
+ filter-rasterized buffers, which fuzzed the 7px-wide bars). #}
84
+ <g data-hw-zone="activity-bars">
82
85
  {% for bar in activity_bars %}
83
86
  {# Zero-count weeks render as 2px stubs; non-zero weeks use sqrt scaling. #}
84
87
  {% if bar.count == 0 %}
@@ -86,7 +89,17 @@
86
89
  {% else %}
87
90
  {% set h = [4, ((bar.count / ch_peak) ** 0.5 * ch_max_h) | int] | max %}
88
91
  {% endif %}
89
- <rect x="{{ (ch_start_x + loop.index0 * ch_stride) | round(1) }}" y="{{ ch_baseline - h }}" width="{{ ch_bar_w }}" height="{{ h }}" rx="1"/>
92
+ {% set bx = (ch_start_x + loop.index0 * ch_stride) | round(1) %}
93
+ {% set by = ch_baseline - h %}
94
+ {# Outer halo — soft wide glow #}
95
+ <rect x="{{ bx - 2 }}" y="{{ by - 2 }}" width="{{ ch_bar_w + 4 }}" height="{{ h + 4 }}" rx="2"
96
+ fill="#A8D4F0" opacity="0.10"/>
97
+ {# Inner halo — tighter brighter glow #}
98
+ <rect x="{{ bx - 1 }}" y="{{ by - 1 }}" width="{{ ch_bar_w + 2 }}" height="{{ h + 2 }}" rx="1.5"
99
+ fill="#A8D4F0" opacity="0.22"/>
100
+ {# Bar body — gradient fill #}
101
+ <rect x="{{ bx }}" y="{{ by }}" width="{{ ch_bar_w }}" height="{{ h }}" rx="1"
102
+ fill="url(#{{ uid }}-bar)" shape-rendering="geometricPrecision"/>
90
103
  {% endfor %}
91
104
  </g>
92
105
 
@@ -98,15 +98,13 @@
98
98
  </feMerge>
99
99
  </filter>
100
100
 
101
- <filter id="{{ uid }}-barglow" x="-5%" y="-20%" width="110%" height="140%">
102
- <feGaussianBlur in="SourceAlpha" stdDeviation="0.8" result="blur"/>
103
- <feFlood flood-color="#A8D4F0" flood-opacity="0.35"/>
104
- <feComposite in2="blur" operator="in" result="glow"/>
105
- <feMerge>
106
- <feMergeNode in="glow"/>
107
- <feMergeNode in="SourceGraphic"/>
108
- </feMerge>
109
- </filter>
101
+ {# NOTE: the magazine specimen uses an feGaussianBlur-based `barglow` filter
102
+ for the activity bars. That filter rasterizes to a pixel buffer and then
103
+ resamples when the SVG is downscaled on mobile, which produces a soft,
104
+ blurry appearance on small viewports. The stats card is commonly embedded
105
+ in README profiles viewed on phones, so we replace the filter with a
106
+ pure-vector 2-layer halo (sibling rects at decreasing opacity) — same
107
+ cyan halo aesthetic, crisp at every scale. #}
110
108
 
111
109
  <clipPath id="{{ uid }}-clip"><rect width="{{ width }}" height="{{ height }}" rx="6"/></clipPath>
112
110
 
@@ -0,0 +1,26 @@
1
+ {# Brutalist strip defs: plate gradient. #}
2
+ <linearGradient id="{{ uid }}-plate" x1="0" x2="0" y1="0" y2="1">
3
+ <stop offset="0%" style="stop-color: var(--dna-surface)"/>
4
+ <stop offset="40%" style="stop-color: var(--dna-surface-alt)"/>
5
+ <stop offset="100%" style="stop-color: var(--dna-surface-deep)"/>
6
+ </linearGradient>
7
+ {# Typography (class-based, preserves pre-v0.2.2 brutalist strip rendering).
8
+ strip.svg.j2 emits class names on identity / metric text; this block defines
9
+ what those classes look like under the default/brutalist paradigm. #}
10
+ <style>
11
+ .{{ uid }}-identity {
12
+ font-family: var(--dna-font-mono, 'SF Mono', monospace);
13
+ font-size: 11px; font-weight: 900; letter-spacing: 0.18em;
14
+ fill: var(--dna-brand-text);
15
+ }
16
+ .{{ uid }}-metric-label {
17
+ font-family: var(--dna-font-mono, 'SF Mono', monospace);
18
+ font-size: 7px; font-weight: 700; letter-spacing: 0.2em;
19
+ fill: var(--dna-ink-muted);
20
+ }
21
+ .{{ uid }}-metric-value {
22
+ font-family: var(--dna-font-display, 'Inter', system-ui);
23
+ font-size: 18px; font-weight: 900; letter-spacing: -0.01em;
24
+ fill: var(--dna-ink-primary);
25
+ }
26
+ </style>
@@ -48,3 +48,38 @@
48
48
  <filter id="{{ uid }}-txt" x="-6%" y="-18%" width="112%" height="136%">
49
49
  <feDropShadow dx="0" dy="0" stdDeviation="0.6" flood-color="var(--dna-surface, #000A14)" flood-opacity="0.9"/>
50
50
  </filter>
51
+ {# Hero text gradient (silver — matches stats/chart hero values) #}
52
+ <linearGradient id="{{ uid }}-ct-hero" x1="0" y1="0" x2="1" y2="0">
53
+ {% for stop in hero_text_gradient | default([
54
+ {"offset":"0%","color":"#94A3B8"},
55
+ {"offset":"20%","color":"#CBD5E1"},
56
+ {"offset":"40%","color":"#F1F5F9"},
57
+ {"offset":"50%","color":"#FFFFFF"},
58
+ {"offset":"60%","color":"#F1F5F9"},
59
+ {"offset":"80%","color":"#CBD5E1"},
60
+ {"offset":"100%","color":"#94A3B8"}
61
+ ]) %}
62
+ <stop offset="{{ stop.offset }}" stop-color="{{ stop.color }}"/>
63
+ {% endfor %}
64
+ </linearGradient>
65
+ {# Typography (class-based, matches stats/chart pattern).
66
+ Magazine specimen: strip identity + metric-label use JetBrains Mono; metric-value
67
+ keeps shields.io-style Impact+skew but with silver hero gradient fill tying it
68
+ visually to stats/chart hero numbers. #}
69
+ <style>
70
+ .{{ uid }}-identity {
71
+ font-family: var(--dna-font-mono, 'JetBrains Mono', monospace);
72
+ font-size: 11px; font-weight: 900; letter-spacing: 0.18em;
73
+ fill: var(--dna-brand-text);
74
+ }
75
+ .{{ uid }}-metric-label {
76
+ font-family: var(--dna-font-mono, 'JetBrains Mono', monospace);
77
+ font-size: 10px; font-weight: 700; letter-spacing: 0.14em;
78
+ fill: var(--dna-signal);
79
+ }
80
+ .{{ uid }}-metric-value {
81
+ font-family: 'Impact', 'Arial Black', system-ui, sans-serif;
82
+ font-size: 22px; font-weight: 800; letter-spacing: -0.01em;
83
+ fill: url(#{{ uid }}-ct-hero);
84
+ }
85
+ </style>
@@ -59,10 +59,7 @@
59
59
  <g clip-path="url(#{{ uid }}-id-clip)">
60
60
  <text data-hw-zone="identity"
61
61
  x="{{ identity_x }}" y="{{ (strip_h // 2) + 4 }}"
62
- font-family="{{ strip_identity_font | default('var(--dna-font-mono, SF Mono, monospace)') }}"
63
- font-size="11" font-weight="{{ strip_identity_weight | default(900) }}"
64
- fill="{{ strip_identity_fill | default('var(--dna-brand-text)') }}"
65
- letter-spacing="{{ strip_identity_letter_spacing | default('0.18em') }}">{{ identity | upper }}</text>
62
+ class="{{ uid }}-identity">{{ identity | upper }}</text>
66
63
  </g>
67
64
 
68
65
  {# === Metric cells with dividers === #}
@@ -86,16 +83,18 @@
86
83
  {% set cell_x = cell_start + (loop.index0 * metric_pitch) %}
87
84
  <g data-hw-zone="metric-{{ loop.index0 }}" transform="translate({{ cell_x }},0)">
88
85
  <text x="{{ metric_pitch // 2 }}" y="{{ strip_metric_label_y | default(18) }}" text-anchor="middle"
89
- font-family="{{ strip_metric_label_font | default('var(--dna-font-mono, SF Mono, monospace)') }}"
90
- font-size="{{ strip_metric_label_size | default(7) }}" font-weight="700"
91
- fill="{{ strip_metric_label_fill | default('var(--dna-ink-muted)') }}"
92
- letter-spacing="{{ strip_metric_label_letter_spacing | default('0.2em') }}">{{ metric.label | upper }}</text>
86
+ class="{{ uid }}-metric-label">{{ metric.label | upper }}</text>
87
+ {# Metric value chrome paradigm gets a translate+skew group so the skew
88
+ pivots around the text center. Other paradigms render upright. #}
89
+ {% if paradigm_strip == "chrome" %}
90
+ <g transform="translate({{ metric_pitch // 2 }},{{ strip_metric_value_y | default(36) }}) skewX(-8)">
91
+ <text x="0" y="0" text-anchor="middle" dominant-baseline="central"
92
+ class="{{ uid }}-metric-value">{{ metric.value }}</text>
93
+ </g>
94
+ {% else %}
93
95
  <text x="{{ metric_pitch // 2 }}" y="{{ strip_metric_value_y | default(36) }}" text-anchor="middle"
94
- font-family="var(--dna-font-display, 'Inter', system-ui)"
95
- font-size="18" font-weight="{{ strip_metric_value_weight | default(900) }}"
96
- fill="{{ strip_metric_value_fill | default(glyph_fill | default('var(--dna-ink-primary)')) }}"
97
- letter-spacing="-0.01em"
98
- {% if strip_metric_value_skew | default(0) %}transform="skewX({{ strip_metric_value_skew }})"{% endif %}>{{ metric.value }}</text>
96
+ class="{{ uid }}-metric-value">{{ metric.value }}</text>
97
+ {% endif %}
99
98
  {% if metric.delta %}
100
99
  <text x="{{ metric_pitch // 2 }}" y="48" text-anchor="middle"
101
100
  class="hw-delta hw-delta--{{ metric.delta_dir | default('neutral') }}">{{ metric.delta }}</text>
@@ -55,5 +55,4 @@ def test_non_stateful_frame_omits_status_and_expression() -> None:
55
55
  assert ".hw-value" not in css, "Divider should not include expression layer"
56
56
 
57
57
  # Bridge classes should NOT be present (divider is not in bridge frames)
58
- assert ".hw-frame-bg" not in css or "divider" in css, \
59
- "Divider should not include bridge classes"
58
+ assert ".hw-frame-bg" not in css or "divider" in css, "Divider should not include bridge classes"
@@ -161,7 +161,7 @@ def test_build_markers_square_emits_crosshair() -> None:
161
161
  out = _build_markers([(50, 50), (100, 200)], shape="square", size=10)
162
162
  assert "<rect" in out
163
163
  # Non-final marker: translate-centered group with crosshair lines.
164
- assert 'translate(50,50)' in out
164
+ assert "translate(50,50)" in out
165
165
  assert 'width="10"' in out
166
166
  assert "<line" in out # crosshair lines present
167
167
  # Final marker: endpoint beacon (nested squares).
@@ -236,7 +236,7 @@ def test_build_chart_svg_miter_angular(
236
236
  },
237
237
  )
238
238
  assert "<polyline" in result["polyline"]
239
- assert "stroke-linejoin=\"miter\"" in result["polyline"]
239
+ assert 'stroke-linejoin="miter"' in result["polyline"]
240
240
  assert "<polygon" in result["area"]
241
241
  assert "<rect" in result["markers"]
242
242
 
@@ -58,6 +58,47 @@ def test_parse_streak_walks_backwards(synthetic_html: str) -> None:
58
58
  assert parsed["streak_days"] == 3
59
59
 
60
60
 
61
+ def test_parse_streak_grace_day_for_empty_today() -> None:
62
+ """The most-recent cell may be zero without breaking the streak.
63
+
64
+ GitHub renders today's empty cell at the rightmost position before the
65
+ user has committed today. A morning stats check on an otherwise active
66
+ contributor should NOT report streak=0 just because today hasn't happened
67
+ yet. Subsequent zeros still break the streak.
68
+ """
69
+ html = (
70
+ '<td class="ContributionCalendar-day" data-date="2025-04-08" data-level="0">&nbsp;</td>'
71
+ '<td class="ContributionCalendar-day" data-date="2025-04-09" data-level="2">&nbsp;</td>'
72
+ '<td class="ContributionCalendar-day" data-date="2025-04-10" data-level="3">&nbsp;</td>'
73
+ '<td class="ContributionCalendar-day" data-date="2025-04-11" data-level="4">&nbsp;</td>'
74
+ '<td class="ContributionCalendar-day" data-date="2025-04-12" data-level="0">&nbsp;</td>'
75
+ '<tool-tip>No contributions on April 8</tool-tip>'
76
+ '<tool-tip>5 contributions on April 9</tool-tip>'
77
+ '<tool-tip>12 contributions on April 10</tool-tip>'
78
+ '<tool-tip>30 contributions on April 11</tool-tip>'
79
+ '<tool-tip>No contributions on April 12</tool-tip>'
80
+ )
81
+ parsed = parse_contribution_html(html)
82
+ # Today (Apr 12) is empty → grace. Apr 11 (30), Apr 10 (12), Apr 9 (5) are
83
+ # all non-zero → streak = 3. Apr 8 is zero → break.
84
+ assert parsed["streak_days"] == 3
85
+
86
+
87
+ def test_parse_streak_two_consecutive_zeros_break() -> None:
88
+ """Grace applies to the single most-recent cell only, not cumulative."""
89
+ html = (
90
+ '<td class="ContributionCalendar-day" data-date="2025-04-10" data-level="3">&nbsp;</td>'
91
+ '<td class="ContributionCalendar-day" data-date="2025-04-11" data-level="0">&nbsp;</td>'
92
+ '<td class="ContributionCalendar-day" data-date="2025-04-12" data-level="0">&nbsp;</td>'
93
+ '<tool-tip>12 contributions on April 10</tool-tip>'
94
+ '<tool-tip>No contributions on April 11</tool-tip>'
95
+ '<tool-tip>No contributions on April 12</tool-tip>'
96
+ )
97
+ parsed = parse_contribution_html(html)
98
+ # Today (Apr 12) is grace-zero, Apr 11 is also zero → streak breaks.
99
+ assert parsed["streak_days"] == 0
100
+
101
+
61
102
  def test_parse_chronological_order(synthetic_html: str) -> None:
62
103
  parsed = parse_contribution_html(synthetic_html)
63
104
  dates = [c["date"] for c in parsed["heatmap_grid"]]
@@ -260,8 +260,7 @@ def test_receipt_tier2_vertical_stacking() -> None:
260
260
  assert len(tier2_cells) >= 2, f"Expected >=2 tier-2 cells, got {len(tier2_cells)}"
261
261
  for name_y, name, meta_y in tier2_cells:
262
262
  assert name_y != meta_y, (
263
- f"Tier-2 cell '{name}' has name_y={name_y} == meta_y={meta_y} — "
264
- f"text collision (stacking regression)"
263
+ f"Tier-2 cell '{name}' has name_y={name_y} == meta_y={meta_y} — text collision (stacking regression)"
265
264
  )
266
265
 
267
266
 
@@ -287,9 +286,7 @@ def test_receipt_treemap_error_annotations() -> None:
287
286
 
288
287
  # Every tool with errors must have a matching annotation
289
288
  for name, count in expected_errors.items():
290
- assert str(count) in error_annotations, (
291
- f"Expected ✗{count} for {name}, got annotations: {error_annotations}"
292
- )
289
+ assert str(count) in error_annotations, f"Expected ✗{count} for {name}, got annotations: {error_annotations}"
293
290
 
294
291
  # No extra annotations beyond what the fixture defines
295
292
  assert len(error_annotations) == len(expected_errors), (
@@ -300,4 +297,4 @@ def test_receipt_treemap_error_annotations() -> None:
300
297
  for m in re.finditer(r'<g clip-path="[^"]*">(.*?)</g>', svg, re.DOTALL):
301
298
  clip_content = m.group(1)
302
299
  if "cell-error" in clip_content:
303
- assert "text-anchor=\"end\"" in clip_content, "Error text must be right-aligned"
300
+ assert 'text-anchor="end"' in clip_content, "Error text must be right-aligned"
@@ -1,6 +0,0 @@
1
- {# Brutalist strip defs: plate gradient. #}
2
- <linearGradient id="{{ uid }}-plate" x1="0" x2="0" y1="0" y2="1">
3
- <stop offset="0%" style="stop-color: var(--dna-surface)"/>
4
- <stop offset="40%" style="stop-color: var(--dna-surface-alt)"/>
5
- <stop offset="100%" style="stop-color: var(--dna-surface-deep)"/>
6
- </linearGradient>
File without changes
File without changes
File without changes
File without changes