hyperweave 0.2.8__tar.gz → 0.2.9__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 (360) hide show
  1. {hyperweave-0.2.8 → hyperweave-0.2.9}/CHANGELOG.md +11 -0
  2. {hyperweave-0.2.8 → hyperweave-0.2.9}/PKG-INFO +1 -1
  3. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/_version.py +2 -2
  4. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/connectors/github.py +131 -99
  5. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/render/chart_engine.py +86 -26
  6. {hyperweave-0.2.8 → hyperweave-0.2.9}/tests/test_chart_engine.py +110 -20
  7. {hyperweave-0.2.8 → hyperweave-0.2.9}/tests/test_connectors.py +155 -69
  8. {hyperweave-0.2.8 → hyperweave-0.2.9}/.dockerignore +0 -0
  9. {hyperweave-0.2.8 → hyperweave-0.2.9}/.github/workflows/ci.yml +0 -0
  10. {hyperweave-0.2.8 → hyperweave-0.2.9}/.github/workflows/deploy.yml +0 -0
  11. {hyperweave-0.2.8 → hyperweave-0.2.9}/.github/workflows/publish.yml +0 -0
  12. {hyperweave-0.2.8 → hyperweave-0.2.9}/.gitignore +0 -0
  13. {hyperweave-0.2.8 → hyperweave-0.2.9}/.pre-commit-config.yaml +0 -0
  14. {hyperweave-0.2.8 → hyperweave-0.2.9}/Dockerfile +0 -0
  15. {hyperweave-0.2.8 → hyperweave-0.2.9}/LICENSE +0 -0
  16. {hyperweave-0.2.8 → hyperweave-0.2.9}/README.md +0 -0
  17. {hyperweave-0.2.8 → hyperweave-0.2.9}/assets/buttons/button-liquid.svg +0 -0
  18. {hyperweave-0.2.8 → hyperweave-0.2.9}/assets/cards/card-butterfly.svg +0 -0
  19. {hyperweave-0.2.8 → hyperweave-0.2.9}/assets/cards/card-python.svg +0 -0
  20. {hyperweave-0.2.8 → hyperweave-0.2.9}/assets/cards/card-sunflower.svg +0 -0
  21. {hyperweave-0.2.8 → hyperweave-0.2.9}/assets/cards/card-waves.svg +0 -0
  22. {hyperweave-0.2.8 → hyperweave-0.2.9}/assets/examples/brutalist-emerald/badge_critical.svg +0 -0
  23. {hyperweave-0.2.8 → hyperweave-0.2.9}/assets/examples/brutalist-emerald/badge_passing.svg +0 -0
  24. {hyperweave-0.2.8 → hyperweave-0.2.9}/assets/examples/brutalist-emerald/badge_warning.svg +0 -0
  25. {hyperweave-0.2.8 → hyperweave-0.2.9}/assets/examples/brutalist-emerald/banner.svg +0 -0
  26. {hyperweave-0.2.8 → hyperweave-0.2.9}/assets/examples/brutalist-emerald/icons/discord.svg +0 -0
  27. {hyperweave-0.2.8 → hyperweave-0.2.9}/assets/examples/brutalist-emerald/icons/notion.svg +0 -0
  28. {hyperweave-0.2.8 → hyperweave-0.2.9}/assets/examples/brutalist-emerald/icons/reddit.svg +0 -0
  29. {hyperweave-0.2.8 → hyperweave-0.2.9}/assets/examples/brutalist-emerald/icons/spotify.svg +0 -0
  30. {hyperweave-0.2.8 → hyperweave-0.2.9}/assets/examples/brutalist-emerald/icons/youtube.svg +0 -0
  31. {hyperweave-0.2.8 → hyperweave-0.2.9}/assets/examples/brutalist-emerald/marquee_counter.svg +0 -0
  32. {hyperweave-0.2.8 → hyperweave-0.2.9}/assets/examples/brutalist-emerald/marquee_horizontal.svg +0 -0
  33. {hyperweave-0.2.8 → hyperweave-0.2.9}/assets/examples/brutalist-emerald/marquee_vertical.svg +0 -0
  34. {hyperweave-0.2.8 → hyperweave-0.2.9}/assets/examples/brutalist-emerald/profile-cards/chart_stars_full.svg +0 -0
  35. {hyperweave-0.2.8 → hyperweave-0.2.9}/assets/examples/brutalist-emerald/profile-cards/stats.svg +0 -0
  36. {hyperweave-0.2.8 → hyperweave-0.2.9}/assets/examples/brutalist-emerald/strip.svg +0 -0
  37. {hyperweave-0.2.8 → hyperweave-0.2.9}/assets/examples/chrome-horizon/badge_critical.svg +0 -0
  38. {hyperweave-0.2.8 → hyperweave-0.2.9}/assets/examples/chrome-horizon/badge_passing.svg +0 -0
  39. {hyperweave-0.2.8 → hyperweave-0.2.9}/assets/examples/chrome-horizon/badge_warning.svg +0 -0
  40. {hyperweave-0.2.8 → hyperweave-0.2.9}/assets/examples/chrome-horizon/banner.svg +0 -0
  41. {hyperweave-0.2.8 → hyperweave-0.2.9}/assets/examples/chrome-horizon/icons/bluesky.svg +0 -0
  42. {hyperweave-0.2.8 → hyperweave-0.2.9}/assets/examples/chrome-horizon/icons/github.svg +0 -0
  43. {hyperweave-0.2.8 → hyperweave-0.2.9}/assets/examples/chrome-horizon/icons/instagram.svg +0 -0
  44. {hyperweave-0.2.8 → hyperweave-0.2.9}/assets/examples/chrome-horizon/icons/mastodon.svg +0 -0
  45. {hyperweave-0.2.8 → hyperweave-0.2.9}/assets/examples/chrome-horizon/icons/x.svg +0 -0
  46. {hyperweave-0.2.8 → hyperweave-0.2.9}/assets/examples/chrome-horizon/marquee_counter.svg +0 -0
  47. {hyperweave-0.2.8 → hyperweave-0.2.9}/assets/examples/chrome-horizon/marquee_horizontal.svg +0 -0
  48. {hyperweave-0.2.8 → hyperweave-0.2.9}/assets/examples/chrome-horizon/marquee_vertical.svg +0 -0
  49. {hyperweave-0.2.8 → hyperweave-0.2.9}/assets/examples/chrome-horizon/profile-cards/chart_stars_full.svg +0 -0
  50. {hyperweave-0.2.8 → hyperweave-0.2.9}/assets/examples/chrome-horizon/profile-cards/stats.svg +0 -0
  51. {hyperweave-0.2.8 → hyperweave-0.2.9}/assets/examples/chrome-horizon/strip.svg +0 -0
  52. {hyperweave-0.2.8 → hyperweave-0.2.9}/assets/examples/telemetry/master_card.svg +0 -0
  53. {hyperweave-0.2.8 → hyperweave-0.2.9}/assets/examples/telemetry/receipt.svg +0 -0
  54. {hyperweave-0.2.8 → hyperweave-0.2.9}/assets/examples/telemetry/rhythm_strip.svg +0 -0
  55. {hyperweave-0.2.8 → hyperweave-0.2.9}/assets/footers/inneraura-footer-liquid.svg +0 -0
  56. {hyperweave-0.2.8 → hyperweave-0.2.9}/assets/footers/inneraura-footer-purple.svg +0 -0
  57. {hyperweave-0.2.8 → hyperweave-0.2.9}/assets/footers/inneraura-footer.svg +0 -0
  58. {hyperweave-0.2.8 → hyperweave-0.2.9}/assets/hyperweave-banner.svg +0 -0
  59. {hyperweave-0.2.8 → hyperweave-0.2.9}/assets/icons/cobalt-sapphire-discord.svg +0 -0
  60. {hyperweave-0.2.8 → hyperweave-0.2.9}/assets/icons/cobalt-sapphire-docs.svg +0 -0
  61. {hyperweave-0.2.8 → hyperweave-0.2.9}/assets/icons/cobalt-sapphire-github.svg +0 -0
  62. {hyperweave-0.2.8 → hyperweave-0.2.9}/assets/icons/cobalt-sapphire-instagram.svg +0 -0
  63. {hyperweave-0.2.8 → hyperweave-0.2.9}/assets/icons/cobalt-sapphire-linkedin.svg +0 -0
  64. {hyperweave-0.2.8 → hyperweave-0.2.9}/assets/icons/cobalt-sapphire-tiktok.svg +0 -0
  65. {hyperweave-0.2.8 → hyperweave-0.2.9}/assets/icons/cobalt-sapphire-x.svg +0 -0
  66. {hyperweave-0.2.8 → hyperweave-0.2.9}/assets/icons/cobalt-sapphire-youtube.svg +0 -0
  67. {hyperweave-0.2.8 → hyperweave-0.2.9}/assets/marquees/badge-showcase-triple.svg +0 -0
  68. {hyperweave-0.2.8 → hyperweave-0.2.9}/assets/marquees/genome-marquee-triple.svg +0 -0
  69. {hyperweave-0.2.8 → hyperweave-0.2.9}/assets/marquees/sample-badges.svg +0 -0
  70. {hyperweave-0.2.8 → hyperweave-0.2.9}/assets/mintlify-assets/404.svg +0 -0
  71. {hyperweave-0.2.8 → hyperweave-0.2.9}/assets/mintlify-assets/callout-icons.svg +0 -0
  72. {hyperweave-0.2.8 → hyperweave-0.2.9}/assets/mintlify-assets/divider.svg +0 -0
  73. {hyperweave-0.2.8 → hyperweave-0.2.9}/assets/mintlify-assets/favicon.svg +0 -0
  74. {hyperweave-0.2.8 → hyperweave-0.2.9}/assets/mintlify-assets/hero-banner.svg +0 -0
  75. {hyperweave-0.2.8 → hyperweave-0.2.9}/assets/mintlify-assets/hyperweave-banner-v2.svg +0 -0
  76. {hyperweave-0.2.8 → hyperweave-0.2.9}/assets/mintlify-assets/hyperweave-navbar-logo.svg +0 -0
  77. {hyperweave-0.2.8 → hyperweave-0.2.9}/assets/mintlify-assets/loader.svg +0 -0
  78. {hyperweave-0.2.8 → hyperweave-0.2.9}/assets/mintlify-assets/og-image.svg +0 -0
  79. {hyperweave-0.2.8 → hyperweave-0.2.9}/assets/timelines/hyperweave-roadmap.svg +0 -0
  80. {hyperweave-0.2.8 → hyperweave-0.2.9}/docs/assets/favicon.svg +0 -0
  81. {hyperweave-0.2.8 → hyperweave-0.2.9}/docs/assets/hyperweave-banner.svg +0 -0
  82. {hyperweave-0.2.8 → hyperweave-0.2.9}/docs/assets/hyperweave-navbar-logo.svg +0 -0
  83. {hyperweave-0.2.8 → hyperweave-0.2.9}/docs/assets/og-image.svg +0 -0
  84. {hyperweave-0.2.8 → hyperweave-0.2.9}/docs/explanation/architecture.mdx +0 -0
  85. {hyperweave-0.2.8 → hyperweave-0.2.9}/docs/explanation/camo-compatibility.mdx +0 -0
  86. {hyperweave-0.2.8 → hyperweave-0.2.9}/docs/explanation/cim-compliance.mdx +0 -0
  87. {hyperweave-0.2.8 → hyperweave-0.2.9}/docs/explanation/genome-profile-system.mdx +0 -0
  88. {hyperweave-0.2.8 → hyperweave-0.2.9}/docs/explanation/metadata-tiers.mdx +0 -0
  89. {hyperweave-0.2.8 → hyperweave-0.2.9}/docs/how-to/add-motion-to-badges.mdx +0 -0
  90. {hyperweave-0.2.8 → hyperweave-0.2.9}/docs/how-to/create-session-receipts.mdx +0 -0
  91. {hyperweave-0.2.8 → hyperweave-0.2.9}/docs/how-to/set-up-claude-code-hooks.mdx +0 -0
  92. {hyperweave-0.2.8 → hyperweave-0.2.9}/docs/how-to/use-live-data-badges.mdx +0 -0
  93. {hyperweave-0.2.8 → hyperweave-0.2.9}/docs/introduction.mdx +0 -0
  94. {hyperweave-0.2.8 → hyperweave-0.2.9}/docs/mint.json +0 -0
  95. {hyperweave-0.2.8 → hyperweave-0.2.9}/docs/quickstart.mdx +0 -0
  96. {hyperweave-0.2.8 → hyperweave-0.2.9}/docs/reference/cli.mdx +0 -0
  97. {hyperweave-0.2.8 → hyperweave-0.2.9}/docs/reference/compose-spec.mdx +0 -0
  98. {hyperweave-0.2.8 → hyperweave-0.2.9}/docs/reference/genomes.mdx +0 -0
  99. {hyperweave-0.2.8 → hyperweave-0.2.9}/docs/reference/http-api.mdx +0 -0
  100. {hyperweave-0.2.8 → hyperweave-0.2.9}/docs/reference/mcp-tools.mdx +0 -0
  101. {hyperweave-0.2.8 → hyperweave-0.2.9}/docs/reference/motions.mdx +0 -0
  102. {hyperweave-0.2.8 → hyperweave-0.2.9}/docs/reference/telemetry-contract.mdx +0 -0
  103. {hyperweave-0.2.8 → hyperweave-0.2.9}/docs/spec/hyperweave-protocol.mdx +0 -0
  104. {hyperweave-0.2.8 → hyperweave-0.2.9}/docs/tutorials/build-an-artifact-kit.mdx +0 -0
  105. {hyperweave-0.2.8 → hyperweave-0.2.9}/docs/tutorials/compose-your-first-badge.mdx +0 -0
  106. {hyperweave-0.2.8 → hyperweave-0.2.9}/docs/tutorials/install-session-telemetry.mdx +0 -0
  107. {hyperweave-0.2.8 → hyperweave-0.2.9}/fly.toml +0 -0
  108. {hyperweave-0.2.8 → hyperweave-0.2.9}/hooks/install.py +0 -0
  109. {hyperweave-0.2.8 → hyperweave-0.2.9}/hooks/session_end.sh +0 -0
  110. {hyperweave-0.2.8 → hyperweave-0.2.9}/justfile +0 -0
  111. {hyperweave-0.2.8 → hyperweave-0.2.9}/pyproject.toml +0 -0
  112. {hyperweave-0.2.8 → hyperweave-0.2.9}/scripts/extract_font_metrics.py +0 -0
  113. {hyperweave-0.2.8 → hyperweave-0.2.9}/scripts/extract_glyphs.py +0 -0
  114. {hyperweave-0.2.8 → hyperweave-0.2.9}/scripts/generate_hw_compliant.py +0 -0
  115. {hyperweave-0.2.8 → hyperweave-0.2.9}/scripts/generate_proofset.py +0 -0
  116. {hyperweave-0.2.8 → hyperweave-0.2.9}/scripts/stress_test.py +0 -0
  117. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/__init__.py +0 -0
  118. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/__main__.py +0 -0
  119. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/cli.py +0 -0
  120. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/compose/__init__.py +0 -0
  121. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/compose/assembler.py +0 -0
  122. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/compose/context.py +0 -0
  123. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/compose/engine.py +0 -0
  124. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/compose/lanes.py +0 -0
  125. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/compose/resolver.py +0 -0
  126. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/compose/resolvers/__init__.py +0 -0
  127. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/compose/resolvers/chart.py +0 -0
  128. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/compose/resolvers/stats.py +0 -0
  129. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/compose/resolvers/timeline.py +0 -0
  130. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/compose/rhythm.py +0 -0
  131. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/compose/validate_paradigms.py +0 -0
  132. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/config/__init__.py +0 -0
  133. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/config/genome_validator.py +0 -0
  134. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/config/loader.py +0 -0
  135. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/config/registry.py +0 -0
  136. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/config/settings.py +0 -0
  137. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/connectors/__init__.py +0 -0
  138. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/connectors/arxiv.py +0 -0
  139. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/connectors/base.py +0 -0
  140. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/connectors/cache.py +0 -0
  141. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/connectors/rest.py +0 -0
  142. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/core/__init__.py +0 -0
  143. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/core/color.py +0 -0
  144. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/core/contracts.py +0 -0
  145. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/core/enums.py +0 -0
  146. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/core/font_metrics.py +0 -0
  147. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/core/models.py +0 -0
  148. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/core/paradigm.py +0 -0
  149. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/core/schema.py +0 -0
  150. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/core/state.py +0 -0
  151. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/core/text.py +0 -0
  152. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/core/thresholds.py +0 -0
  153. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/data/css/accessibility.css +0 -0
  154. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/data/css/bridge.css +0 -0
  155. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/data/css/expression.css +0 -0
  156. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/data/css/phase-colors.css +0 -0
  157. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/data/css/status.css +0 -0
  158. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/data/css/telemetry.css +0 -0
  159. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/data/font-metrics/inter.json +0 -0
  160. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/data/font-metrics/jetbrains-mono.json +0 -0
  161. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/data/font-metrics/orbitron.json +0 -0
  162. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/data/fonts/jetbrains-mono.b64 +0 -0
  163. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/data/fonts/jetbrains-mono.meta.json +0 -0
  164. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/data/fonts/orbitron.b64 +0 -0
  165. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/data/fonts/orbitron.meta.json +0 -0
  166. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/data/genomes/brutalist-emerald.json +0 -0
  167. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/data/genomes/chrome-horizon.json +0 -0
  168. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/data/genomes/telemetry-void.json +0 -0
  169. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/data/glyphs.json +0 -0
  170. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/data/motions/border/chromatic-pulse.yaml +0 -0
  171. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/data/motions/border/corner-trace.yaml +0 -0
  172. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/data/motions/border/dual-orbit.yaml +0 -0
  173. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/data/motions/border/entanglement.yaml +0 -0
  174. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/data/motions/border/rimrun.yaml +0 -0
  175. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/data/motions/kinetic/bars.yaml +0 -0
  176. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/data/motions/kinetic/breach.yaml +0 -0
  177. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/data/motions/kinetic/broadcast.yaml +0 -0
  178. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/data/motions/kinetic/cascade.yaml +0 -0
  179. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/data/motions/kinetic/collapse.yaml +0 -0
  180. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/data/motions/kinetic/converge.yaml +0 -0
  181. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/data/motions/kinetic/crash.yaml +0 -0
  182. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/data/motions/kinetic/drop.yaml +0 -0
  183. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/data/motions/kinetic/pulse.yaml +0 -0
  184. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/data/motions/static.yaml +0 -0
  185. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/data/paradigms/brutalist.yaml +0 -0
  186. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/data/paradigms/chrome.yaml +0 -0
  187. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/data/paradigms/default.yaml +0 -0
  188. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/data/policies/normal.json +0 -0
  189. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/data/policies/permissive.json +0 -0
  190. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/data/policies/ungoverned.json +0 -0
  191. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/data/profiles/brutalist.contract.json +0 -0
  192. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/data/profiles/brutalist.yaml +0 -0
  193. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/data/profiles/chrome.contract.json +0 -0
  194. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/data/profiles/chrome.yaml +0 -0
  195. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/data/specimens.yaml +0 -0
  196. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/data/telemetry/model-pricing.yaml +0 -0
  197. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/data/telemetry/stage-config.yaml +0 -0
  198. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/data/telemetry/stage-labels.yaml +0 -0
  199. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/data/telemetry/tool-classes.yaml +0 -0
  200. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/data/telemetry/tool-colors.yaml +0 -0
  201. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/data/terminals/arrow.json +0 -0
  202. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/data/terminals/aurora.json +0 -0
  203. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/data/terminals/beacon.json +0 -0
  204. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/data/terminals/bracket3d.json +0 -0
  205. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/data/terminals/chevron.json +0 -0
  206. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/data/terminals/crosshair.json +0 -0
  207. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/data/terminals/diamond.json +0 -0
  208. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/data/terminals/rocket.json +0 -0
  209. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/data/terminals/rules/bar.json +0 -0
  210. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/data/terminals/rules/dashed.json +0 -0
  211. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/data/terminals/rules/five-wave.json +0 -0
  212. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/data/terminals/rules/spectral.json +0 -0
  213. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/data/terminals/rules/straight.json +0 -0
  214. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/data/terminals/rules/wave.json +0 -0
  215. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/data/terminals/stijl.json +0 -0
  216. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/kit.py +0 -0
  217. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/mcp/__init__.py +0 -0
  218. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/mcp/__main__.py +0 -0
  219. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/mcp/server.py +0 -0
  220. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/py.typed +0 -0
  221. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/render/__init__.py +0 -0
  222. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/render/fonts.py +0 -0
  223. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/render/glyphs.py +0 -0
  224. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/render/motion.py +0 -0
  225. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/render/templates.py +0 -0
  226. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/serve/__init__.py +0 -0
  227. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/serve/app.py +0 -0
  228. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/telemetry/__init__.py +0 -0
  229. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/telemetry/capture.py +0 -0
  230. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/telemetry/contract.py +0 -0
  231. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/telemetry/corrections.py +0 -0
  232. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/telemetry/cost.py +0 -0
  233. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/telemetry/models.py +0 -0
  234. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/telemetry/parser.py +0 -0
  235. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/telemetry/stages.py +0 -0
  236. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/components/chart-area.svg.j2 +0 -0
  237. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/components/chart-axes.svg.j2 +0 -0
  238. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/components/chart-empty-state.svg.j2 +0 -0
  239. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/components/chart-gridlines.svg.j2 +0 -0
  240. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/components/chart-markers/circle.svg.j2 +0 -0
  241. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/components/chart-markers/diamond.svg.j2 +0 -0
  242. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/components/chart-markers/endpoint-diamond.svg.j2 +0 -0
  243. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/components/chart-markers/endpoint-rect.svg.j2 +0 -0
  244. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/components/chart-markers/rect.svg.j2 +0 -0
  245. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/components/chart-milestone.svg.j2 +0 -0
  246. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/components/chart-polyline.svg.j2 +0 -0
  247. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/components/filter.svg.j2 +0 -0
  248. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/components/glyph-inline.svg.j2 +0 -0
  249. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/components/glyph.svg.j2 +0 -0
  250. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/components/gradient.svg.j2 +0 -0
  251. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/components/metadata.svg.j2 +0 -0
  252. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/components/metric.svg.j2 +0 -0
  253. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/components/phase-segment.svg.j2 +0 -0
  254. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/components/rule.svg.j2 +0 -0
  255. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/components/status.svg.j2 +0 -0
  256. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/components/terminal.svg.j2 +0 -0
  257. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/components/treemap.svg.j2 +0 -0
  258. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/document.svg.j2 +0 -0
  259. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/error-badge.svg.j2 +0 -0
  260. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/frames/badge/brutalist-content.j2 +0 -0
  261. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/frames/badge/brutalist-defs.j2 +0 -0
  262. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/frames/badge/chrome-content.j2 +0 -0
  263. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/frames/badge/chrome-defs.j2 +0 -0
  264. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/frames/badge/default-content.j2 +0 -0
  265. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/frames/badge/default-defs.j2 +0 -0
  266. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/frames/badge.svg.j2 +0 -0
  267. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/frames/banner/brutalist-defs.j2 +0 -0
  268. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/frames/banner/chrome-content.j2 +0 -0
  269. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/frames/banner/chrome-defs.j2 +0 -0
  270. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/frames/banner/default-content.j2 +0 -0
  271. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/frames/banner/default-defs.j2 +0 -0
  272. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/frames/banner.svg.j2 +0 -0
  273. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/frames/catalog.svg.j2 +0 -0
  274. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/frames/chart/brutalist-content.j2 +0 -0
  275. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/frames/chart/brutalist-defs.j2 +0 -0
  276. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/frames/chart/chrome-content.j2 +0 -0
  277. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/frames/chart/chrome-defs.j2 +0 -0
  278. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/frames/chart.svg.j2 +0 -0
  279. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/frames/divider.svg.j2 +0 -0
  280. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/frames/icon/brutalist-content.j2 +0 -0
  281. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/frames/icon/brutalist-defs.j2 +0 -0
  282. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/frames/icon/chrome-content.j2 +0 -0
  283. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/frames/icon/chrome-defs.j2 +0 -0
  284. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/frames/icon/default-content.j2 +0 -0
  285. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/frames/icon/default-defs.j2 +0 -0
  286. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/frames/icon.svg.j2 +0 -0
  287. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/frames/marquee-counter/brutalist-content.j2 +0 -0
  288. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/frames/marquee-counter/brutalist-defs.j2 +0 -0
  289. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/frames/marquee-counter/chrome-content.j2 +0 -0
  290. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/frames/marquee-counter/chrome-defs.j2 +0 -0
  291. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/frames/marquee-counter.svg.j2 +0 -0
  292. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/frames/marquee-horizontal/brutalist-content.j2 +0 -0
  293. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/frames/marquee-horizontal/brutalist-defs.j2 +0 -0
  294. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/frames/marquee-horizontal/chrome-content.j2 +0 -0
  295. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/frames/marquee-horizontal/chrome-defs.j2 +0 -0
  296. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/frames/marquee-horizontal.svg.j2 +0 -0
  297. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/frames/marquee-vertical/brutalist-content.j2 +0 -0
  298. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/frames/marquee-vertical/brutalist-defs.j2 +0 -0
  299. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/frames/marquee-vertical/chrome-content.j2 +0 -0
  300. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/frames/marquee-vertical/chrome-defs.j2 +0 -0
  301. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/frames/marquee-vertical.svg.j2 +0 -0
  302. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/frames/master-card.svg.j2 +0 -0
  303. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/frames/receipt.svg.j2 +0 -0
  304. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/frames/rhythm-strip.svg.j2 +0 -0
  305. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/frames/stats/brutalist-content.j2 +0 -0
  306. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/frames/stats/brutalist-defs.j2 +0 -0
  307. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/frames/stats/chrome-content.j2 +0 -0
  308. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/frames/stats/chrome-defs.j2 +0 -0
  309. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/frames/stats.svg.j2 +0 -0
  310. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/frames/strip/brutalist-content.j2 +0 -0
  311. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/frames/strip/brutalist-defs.j2 +0 -0
  312. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/frames/strip/brutalist-status.j2 +0 -0
  313. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/frames/strip/chrome-content.j2 +0 -0
  314. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/frames/strip/chrome-defs.j2 +0 -0
  315. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/frames/strip/chrome-status.j2 +0 -0
  316. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/frames/strip/default-content.j2 +0 -0
  317. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/frames/strip/default-defs.j2 +0 -0
  318. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/frames/strip/default-status.j2 +0 -0
  319. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/frames/strip.svg.j2 +0 -0
  320. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/frames/timeline/default-content.j2 +0 -0
  321. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/frames/timeline.svg.j2 +0 -0
  322. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/motions/border/chromatic-pulse.svg.j2 +0 -0
  323. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/motions/border/corner-trace.svg.j2 +0 -0
  324. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/motions/border/dual-orbit.svg.j2 +0 -0
  325. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/motions/border/entanglement.svg.j2 +0 -0
  326. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/motions/border/rimrun.svg.j2 +0 -0
  327. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/motions/kinetic/bars.svg.j2 +0 -0
  328. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/motions/kinetic/breach.svg.j2 +0 -0
  329. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/motions/kinetic/broadcast.svg.j2 +0 -0
  330. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/motions/kinetic/cascade.svg.j2 +0 -0
  331. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/motions/kinetic/collapse.svg.j2 +0 -0
  332. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/motions/kinetic/converge.svg.j2 +0 -0
  333. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/motions/kinetic/crash.svg.j2 +0 -0
  334. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/motions/kinetic/drop.svg.j2 +0 -0
  335. {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/motions/kinetic/pulse.svg.j2 +0 -0
  336. {hyperweave-0.2.8 → hyperweave-0.2.9}/tests/__init__.py +0 -0
  337. {hyperweave-0.2.8 → hyperweave-0.2.9}/tests/conftest.py +0 -0
  338. {hyperweave-0.2.8 → hyperweave-0.2.9}/tests/fixtures/github_contributions/synthetic.html +0 -0
  339. {hyperweave-0.2.8 → hyperweave-0.2.9}/tests/fixtures/session.json +0 -0
  340. {hyperweave-0.2.8 → hyperweave-0.2.9}/tests/fixtures/session.jsonl +0 -0
  341. {hyperweave-0.2.8 → hyperweave-0.2.9}/tests/helpers.py +0 -0
  342. {hyperweave-0.2.8 → hyperweave-0.2.9}/tests/test_assembler.py +0 -0
  343. {hyperweave-0.2.8 → hyperweave-0.2.9}/tests/test_chart_frame.py +0 -0
  344. {hyperweave-0.2.8 → hyperweave-0.2.9}/tests/test_core.py +0 -0
  345. {hyperweave-0.2.8 → hyperweave-0.2.9}/tests/test_github_scrape.py +0 -0
  346. {hyperweave-0.2.8 → hyperweave-0.2.9}/tests/test_kit.py +0 -0
  347. {hyperweave-0.2.8 → hyperweave-0.2.9}/tests/test_mcp.py +0 -0
  348. {hyperweave-0.2.8 → hyperweave-0.2.9}/tests/test_paradigm_dispatch.py +0 -0
  349. {hyperweave-0.2.8 → hyperweave-0.2.9}/tests/test_paradigm_extensibility.py +0 -0
  350. {hyperweave-0.2.8 → hyperweave-0.2.9}/tests/test_proofset.py +0 -0
  351. {hyperweave-0.2.8 → hyperweave-0.2.9}/tests/test_render.py +0 -0
  352. {hyperweave-0.2.8 → hyperweave-0.2.9}/tests/test_resolver_rhythm.py +0 -0
  353. {hyperweave-0.2.8 → hyperweave-0.2.9}/tests/test_serve.py +0 -0
  354. {hyperweave-0.2.8 → hyperweave-0.2.9}/tests/test_serve_live_state.py +0 -0
  355. {hyperweave-0.2.8 → hyperweave-0.2.9}/tests/test_state_inference.py +0 -0
  356. {hyperweave-0.2.8 → hyperweave-0.2.9}/tests/test_stats_card.py +0 -0
  357. {hyperweave-0.2.8 → hyperweave-0.2.9}/tests/test_telemetry.py +0 -0
  358. {hyperweave-0.2.8 → hyperweave-0.2.9}/tests/test_telemetry_integration.py +0 -0
  359. {hyperweave-0.2.8 → hyperweave-0.2.9}/tests/test_timeline.py +0 -0
  360. {hyperweave-0.2.8 → hyperweave-0.2.9}/uv.lock +0 -0
@@ -5,6 +5,17 @@ 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.9] - 2026-04-21
9
+
10
+ ### Fixed
11
+
12
+ - **Star history charts now render an authentic growth curve for any repo size.** v0.2.8 fetched only the most-recent 2,000 stargazers for any repo with more than ~2,000 stars, which meant both the 361,000-star `openclaw/openclaw` and the 40,000-star `JuliusBrussee/caveman` rendered as flat horizontal lines near their total-star value — the captured window's variation was invisible on the full-range Y-axis. Star history now uses GraphQL cursor-offset sampling to pull 12 real stargazer timestamps evenly distributed across the repository's full lifetime, regardless of whether the repo has 30 stars or 500,000. The resulting curve shows actual adoption shape — slow-early, explosive-middle, tapering-late — rather than either a hockey-stick or a flat line.
13
+ - **X-axis date labels adapt to the repository's lifetime.** A viral repo that went from 0 to 40K stars in two weeks was being labeled with a single "2026" at the center of its axis; a mature repo with 10 years of history showed the same year-only format. The axis now selects granularity from the actual temporal span — daily labels ("Apr 05", "Apr 12") when the span is under two weeks, weekly under 90 days, month-plus-year under two years, yearly up to ten years, and every-other-year beyond. Labels maintain a minimum 48-pixel horizontal gap so they never collide — the "202324" pileup is gone.
14
+
15
+ ### Changed
16
+
17
+ - **Parallel GraphQL dispatch is now bounded.** Cold fetches fan out twelve cursor-offset queries across four concurrent in-flight requests via `asyncio.Semaphore`. This keeps cold-fetch latency under ~1 second while staying well below per-minute abuse-detection thresholds that bursty same-token parallelism could otherwise trip.
18
+
8
19
  ## [0.2.8] - 2026-04-20
9
20
 
10
21
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hyperweave
3
- Version: 0.2.8
3
+ Version: 0.2.9
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
@@ -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.8'
22
- __version_tuple__ = version_tuple = (0, 2, 8)
21
+ __version__ = version = '0.2.9'
22
+ __version_tuple__ = version_tuple = (0, 2, 9)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -3,6 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import asyncio
6
+ import base64
6
7
  import math
7
8
  import re
8
9
  from datetime import UTC, datetime
@@ -151,19 +152,25 @@ _STARGAZER_PAGE_SIZE = 100
151
152
  _STARGAZER_PAGE_CAP = 400
152
153
  _STARGAZER_ACCEPT_HEADER = "application/vnd.github.v3.star+json"
153
154
 
154
- # GraphQL-path constants. Walking backwards from "now" via cursor pagination
155
- # bypasses the REST 400-page cap entirely. For small repos (≤2000 stars) we
156
- # get full lifetime coverage; for mega-repos we get a recent-growth window,
157
- # which is the behavior star-history.com uses for the same reason.
158
- _GRAPHQL_PAGE_SIZE = 100
159
- _GRAPHQL_MAX_PAGES = 20 # 20 * 100 = 2000 most-recent stargazers
160
-
161
- _STARGAZER_GRAPHQL_QUERY = """
162
- query StargazerWindow($owner: String!, $repo: String!, $cursor: String) {
155
+ # GraphQL cursor-offset sampling constants.
156
+ #
157
+ # GitHub's GraphQL cursors decode to the literal text ``cursor:<N>`` where
158
+ # N is a 0-indexed offset into the stargazer list. By Base64-encoding
159
+ # ``cursor:<N-1>`` we can construct an ``after:`` anchor that lets ``first: 1``
160
+ # return exactly the Nth stargazer. This bypasses GitHub's 400-page REST cap
161
+ # and lets us sample any offset in a 500K-star repo cheaply.
162
+ #
163
+ # 12 offsets * 1 node per call = 12 GraphQL points per cold fetch.
164
+ # ``asyncio.Semaphore(_GRAPHQL_CONCURRENCY)`` caps fan-out so bursty
165
+ # same-token parallelism doesn't trigger GitHub's per-minute abuse detection.
166
+ _DEFAULT_SAMPLE_COUNT = 12
167
+ _GRAPHQL_CONCURRENCY = 4
168
+
169
+ _CURSOR_OFFSET_QUERY = """
170
+ query StargazerAtOffset($owner: String!, $repo: String!, $cursor: String) {
163
171
  repository(owner: $owner, name: $repo) {
164
172
  stargazerCount
165
- stargazers(last: 100, before: $cursor) {
166
- pageInfo { startCursor hasPreviousPage }
173
+ stargazers(first: 1, after: $cursor) {
167
174
  edges { starredAt }
168
175
  }
169
176
  }
@@ -171,6 +178,22 @@ query StargazerWindow($owner: String!, $repo: String!, $cursor: String) {
171
178
  """
172
179
 
173
180
 
181
+ def _cursor_for_offset(offset: int) -> str | None:
182
+ """Return the Base64-encoded cursor for a 1-indexed stargazer position.
183
+
184
+ GitHub's GraphQL cursors decode to ``cursor:<N>`` where N is 0-indexed.
185
+ Constructing them lets us sample any offset within a repo's stargazer
186
+ list, bypassing both the REST 400-page cap and the cost of walking
187
+ every page via cursor pagination.
188
+
189
+ Returns None for ``offset <= 1`` (no cursor needed — this IS the first
190
+ entry; passing ``after: None`` to GraphQL starts from the beginning).
191
+ """
192
+ if offset <= 1:
193
+ return None
194
+ return base64.b64encode(f"cursor:{offset - 1}".encode()).decode()
195
+
196
+
174
197
  async def fetch_stargazer_history(
175
198
  owner: str,
176
199
  repo: str,
@@ -199,10 +222,11 @@ async def fetch_stargazer_history(
199
222
 
200
223
  result: dict[str, Any] | None = None
201
224
 
202
- # Primary path: GraphQL (requires auth). Walks backwards from most-recent
203
- # so mega-repos get a detailed recent-growth curve instead of a hockey
204
- # stick. A per-page budget of 20 * 100 = 2000 stargazers per cold fetch
205
- # keeps us well under the 5000-points/hour GraphQL rate limit.
225
+ # Primary path: GraphQL cursor-offset sampling (requires auth). Constructs
226
+ # cursors for N evenly-distributed stargazer offsets across [1, total_stars]
227
+ # and fetches one stargazer per offset. Bypasses GitHub's 400-page REST cap,
228
+ # so a 361K-star repo gets 12 real points across its full lifetime instead
229
+ # of a hockey-stick or flat line.
206
230
  if _get_github_token():
207
231
  try:
208
232
  result = await _fetch_stargazer_history_graphql(owner, repo, sample_pages)
@@ -221,68 +245,95 @@ async def fetch_stargazer_history(
221
245
  async def _fetch_stargazer_history_graphql(
222
246
  owner: str,
223
247
  repo: str,
224
- sample_pages: int,
248
+ sample_count: int = _DEFAULT_SAMPLE_COUNT,
225
249
  ) -> dict[str, Any]:
226
- """GraphQL stargazer fetch walks cursor pagination backwards from most-recent.
227
-
228
- The REST ``/stargazers`` endpoint caps deep pagination at ~400 pages, so
229
- for a 361k-star repo we could only ever see stars #1 to #40000 — the
230
- early hype window producing the flat-then-vertical "hockey stick"
231
- curve. GraphQL cursor pagination has no such cap; we simply walk backward
232
- from the endpoint, collect up to ``_GRAPHQL_MAX_PAGES`` pages, and stop
233
- early if we exhaust the repo's history (small repos get full lifetime).
250
+ """Sample N stargazer timestamps at evenly-distributed cursor offsets.
251
+
252
+ Strategy: given a repo's ``total_stars``, fetch one stargazer at each of
253
+ N evenly-spaced offsets across ``[1, total_stars]``. Each offset maps to
254
+ a constructed cursor (``base64("cursor:<N-1>")``); GraphQL returns that
255
+ stargazer's ``starred_at``. Pair with the known offset to form a
256
+ ``{date, count}`` point. Appends a now-point at ``total_stars``.
257
+
258
+ Works for any repo size — 30 stars, 2938 stars, 361k stars — because
259
+ cursor-offset sampling is not subject to GitHub's 400-page REST cap.
260
+
261
+ Concurrency is bounded at ``_GRAPHQL_CONCURRENCY`` to avoid tripping
262
+ per-minute abuse-detection heuristics on bursty parallel fan-out.
234
263
  """
235
264
  identifier = f"{owner}/{repo}"
236
- variables: dict[str, Any] = {"owner": owner, "repo": repo, "cursor": None}
237
-
238
- all_timestamps: list[str] = []
239
- total_stars = 0
240
-
241
- for _ in range(_GRAPHQL_MAX_PAGES):
242
- response = await fetch_graphql(_STARGAZER_GRAPHQL_QUERY, variables, provider=PROVIDER)
243
- repo_data = (response.get("data") or {}).get("repository")
244
- if not repo_data:
245
- # Repo doesn't exist or query errored — return empty honestly.
246
- return {
247
- "points": [],
248
- "current_stars": 0,
249
- "repo": identifier,
250
- "ttl": STARGAZER_HISTORY_TTL,
251
- }
252
-
253
- total_stars = int(repo_data.get("stargazerCount", 0))
254
- if total_stars == 0:
255
- return {
256
- "points": [],
257
- "current_stars": 0,
258
- "repo": identifier,
259
- "ttl": STARGAZER_HISTORY_TTL,
260
- }
261
-
262
- stargazers = repo_data.get("stargazers") or {}
263
- edges = stargazers.get("edges") or []
264
- # GraphQL `last/before` returns edges in ASC chronological order.
265
- # Prepend so the accumulated list stays sorted oldest→newest across
266
- # multiple backward walks.
267
- page_timestamps = [e["starredAt"] for e in edges if isinstance(e, dict) and e.get("starredAt")]
268
- all_timestamps = page_timestamps + all_timestamps
269
-
270
- page_info = stargazers.get("pageInfo") or {}
271
- if not page_info.get("hasPreviousPage"):
272
- break
273
- variables["cursor"] = page_info.get("startCursor")
274
265
 
275
- if not all_timestamps:
276
- return {
277
- "points": [],
278
- "current_stars": total_stars,
279
- "repo": identifier,
280
- "ttl": STARGAZER_HISTORY_TTL,
281
- }
266
+ # First query: learn total_stars and pick up the first stargazer's
267
+ # timestamp in the same round-trip (offset 1 uses no cursor anyway).
268
+ first_response = await fetch_graphql(
269
+ _CURSOR_OFFSET_QUERY,
270
+ {"owner": owner, "repo": repo, "cursor": None},
271
+ provider=PROVIDER,
272
+ )
273
+ repo_data = (first_response.get("data") or {}).get("repository")
274
+ if not repo_data:
275
+ return _empty_stargazer_result(identifier)
276
+
277
+ total_stars = int(repo_data.get("stargazerCount", 0))
278
+ if total_stars == 0:
279
+ return _empty_stargazer_result(identifier)
282
280
 
283
- points = _downsample_timestamps(all_timestamps, sample_pages, total_stars)
284
- # Append honest "now" point — matches the v0.2.7 §1.4 contract.
285
- points.append({"date": datetime.now(UTC).isoformat(), "count": total_stars})
281
+ first_edges = (repo_data.get("stargazers") or {}).get("edges") or []
282
+ first_starred_at: str | None = None
283
+ if first_edges and isinstance(first_edges[0], dict):
284
+ first_starred_at = first_edges[0].get("starredAt")
285
+
286
+ # Clamp sample count to total_stars — small repos get every star sampled
287
+ # (no point requesting 12 offsets from a 5-star repo).
288
+ effective_samples = min(sample_count, total_stars)
289
+
290
+ # Compute evenly-distributed offsets across the full stargazer list.
291
+ # Always includes position 1 (oldest) and position total_stars (newest).
292
+ if effective_samples <= 1:
293
+ offsets = [1]
294
+ else:
295
+ step = (total_stars - 1) / (effective_samples - 1)
296
+ offsets = sorted({max(1, round(1 + step * i)) for i in range(effective_samples)})
297
+
298
+ # Bounded concurrent fan-out: semaphore throttles to _GRAPHQL_CONCURRENCY
299
+ # in-flight requests at a time. For 12 offsets at concurrency 4, that's
300
+ # 3 waves of ≤4 calls each → ~1s cold-fetch latency.
301
+ sem = asyncio.Semaphore(_GRAPHQL_CONCURRENCY)
302
+
303
+ async def _fetch_one(offset: int) -> tuple[int, str | None]:
304
+ # Offset 1 is already in hand from the total_stars lookup.
305
+ if offset == 1 and first_starred_at is not None:
306
+ return offset, first_starred_at
307
+ cursor = _cursor_for_offset(offset)
308
+ async with sem:
309
+ resp = await fetch_graphql(
310
+ _CURSOR_OFFSET_QUERY,
311
+ {"owner": owner, "repo": repo, "cursor": cursor},
312
+ provider=PROVIDER,
313
+ )
314
+ edges_data = (((resp.get("data") or {}).get("repository") or {}).get("stargazers") or {}).get("edges") or []
315
+ starred_at: str | None = None
316
+ if edges_data and isinstance(edges_data[0], dict):
317
+ starred_at = edges_data[0].get("starredAt")
318
+ return offset, starred_at
319
+
320
+ results = await asyncio.gather(
321
+ *[_fetch_one(off) for off in offsets],
322
+ return_exceptions=True,
323
+ )
324
+
325
+ points: list[dict[str, Any]] = []
326
+ for item in results:
327
+ if isinstance(item, BaseException):
328
+ continue
329
+ offset, starred_at = item
330
+ if starred_at is None:
331
+ continue
332
+ points.append({"date": starred_at, "count": offset})
333
+
334
+ # Append the honest now-point: current timestamp at the real total count.
335
+ if points:
336
+ points.append({"date": datetime.now(UTC).isoformat(), "count": total_stars})
286
337
  points.sort(key=lambda p: p["date"])
287
338
 
288
339
  return {
@@ -293,33 +344,14 @@ async def _fetch_stargazer_history_graphql(
293
344
  }
294
345
 
295
346
 
296
- def _downsample_timestamps(
297
- timestamps: list[str],
298
- n_samples: int,
299
- total_stars: int,
300
- ) -> list[dict[str, Any]]:
301
- """Reduce N ascending timestamps to ``n_samples`` evenly-spaced points.
302
-
303
- The count at each sampled timestamp is computed backwards from
304
- ``total_stars``: since ``timestamps`` represents the most-recent
305
- ``len(timestamps)`` stargazers in chronological order, the final
306
- timestamp corresponds to ``total_stars`` and the earliest to
307
- ``total_stars - (n - 1)``.
308
- """
309
- n = len(timestamps)
310
- if n == 0:
311
- return []
312
- base = total_stars - (n - 1)
313
- if n <= n_samples:
314
- return [{"date": ts, "count": base + idx} for idx, ts in enumerate(timestamps)]
315
-
316
- # Pick n_samples evenly-spaced indices across [0, n-1], always including
317
- # the endpoints so the curve starts and ends at the captured extremes.
318
- if n_samples <= 1:
319
- return [{"date": timestamps[-1], "count": total_stars}]
320
- step = (n - 1) / (n_samples - 1)
321
- indices = sorted({round(i * step) for i in range(n_samples)})
322
- return [{"date": timestamps[idx], "count": base + idx} for idx in indices]
347
+ def _empty_stargazer_result(identifier: str) -> dict[str, Any]:
348
+ """Shared empty-state shape for zero-star or missing-repo cases."""
349
+ return {
350
+ "points": [],
351
+ "current_stars": 0,
352
+ "repo": identifier,
353
+ "ttl": STARGAZER_HISTORY_TTL,
354
+ }
323
355
 
324
356
 
325
357
  async def _fetch_stargazer_history_rest(
@@ -32,7 +32,7 @@ from __future__ import annotations
32
32
 
33
33
  import math
34
34
  from dataclasses import dataclass
35
- from datetime import UTC, datetime
35
+ from datetime import UTC, datetime, timedelta
36
36
  from typing import Any
37
37
 
38
38
  # ── Data types ─────────────────────────────────────────────────────────────
@@ -513,35 +513,95 @@ def _build_y_labels(ticks: list[int], v_min: int, v_max: int, vp: Viewport) -> l
513
513
  return out
514
514
 
515
515
 
516
- def _build_x_year_labels(points: list[ChartPoint], vp: Viewport) -> list[dict[str, Any]]:
517
- """Return year-string labels positioned at jan-1 boundaries in the data range.
516
+ # Minimum horizontal pixel gap between two x-axis labels. Year labels (4 chars)
517
+ # need a bit more room than milestone labels (40 px); 48 px keeps well-spaced
518
+ # "2023" / "2024" / "2025" readable without forcing unnecessary drops.
519
+ _X_LABEL_MIN_GAP_PX: int = 48
518
520
 
519
- Matches star-history.com's convention: just year numbers (2024, 2025, 2026)
520
- at the X positions where each year begins. No "EARLY '24", "MID '25", etc.
521
521
 
522
- Single-year spans collapse to one label at the viewport center.
522
+ def _build_x_date_labels(points: list[ChartPoint], vp: Viewport) -> list[dict[str, Any]]:
523
+ """Adaptive x-axis date labels — granularity follows the data's temporal span.
524
+
525
+ Tick formats:
526
+ < 14 days → daily ("Apr 05")
527
+ < 90 days → weekly ("Apr 05" at 7-day steps)
528
+ < 2 years → monthly ("Apr 2026")
529
+ < 10 years → yearly ("2026")
530
+ else → every other year ("2012", "2014", ...)
531
+
532
+ A single-point input renders one centered label with full "%b %d, %Y".
533
+ After candidate generation, a min-gap de-overlap pass removes any middle
534
+ label within ``_X_LABEL_MIN_GAP_PX`` of a previously-kept one. The first
535
+ and last labels are preserved unconditionally because they're the
536
+ temporal endpoints a reader expects to see.
523
537
  """
524
538
  if not points:
525
539
  return []
526
- y_start = points[0].date.year
527
- y_end = points[-1].date.year
528
- if len(points) == 1 or y_start == y_end:
529
- return [{"x": vp.x + vp.w // 2, "text": str(y_start), "anchor": "middle"}]
530
-
531
- t0 = points[0].date.timestamp()
532
- t1 = points[-1].date.timestamp()
533
- t_span = max(1.0, t1 - t0)
534
-
535
- labels: list[dict[str, Any]] = [{"x": vp.x, "text": str(y_start), "anchor": "start"}]
536
- for y in range(y_start + 1, y_end + 1):
537
- jan1 = datetime(y, 1, 1, tzinfo=UTC).timestamp()
538
- if t0 < jan1 <= t1:
539
- frac = (jan1 - t0) / t_span
540
- px = vp.x + round(frac * vp.w)
541
- # Anchor at right-edge if within 20px of viewport right; else middle.
542
- anchor = "end" if (vp.x + vp.w) - px < 20 else "middle"
543
- labels.append({"x": px, "text": str(y), "anchor": anchor})
544
- return labels
540
+ if len(points) == 1:
541
+ p = points[0]
542
+ return [{"x": vp.x + vp.w // 2, "text": p.date.strftime("%b %d, %Y"), "anchor": "middle"}]
543
+
544
+ t0 = points[0].date
545
+ t1 = points[-1].date
546
+ span = t1 - t0
547
+
548
+ # Select granularity + format from the actual span.
549
+ if span < timedelta(days=14):
550
+ format_str = "%b %d"
551
+ step = timedelta(days=1)
552
+ elif span < timedelta(days=90):
553
+ format_str = "%b %d"
554
+ step = timedelta(days=7)
555
+ elif span < timedelta(days=730):
556
+ format_str = "%b %Y"
557
+ step = timedelta(days=30) # 1 month
558
+ elif span < timedelta(days=3650):
559
+ format_str = "%Y"
560
+ step = timedelta(days=365)
561
+ else:
562
+ format_str = "%Y"
563
+ step = timedelta(days=730) # every other year
564
+
565
+ # Generate candidate ticks, projecting each to pixel x via the same scale
566
+ # as the polyline so labels sit directly under their corresponding data.
567
+ t_span_s = max(span.total_seconds(), 1.0)
568
+ candidates: list[dict[str, Any]] = []
569
+ cursor = t0
570
+ while cursor <= t1:
571
+ frac_t = (cursor - t0).total_seconds() / t_span_s
572
+ px = vp.x + round(frac_t * vp.w)
573
+ candidates.append({"x": px, "text": cursor.strftime(format_str), "anchor": "middle"})
574
+ cursor = cursor + step
575
+
576
+ # Ensure the terminal endpoint is in the candidate set (may not land on a
577
+ # step boundary otherwise).
578
+ last_px = vp.x + vp.w
579
+ if not candidates or candidates[-1]["x"] < last_px - 2:
580
+ candidates.append({"x": last_px, "text": t1.strftime(format_str), "anchor": "end"})
581
+
582
+ # First label flush with the y-axis for a cleaner left edge.
583
+ candidates[0]["anchor"] = "start"
584
+
585
+ # De-overlap: preserve first + last; drop any middle label within min-gap
586
+ # of a previously-kept one. Same algorithm as _build_milestones.
587
+ if len(candidates) <= 2:
588
+ return candidates
589
+ kept: list[dict[str, Any]] = [candidates[0]]
590
+ for label in candidates[1:-1]:
591
+ if label["x"] - kept[-1]["x"] >= _X_LABEL_MIN_GAP_PX:
592
+ kept.append(label)
593
+ # The terminal endpoint is always included. If it's too close to the
594
+ # last-kept middle label, or has identical text (e.g. yearly granularity
595
+ # where the last jan-1 tick and the terminal point both read "2026"),
596
+ # replace that middle label with the terminal rather than duplicating.
597
+ last_candidate = candidates[-1]
598
+ too_close = last_candidate["x"] - kept[-1]["x"] < _X_LABEL_MIN_GAP_PX
599
+ same_text = last_candidate["text"] == kept[-1]["text"]
600
+ if too_close or same_text:
601
+ kept[-1] = last_candidate
602
+ else:
603
+ kept.append(last_candidate)
604
+ return kept
545
605
 
546
606
 
547
607
  def _build_empty_state(vp: Viewport, message: str) -> dict[str, Any] | None:
@@ -611,7 +671,7 @@ def build_chart_svg(
611
671
  ticks = _nice_y_ticks(v_max)
612
672
  effective_max = ticks[-1] if ticks else max(v_max, 1)
613
673
  y_labels = _build_y_labels(ticks, 0, effective_max, viewport)
614
- x_labels = _build_x_year_labels(points, viewport)
674
+ x_labels = _build_x_date_labels(points, viewport)
615
675
  # Project with zero-baseline so the polyline aligns to the tick labels.
616
676
  projected = _project_points(points, viewport, v_min=0, v_max=effective_max)
617
677
  else:
@@ -19,7 +19,7 @@ from hyperweave.render.chart_engine import (
19
19
  _build_markers,
20
20
  _build_milestones,
21
21
  _build_polyline_points,
22
- _build_x_year_labels,
22
+ _build_x_date_labels,
23
23
  _format_y_tick,
24
24
  _nice_y_ticks,
25
25
  _normalize_points,
@@ -457,8 +457,8 @@ def test_format_y_tick_integer_and_k_notation() -> None:
457
457
  # ── X-axis year labels ────────────────────────────────────────────────
458
458
 
459
459
 
460
- def test_x_year_labels_single_year(sample_viewport: Viewport) -> None:
461
- """Data entirely within one year one label centered on the viewport."""
460
+ def test_x_date_labels_monthly_granularity_for_year_one_span(sample_viewport: Viewport) -> None:
461
+ """Data spanning ~10 monthsmonthly format ("Mon Year"), not yearly."""
462
462
  pts = _normalize_points(
463
463
  [
464
464
  {"date": "2025-01-15", "count": 1},
@@ -466,18 +466,85 @@ def test_x_year_labels_single_year(sample_viewport: Viewport) -> None:
466
466
  {"date": "2025-11-20", "count": 6},
467
467
  ]
468
468
  )
469
- labels = _build_x_year_labels(pts, sample_viewport)
470
- assert labels == [
471
- {
472
- "x": sample_viewport.x + sample_viewport.w // 2,
473
- "text": "2025",
474
- "anchor": "middle",
475
- }
476
- ]
469
+ labels = _build_x_date_labels(pts, sample_viewport)
470
+ # All labels should use "Mon YYYY" format (e.g., "Jan 2025", "Apr 2025").
471
+ assert labels, "expected at least one label for a 10-month span"
472
+ for label in labels:
473
+ # Match "Abc 2025" pattern — three-letter month followed by space + year.
474
+ parts = label["text"].split()
475
+ assert len(parts) == 2
476
+ assert parts[1] == "2025"
477
+
478
+
479
+ def test_x_date_labels_yearly_for_multi_year_range(sample_viewport: Viewport) -> None:
480
+ """Three-year span → yearly format (2024, 2025, 2026)."""
481
+ pts = _normalize_points(
482
+ [
483
+ {"date": "2024-01-01", "count": 1},
484
+ {"date": "2025-06-01", "count": 100},
485
+ {"date": "2026-04-01", "count": 500},
486
+ ]
487
+ )
488
+ labels = _build_x_date_labels(pts, sample_viewport)
489
+ texts = [label["text"] for label in labels]
490
+ # Every label is a 4-digit year.
491
+ for text in texts:
492
+ assert text.isdigit() and len(text) == 4
493
+ # First and last are the endpoints of the data.
494
+ assert labels[0]["anchor"] == "start"
495
+ assert labels[0]["x"] == sample_viewport.x
496
+ # Data starts in 2024 and ends in 2026; endpoints must reflect that.
497
+ assert texts[0] == "2024"
498
+ assert texts[-1] == "2026"
499
+
500
+
501
+ def test_x_date_labels_daily_for_viral_two_week_span(sample_viewport: Viewport) -> None:
502
+ """2-week span (caveman case) → daily format ("Apr 05", "Apr 12"...).
503
+
504
+ This is the regression test that locks in the fix for the
505
+ JuliusBrussee/caveman rendering — the old year-only labels showed a
506
+ single lonely "2026" mid-axis; daily granularity makes the 2-week
507
+ adoption curve legible.
508
+ """
509
+ pts = _normalize_points(
510
+ [
511
+ {"date": "2026-04-05T00:00:00Z", "count": 1},
512
+ {"date": "2026-04-10T00:00:00Z", "count": 15_000},
513
+ {"date": "2026-04-19T00:00:00Z", "count": 40_000},
514
+ ]
515
+ )
516
+ labels = _build_x_date_labels(pts, sample_viewport)
517
+ # Every label uses "Mon DD" format — no year suffix, no single "2026".
518
+ for label in labels:
519
+ parts = label["text"].split()
520
+ assert len(parts) == 2, f"expected 'Mon DD' format, got {label['text']!r}"
521
+ # Day portion is zero-padded 2-digit integer.
522
+ assert parts[1].isdigit() and len(parts[1]) == 2
477
523
 
478
524
 
479
- def test_x_year_labels_multi_year_range(sample_viewport: Viewport) -> None:
480
- """Three-year spanstart year at left, interior years at jan-1 positions."""
525
+ def test_x_date_labels_every_other_year_for_ancient_repo(sample_viewport: Viewport) -> None:
526
+ """Repo spanning >10 years every-other-year ticks to keep the axis readable."""
527
+ pts = _normalize_points(
528
+ [
529
+ {"date": "2010-06-01", "count": 1},
530
+ {"date": "2026-06-01", "count": 500_000},
531
+ ]
532
+ )
533
+ labels = _build_x_date_labels(pts, sample_viewport)
534
+ texts = [label["text"] for label in labels]
535
+ # Endpoints always preserved.
536
+ assert texts[0] == "2010"
537
+ # Endpoint is dropped/merged into the last-kept if too close; 2026 should be present.
538
+ assert "2026" in texts
539
+
540
+
541
+ def test_x_date_labels_deoverlap_drops_clustered_middles(sample_viewport: Viewport) -> None:
542
+ """De-overlap pass: temporally-close middle labels are dropped; endpoints preserved.
543
+
544
+ With a narrow viewport (just 80px wide) and a 3-year span, the yearly
545
+ labels at jan-1 boundaries would collide. Only endpoints survive.
546
+ """
547
+ narrow_vp = Viewport(x=0, y=0, w=80, h=50)
481
548
  pts = _normalize_points(
482
549
  [
483
550
  {"date": "2024-01-01", "count": 1},
@@ -485,19 +552,42 @@ def test_x_year_labels_multi_year_range(sample_viewport: Viewport) -> None:
485
552
  {"date": "2026-04-01", "count": 500},
486
553
  ]
487
554
  )
488
- labels = _build_x_year_labels(pts, sample_viewport)
555
+ labels = _build_x_date_labels(pts, narrow_vp)
556
+ # Under severe width pressure, only the endpoints survive.
557
+ assert len(labels) <= 2
489
558
  texts = [label["text"] for label in labels]
490
- # Expect every year in the range represented exactly once.
491
- assert texts == ["2024", "2025", "2026"]
492
- # Start year anchored at left, subsequent labels middle/end.
559
+ assert "2024" in texts
560
+ assert "2026" in texts
561
+
562
+
563
+ def test_x_date_labels_preserve_endpoints_unconditionally(sample_viewport: Viewport) -> None:
564
+ """Even when middle labels are dropped, first and last labels persist."""
565
+ pts = _normalize_points(
566
+ [
567
+ {"date": "2024-01-01", "count": 1},
568
+ {"date": "2026-04-01", "count": 500},
569
+ ]
570
+ )
571
+ labels = _build_x_date_labels(pts, sample_viewport)
572
+ # Two-point input with wide viewport keeps both endpoints.
573
+ assert len(labels) >= 2
493
574
  assert labels[0]["anchor"] == "start"
494
- assert labels[0]["x"] == sample_viewport.x
575
+ assert labels[-1]["anchor"] == "end"
576
+
577
+
578
+ def test_x_date_labels_single_point_full_date(sample_viewport: Viewport) -> None:
579
+ """Single data point → one centered full-date label ("Apr 05, 2026")."""
580
+ pts = _normalize_points([{"date": "2026-04-05", "count": 1}])
581
+ labels = _build_x_date_labels(pts, sample_viewport)
582
+ assert len(labels) == 1
583
+ assert labels[0]["text"] == "Apr 05, 2026"
584
+ assert labels[0]["anchor"] == "middle"
495
585
 
496
586
 
497
- def test_x_year_labels_empty_points() -> None:
587
+ def test_x_date_labels_empty_points() -> None:
498
588
  """No points → no labels (caller should render an empty-state overlay instead)."""
499
589
  vp = Viewport(x=0, y=0, w=100, h=50)
500
- assert _build_x_year_labels([], vp) == []
590
+ assert _build_x_date_labels([], vp) == []
501
591
 
502
592
 
503
593
  # ── Zero-time-span defense (bug 2 reproducer) ────────────────────────