hyperweave 0.2.6__tar.gz → 0.2.8__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.6 → hyperweave-0.2.8}/CHANGELOG.md +43 -0
  2. {hyperweave-0.2.6 → hyperweave-0.2.8}/PKG-INFO +1 -1
  3. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/_version.py +2 -2
  4. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/compose/context.py +11 -7
  5. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/compose/resolver.py +53 -61
  6. hyperweave-0.2.8/src/hyperweave/compose/rhythm.py +109 -0
  7. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/config/settings.py +5 -1
  8. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/connectors/base.py +63 -0
  9. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/connectors/github.py +197 -32
  10. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/core/state.py +6 -0
  11. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/mcp/server.py +22 -0
  12. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/render/chart_engine.py +172 -194
  13. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/telemetry/contract.py +1 -1
  14. hyperweave-0.2.8/src/hyperweave/templates/components/chart-area.svg.j2 +11 -0
  15. hyperweave-0.2.8/src/hyperweave/templates/components/chart-axes.svg.j2 +2 -0
  16. hyperweave-0.2.8/src/hyperweave/templates/components/chart-empty-state.svg.j2 +8 -0
  17. hyperweave-0.2.8/src/hyperweave/templates/components/chart-gridlines.svg.j2 +2 -0
  18. hyperweave-0.2.8/src/hyperweave/templates/components/chart-markers/circle.svg.j2 +2 -0
  19. hyperweave-0.2.8/src/hyperweave/templates/components/chart-markers/diamond.svg.j2 +7 -0
  20. hyperweave-0.2.8/src/hyperweave/templates/components/chart-markers/endpoint-diamond.svg.j2 +14 -0
  21. hyperweave-0.2.8/src/hyperweave/templates/components/chart-markers/endpoint-rect.svg.j2 +12 -0
  22. hyperweave-0.2.8/src/hyperweave/templates/components/chart-markers/rect.svg.j2 +12 -0
  23. hyperweave-0.2.8/src/hyperweave/templates/components/chart-milestone.svg.j2 +7 -0
  24. hyperweave-0.2.8/src/hyperweave/templates/components/chart-polyline.svg.j2 +10 -0
  25. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/templates/components/treemap.svg.j2 +2 -2
  26. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/templates/frames/chart/brutalist-content.j2 +28 -9
  27. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/templates/frames/chart/chrome-content.j2 +27 -8
  28. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/templates/frames/receipt.svg.j2 +7 -1
  29. {hyperweave-0.2.6 → hyperweave-0.2.8}/tests/test_chart_engine.py +131 -50
  30. {hyperweave-0.2.6 → hyperweave-0.2.8}/tests/test_chart_frame.py +5 -5
  31. {hyperweave-0.2.6 → hyperweave-0.2.8}/tests/test_connectors.py +503 -0
  32. {hyperweave-0.2.6 → hyperweave-0.2.8}/tests/test_mcp.py +23 -0
  33. hyperweave-0.2.8/tests/test_resolver_rhythm.py +158 -0
  34. {hyperweave-0.2.6 → hyperweave-0.2.8}/tests/test_state_inference.py +22 -0
  35. {hyperweave-0.2.6 → hyperweave-0.2.8}/tests/test_telemetry.py +3 -3
  36. {hyperweave-0.2.6 → hyperweave-0.2.8}/tests/test_telemetry_integration.py +166 -2
  37. {hyperweave-0.2.6 → hyperweave-0.2.8}/.dockerignore +0 -0
  38. {hyperweave-0.2.6 → hyperweave-0.2.8}/.github/workflows/ci.yml +0 -0
  39. {hyperweave-0.2.6 → hyperweave-0.2.8}/.github/workflows/deploy.yml +0 -0
  40. {hyperweave-0.2.6 → hyperweave-0.2.8}/.github/workflows/publish.yml +0 -0
  41. {hyperweave-0.2.6 → hyperweave-0.2.8}/.gitignore +0 -0
  42. {hyperweave-0.2.6 → hyperweave-0.2.8}/.pre-commit-config.yaml +0 -0
  43. {hyperweave-0.2.6 → hyperweave-0.2.8}/Dockerfile +0 -0
  44. {hyperweave-0.2.6 → hyperweave-0.2.8}/LICENSE +0 -0
  45. {hyperweave-0.2.6 → hyperweave-0.2.8}/README.md +0 -0
  46. {hyperweave-0.2.6 → hyperweave-0.2.8}/assets/buttons/button-liquid.svg +0 -0
  47. {hyperweave-0.2.6 → hyperweave-0.2.8}/assets/cards/card-butterfly.svg +0 -0
  48. {hyperweave-0.2.6 → hyperweave-0.2.8}/assets/cards/card-python.svg +0 -0
  49. {hyperweave-0.2.6 → hyperweave-0.2.8}/assets/cards/card-sunflower.svg +0 -0
  50. {hyperweave-0.2.6 → hyperweave-0.2.8}/assets/cards/card-waves.svg +0 -0
  51. {hyperweave-0.2.6 → hyperweave-0.2.8}/assets/examples/brutalist-emerald/badge_critical.svg +0 -0
  52. {hyperweave-0.2.6 → hyperweave-0.2.8}/assets/examples/brutalist-emerald/badge_passing.svg +0 -0
  53. {hyperweave-0.2.6 → hyperweave-0.2.8}/assets/examples/brutalist-emerald/badge_warning.svg +0 -0
  54. {hyperweave-0.2.6 → hyperweave-0.2.8}/assets/examples/brutalist-emerald/banner.svg +0 -0
  55. {hyperweave-0.2.6 → hyperweave-0.2.8}/assets/examples/brutalist-emerald/icons/discord.svg +0 -0
  56. {hyperweave-0.2.6 → hyperweave-0.2.8}/assets/examples/brutalist-emerald/icons/notion.svg +0 -0
  57. {hyperweave-0.2.6 → hyperweave-0.2.8}/assets/examples/brutalist-emerald/icons/reddit.svg +0 -0
  58. {hyperweave-0.2.6 → hyperweave-0.2.8}/assets/examples/brutalist-emerald/icons/spotify.svg +0 -0
  59. {hyperweave-0.2.6 → hyperweave-0.2.8}/assets/examples/brutalist-emerald/icons/youtube.svg +0 -0
  60. {hyperweave-0.2.6 → hyperweave-0.2.8}/assets/examples/brutalist-emerald/marquee_counter.svg +0 -0
  61. {hyperweave-0.2.6 → hyperweave-0.2.8}/assets/examples/brutalist-emerald/marquee_horizontal.svg +0 -0
  62. {hyperweave-0.2.6 → hyperweave-0.2.8}/assets/examples/brutalist-emerald/marquee_vertical.svg +0 -0
  63. {hyperweave-0.2.6 → hyperweave-0.2.8}/assets/examples/brutalist-emerald/profile-cards/chart_stars_full.svg +0 -0
  64. {hyperweave-0.2.6 → hyperweave-0.2.8}/assets/examples/brutalist-emerald/profile-cards/stats.svg +0 -0
  65. {hyperweave-0.2.6 → hyperweave-0.2.8}/assets/examples/brutalist-emerald/strip.svg +0 -0
  66. {hyperweave-0.2.6 → hyperweave-0.2.8}/assets/examples/chrome-horizon/badge_critical.svg +0 -0
  67. {hyperweave-0.2.6 → hyperweave-0.2.8}/assets/examples/chrome-horizon/badge_passing.svg +0 -0
  68. {hyperweave-0.2.6 → hyperweave-0.2.8}/assets/examples/chrome-horizon/badge_warning.svg +0 -0
  69. {hyperweave-0.2.6 → hyperweave-0.2.8}/assets/examples/chrome-horizon/banner.svg +0 -0
  70. {hyperweave-0.2.6 → hyperweave-0.2.8}/assets/examples/chrome-horizon/icons/bluesky.svg +0 -0
  71. {hyperweave-0.2.6 → hyperweave-0.2.8}/assets/examples/chrome-horizon/icons/github.svg +0 -0
  72. {hyperweave-0.2.6 → hyperweave-0.2.8}/assets/examples/chrome-horizon/icons/instagram.svg +0 -0
  73. {hyperweave-0.2.6 → hyperweave-0.2.8}/assets/examples/chrome-horizon/icons/mastodon.svg +0 -0
  74. {hyperweave-0.2.6 → hyperweave-0.2.8}/assets/examples/chrome-horizon/icons/x.svg +0 -0
  75. {hyperweave-0.2.6 → hyperweave-0.2.8}/assets/examples/chrome-horizon/marquee_counter.svg +0 -0
  76. {hyperweave-0.2.6 → hyperweave-0.2.8}/assets/examples/chrome-horizon/marquee_horizontal.svg +0 -0
  77. {hyperweave-0.2.6 → hyperweave-0.2.8}/assets/examples/chrome-horizon/marquee_vertical.svg +0 -0
  78. {hyperweave-0.2.6 → hyperweave-0.2.8}/assets/examples/chrome-horizon/profile-cards/chart_stars_full.svg +0 -0
  79. {hyperweave-0.2.6 → hyperweave-0.2.8}/assets/examples/chrome-horizon/profile-cards/stats.svg +0 -0
  80. {hyperweave-0.2.6 → hyperweave-0.2.8}/assets/examples/chrome-horizon/strip.svg +0 -0
  81. {hyperweave-0.2.6 → hyperweave-0.2.8}/assets/examples/telemetry/master_card.svg +0 -0
  82. {hyperweave-0.2.6 → hyperweave-0.2.8}/assets/examples/telemetry/receipt.svg +0 -0
  83. {hyperweave-0.2.6 → hyperweave-0.2.8}/assets/examples/telemetry/rhythm_strip.svg +0 -0
  84. {hyperweave-0.2.6 → hyperweave-0.2.8}/assets/footers/inneraura-footer-liquid.svg +0 -0
  85. {hyperweave-0.2.6 → hyperweave-0.2.8}/assets/footers/inneraura-footer-purple.svg +0 -0
  86. {hyperweave-0.2.6 → hyperweave-0.2.8}/assets/footers/inneraura-footer.svg +0 -0
  87. {hyperweave-0.2.6 → hyperweave-0.2.8}/assets/hyperweave-banner.svg +0 -0
  88. {hyperweave-0.2.6 → hyperweave-0.2.8}/assets/icons/cobalt-sapphire-discord.svg +0 -0
  89. {hyperweave-0.2.6 → hyperweave-0.2.8}/assets/icons/cobalt-sapphire-docs.svg +0 -0
  90. {hyperweave-0.2.6 → hyperweave-0.2.8}/assets/icons/cobalt-sapphire-github.svg +0 -0
  91. {hyperweave-0.2.6 → hyperweave-0.2.8}/assets/icons/cobalt-sapphire-instagram.svg +0 -0
  92. {hyperweave-0.2.6 → hyperweave-0.2.8}/assets/icons/cobalt-sapphire-linkedin.svg +0 -0
  93. {hyperweave-0.2.6 → hyperweave-0.2.8}/assets/icons/cobalt-sapphire-tiktok.svg +0 -0
  94. {hyperweave-0.2.6 → hyperweave-0.2.8}/assets/icons/cobalt-sapphire-x.svg +0 -0
  95. {hyperweave-0.2.6 → hyperweave-0.2.8}/assets/icons/cobalt-sapphire-youtube.svg +0 -0
  96. {hyperweave-0.2.6 → hyperweave-0.2.8}/assets/marquees/badge-showcase-triple.svg +0 -0
  97. {hyperweave-0.2.6 → hyperweave-0.2.8}/assets/marquees/genome-marquee-triple.svg +0 -0
  98. {hyperweave-0.2.6 → hyperweave-0.2.8}/assets/marquees/sample-badges.svg +0 -0
  99. {hyperweave-0.2.6 → hyperweave-0.2.8}/assets/mintlify-assets/404.svg +0 -0
  100. {hyperweave-0.2.6 → hyperweave-0.2.8}/assets/mintlify-assets/callout-icons.svg +0 -0
  101. {hyperweave-0.2.6 → hyperweave-0.2.8}/assets/mintlify-assets/divider.svg +0 -0
  102. {hyperweave-0.2.6 → hyperweave-0.2.8}/assets/mintlify-assets/favicon.svg +0 -0
  103. {hyperweave-0.2.6 → hyperweave-0.2.8}/assets/mintlify-assets/hero-banner.svg +0 -0
  104. {hyperweave-0.2.6 → hyperweave-0.2.8}/assets/mintlify-assets/hyperweave-banner-v2.svg +0 -0
  105. {hyperweave-0.2.6 → hyperweave-0.2.8}/assets/mintlify-assets/hyperweave-navbar-logo.svg +0 -0
  106. {hyperweave-0.2.6 → hyperweave-0.2.8}/assets/mintlify-assets/loader.svg +0 -0
  107. {hyperweave-0.2.6 → hyperweave-0.2.8}/assets/mintlify-assets/og-image.svg +0 -0
  108. {hyperweave-0.2.6 → hyperweave-0.2.8}/assets/timelines/hyperweave-roadmap.svg +0 -0
  109. {hyperweave-0.2.6 → hyperweave-0.2.8}/docs/assets/favicon.svg +0 -0
  110. {hyperweave-0.2.6 → hyperweave-0.2.8}/docs/assets/hyperweave-banner.svg +0 -0
  111. {hyperweave-0.2.6 → hyperweave-0.2.8}/docs/assets/hyperweave-navbar-logo.svg +0 -0
  112. {hyperweave-0.2.6 → hyperweave-0.2.8}/docs/assets/og-image.svg +0 -0
  113. {hyperweave-0.2.6 → hyperweave-0.2.8}/docs/explanation/architecture.mdx +0 -0
  114. {hyperweave-0.2.6 → hyperweave-0.2.8}/docs/explanation/camo-compatibility.mdx +0 -0
  115. {hyperweave-0.2.6 → hyperweave-0.2.8}/docs/explanation/cim-compliance.mdx +0 -0
  116. {hyperweave-0.2.6 → hyperweave-0.2.8}/docs/explanation/genome-profile-system.mdx +0 -0
  117. {hyperweave-0.2.6 → hyperweave-0.2.8}/docs/explanation/metadata-tiers.mdx +0 -0
  118. {hyperweave-0.2.6 → hyperweave-0.2.8}/docs/how-to/add-motion-to-badges.mdx +0 -0
  119. {hyperweave-0.2.6 → hyperweave-0.2.8}/docs/how-to/create-session-receipts.mdx +0 -0
  120. {hyperweave-0.2.6 → hyperweave-0.2.8}/docs/how-to/set-up-claude-code-hooks.mdx +0 -0
  121. {hyperweave-0.2.6 → hyperweave-0.2.8}/docs/how-to/use-live-data-badges.mdx +0 -0
  122. {hyperweave-0.2.6 → hyperweave-0.2.8}/docs/introduction.mdx +0 -0
  123. {hyperweave-0.2.6 → hyperweave-0.2.8}/docs/mint.json +0 -0
  124. {hyperweave-0.2.6 → hyperweave-0.2.8}/docs/quickstart.mdx +0 -0
  125. {hyperweave-0.2.6 → hyperweave-0.2.8}/docs/reference/cli.mdx +0 -0
  126. {hyperweave-0.2.6 → hyperweave-0.2.8}/docs/reference/compose-spec.mdx +0 -0
  127. {hyperweave-0.2.6 → hyperweave-0.2.8}/docs/reference/genomes.mdx +0 -0
  128. {hyperweave-0.2.6 → hyperweave-0.2.8}/docs/reference/http-api.mdx +0 -0
  129. {hyperweave-0.2.6 → hyperweave-0.2.8}/docs/reference/mcp-tools.mdx +0 -0
  130. {hyperweave-0.2.6 → hyperweave-0.2.8}/docs/reference/motions.mdx +0 -0
  131. {hyperweave-0.2.6 → hyperweave-0.2.8}/docs/reference/telemetry-contract.mdx +0 -0
  132. {hyperweave-0.2.6 → hyperweave-0.2.8}/docs/spec/hyperweave-protocol.mdx +0 -0
  133. {hyperweave-0.2.6 → hyperweave-0.2.8}/docs/tutorials/build-an-artifact-kit.mdx +0 -0
  134. {hyperweave-0.2.6 → hyperweave-0.2.8}/docs/tutorials/compose-your-first-badge.mdx +0 -0
  135. {hyperweave-0.2.6 → hyperweave-0.2.8}/docs/tutorials/install-session-telemetry.mdx +0 -0
  136. {hyperweave-0.2.6 → hyperweave-0.2.8}/fly.toml +0 -0
  137. {hyperweave-0.2.6 → hyperweave-0.2.8}/hooks/install.py +0 -0
  138. {hyperweave-0.2.6 → hyperweave-0.2.8}/hooks/session_end.sh +0 -0
  139. {hyperweave-0.2.6 → hyperweave-0.2.8}/justfile +0 -0
  140. {hyperweave-0.2.6 → hyperweave-0.2.8}/pyproject.toml +0 -0
  141. {hyperweave-0.2.6 → hyperweave-0.2.8}/scripts/extract_font_metrics.py +0 -0
  142. {hyperweave-0.2.6 → hyperweave-0.2.8}/scripts/extract_glyphs.py +0 -0
  143. {hyperweave-0.2.6 → hyperweave-0.2.8}/scripts/generate_hw_compliant.py +0 -0
  144. {hyperweave-0.2.6 → hyperweave-0.2.8}/scripts/generate_proofset.py +0 -0
  145. {hyperweave-0.2.6 → hyperweave-0.2.8}/scripts/stress_test.py +0 -0
  146. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/__init__.py +0 -0
  147. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/__main__.py +0 -0
  148. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/cli.py +0 -0
  149. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/compose/__init__.py +0 -0
  150. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/compose/assembler.py +0 -0
  151. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/compose/engine.py +0 -0
  152. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/compose/lanes.py +0 -0
  153. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/compose/resolvers/__init__.py +0 -0
  154. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/compose/resolvers/chart.py +0 -0
  155. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/compose/resolvers/stats.py +0 -0
  156. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/compose/resolvers/timeline.py +0 -0
  157. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/compose/validate_paradigms.py +0 -0
  158. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/config/__init__.py +0 -0
  159. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/config/genome_validator.py +0 -0
  160. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/config/loader.py +0 -0
  161. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/config/registry.py +0 -0
  162. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/connectors/__init__.py +0 -0
  163. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/connectors/arxiv.py +0 -0
  164. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/connectors/cache.py +0 -0
  165. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/connectors/rest.py +0 -0
  166. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/core/__init__.py +0 -0
  167. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/core/color.py +0 -0
  168. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/core/contracts.py +0 -0
  169. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/core/enums.py +0 -0
  170. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/core/font_metrics.py +0 -0
  171. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/core/models.py +0 -0
  172. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/core/paradigm.py +0 -0
  173. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/core/schema.py +0 -0
  174. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/core/text.py +0 -0
  175. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/core/thresholds.py +0 -0
  176. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/data/css/accessibility.css +0 -0
  177. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/data/css/bridge.css +0 -0
  178. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/data/css/expression.css +0 -0
  179. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/data/css/phase-colors.css +0 -0
  180. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/data/css/status.css +0 -0
  181. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/data/css/telemetry.css +0 -0
  182. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/data/font-metrics/inter.json +0 -0
  183. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/data/font-metrics/jetbrains-mono.json +0 -0
  184. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/data/font-metrics/orbitron.json +0 -0
  185. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/data/fonts/jetbrains-mono.b64 +0 -0
  186. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/data/fonts/jetbrains-mono.meta.json +0 -0
  187. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/data/fonts/orbitron.b64 +0 -0
  188. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/data/fonts/orbitron.meta.json +0 -0
  189. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/data/genomes/brutalist-emerald.json +0 -0
  190. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/data/genomes/chrome-horizon.json +0 -0
  191. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/data/genomes/telemetry-void.json +0 -0
  192. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/data/glyphs.json +0 -0
  193. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/data/motions/border/chromatic-pulse.yaml +0 -0
  194. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/data/motions/border/corner-trace.yaml +0 -0
  195. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/data/motions/border/dual-orbit.yaml +0 -0
  196. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/data/motions/border/entanglement.yaml +0 -0
  197. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/data/motions/border/rimrun.yaml +0 -0
  198. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/data/motions/kinetic/bars.yaml +0 -0
  199. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/data/motions/kinetic/breach.yaml +0 -0
  200. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/data/motions/kinetic/broadcast.yaml +0 -0
  201. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/data/motions/kinetic/cascade.yaml +0 -0
  202. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/data/motions/kinetic/collapse.yaml +0 -0
  203. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/data/motions/kinetic/converge.yaml +0 -0
  204. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/data/motions/kinetic/crash.yaml +0 -0
  205. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/data/motions/kinetic/drop.yaml +0 -0
  206. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/data/motions/kinetic/pulse.yaml +0 -0
  207. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/data/motions/static.yaml +0 -0
  208. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/data/paradigms/brutalist.yaml +0 -0
  209. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/data/paradigms/chrome.yaml +0 -0
  210. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/data/paradigms/default.yaml +0 -0
  211. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/data/policies/normal.json +0 -0
  212. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/data/policies/permissive.json +0 -0
  213. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/data/policies/ungoverned.json +0 -0
  214. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/data/profiles/brutalist.contract.json +0 -0
  215. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/data/profiles/brutalist.yaml +0 -0
  216. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/data/profiles/chrome.contract.json +0 -0
  217. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/data/profiles/chrome.yaml +0 -0
  218. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/data/specimens.yaml +0 -0
  219. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/data/telemetry/model-pricing.yaml +0 -0
  220. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/data/telemetry/stage-config.yaml +0 -0
  221. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/data/telemetry/stage-labels.yaml +0 -0
  222. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/data/telemetry/tool-classes.yaml +0 -0
  223. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/data/telemetry/tool-colors.yaml +0 -0
  224. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/data/terminals/arrow.json +0 -0
  225. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/data/terminals/aurora.json +0 -0
  226. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/data/terminals/beacon.json +0 -0
  227. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/data/terminals/bracket3d.json +0 -0
  228. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/data/terminals/chevron.json +0 -0
  229. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/data/terminals/crosshair.json +0 -0
  230. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/data/terminals/diamond.json +0 -0
  231. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/data/terminals/rocket.json +0 -0
  232. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/data/terminals/rules/bar.json +0 -0
  233. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/data/terminals/rules/dashed.json +0 -0
  234. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/data/terminals/rules/five-wave.json +0 -0
  235. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/data/terminals/rules/spectral.json +0 -0
  236. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/data/terminals/rules/straight.json +0 -0
  237. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/data/terminals/rules/wave.json +0 -0
  238. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/data/terminals/stijl.json +0 -0
  239. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/kit.py +0 -0
  240. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/mcp/__init__.py +0 -0
  241. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/mcp/__main__.py +0 -0
  242. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/py.typed +0 -0
  243. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/render/__init__.py +0 -0
  244. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/render/fonts.py +0 -0
  245. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/render/glyphs.py +0 -0
  246. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/render/motion.py +0 -0
  247. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/render/templates.py +0 -0
  248. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/serve/__init__.py +0 -0
  249. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/serve/app.py +0 -0
  250. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/telemetry/__init__.py +0 -0
  251. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/telemetry/capture.py +0 -0
  252. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/telemetry/corrections.py +0 -0
  253. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/telemetry/cost.py +0 -0
  254. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/telemetry/models.py +0 -0
  255. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/telemetry/parser.py +0 -0
  256. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/telemetry/stages.py +0 -0
  257. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/templates/components/filter.svg.j2 +0 -0
  258. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/templates/components/glyph-inline.svg.j2 +0 -0
  259. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/templates/components/glyph.svg.j2 +0 -0
  260. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/templates/components/gradient.svg.j2 +0 -0
  261. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/templates/components/metadata.svg.j2 +0 -0
  262. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/templates/components/metric.svg.j2 +0 -0
  263. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/templates/components/phase-segment.svg.j2 +0 -0
  264. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/templates/components/rule.svg.j2 +0 -0
  265. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/templates/components/status.svg.j2 +0 -0
  266. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/templates/components/terminal.svg.j2 +0 -0
  267. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/templates/document.svg.j2 +0 -0
  268. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/templates/error-badge.svg.j2 +0 -0
  269. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/templates/frames/badge/brutalist-content.j2 +0 -0
  270. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/templates/frames/badge/brutalist-defs.j2 +0 -0
  271. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/templates/frames/badge/chrome-content.j2 +0 -0
  272. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/templates/frames/badge/chrome-defs.j2 +0 -0
  273. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/templates/frames/badge/default-content.j2 +0 -0
  274. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/templates/frames/badge/default-defs.j2 +0 -0
  275. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/templates/frames/badge.svg.j2 +0 -0
  276. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/templates/frames/banner/brutalist-defs.j2 +0 -0
  277. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/templates/frames/banner/chrome-content.j2 +0 -0
  278. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/templates/frames/banner/chrome-defs.j2 +0 -0
  279. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/templates/frames/banner/default-content.j2 +0 -0
  280. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/templates/frames/banner/default-defs.j2 +0 -0
  281. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/templates/frames/banner.svg.j2 +0 -0
  282. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/templates/frames/catalog.svg.j2 +0 -0
  283. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/templates/frames/chart/brutalist-defs.j2 +0 -0
  284. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/templates/frames/chart/chrome-defs.j2 +0 -0
  285. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/templates/frames/chart.svg.j2 +0 -0
  286. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/templates/frames/divider.svg.j2 +0 -0
  287. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/templates/frames/icon/brutalist-content.j2 +0 -0
  288. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/templates/frames/icon/brutalist-defs.j2 +0 -0
  289. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/templates/frames/icon/chrome-content.j2 +0 -0
  290. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/templates/frames/icon/chrome-defs.j2 +0 -0
  291. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/templates/frames/icon/default-content.j2 +0 -0
  292. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/templates/frames/icon/default-defs.j2 +0 -0
  293. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/templates/frames/icon.svg.j2 +0 -0
  294. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/templates/frames/marquee-counter/brutalist-content.j2 +0 -0
  295. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/templates/frames/marquee-counter/brutalist-defs.j2 +0 -0
  296. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/templates/frames/marquee-counter/chrome-content.j2 +0 -0
  297. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/templates/frames/marquee-counter/chrome-defs.j2 +0 -0
  298. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/templates/frames/marquee-counter.svg.j2 +0 -0
  299. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/templates/frames/marquee-horizontal/brutalist-content.j2 +0 -0
  300. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/templates/frames/marquee-horizontal/brutalist-defs.j2 +0 -0
  301. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/templates/frames/marquee-horizontal/chrome-content.j2 +0 -0
  302. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/templates/frames/marquee-horizontal/chrome-defs.j2 +0 -0
  303. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/templates/frames/marquee-horizontal.svg.j2 +0 -0
  304. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/templates/frames/marquee-vertical/brutalist-content.j2 +0 -0
  305. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/templates/frames/marquee-vertical/brutalist-defs.j2 +0 -0
  306. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/templates/frames/marquee-vertical/chrome-content.j2 +0 -0
  307. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/templates/frames/marquee-vertical/chrome-defs.j2 +0 -0
  308. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/templates/frames/marquee-vertical.svg.j2 +0 -0
  309. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/templates/frames/master-card.svg.j2 +0 -0
  310. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/templates/frames/rhythm-strip.svg.j2 +0 -0
  311. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/templates/frames/stats/brutalist-content.j2 +0 -0
  312. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/templates/frames/stats/brutalist-defs.j2 +0 -0
  313. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/templates/frames/stats/chrome-content.j2 +0 -0
  314. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/templates/frames/stats/chrome-defs.j2 +0 -0
  315. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/templates/frames/stats.svg.j2 +0 -0
  316. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/templates/frames/strip/brutalist-content.j2 +0 -0
  317. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/templates/frames/strip/brutalist-defs.j2 +0 -0
  318. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/templates/frames/strip/brutalist-status.j2 +0 -0
  319. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/templates/frames/strip/chrome-content.j2 +0 -0
  320. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/templates/frames/strip/chrome-defs.j2 +0 -0
  321. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/templates/frames/strip/chrome-status.j2 +0 -0
  322. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/templates/frames/strip/default-content.j2 +0 -0
  323. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/templates/frames/strip/default-defs.j2 +0 -0
  324. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/templates/frames/strip/default-status.j2 +0 -0
  325. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/templates/frames/strip.svg.j2 +0 -0
  326. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/templates/frames/timeline/default-content.j2 +0 -0
  327. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/templates/frames/timeline.svg.j2 +0 -0
  328. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/templates/motions/border/chromatic-pulse.svg.j2 +0 -0
  329. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/templates/motions/border/corner-trace.svg.j2 +0 -0
  330. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/templates/motions/border/dual-orbit.svg.j2 +0 -0
  331. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/templates/motions/border/entanglement.svg.j2 +0 -0
  332. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/templates/motions/border/rimrun.svg.j2 +0 -0
  333. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/templates/motions/kinetic/bars.svg.j2 +0 -0
  334. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/templates/motions/kinetic/breach.svg.j2 +0 -0
  335. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/templates/motions/kinetic/broadcast.svg.j2 +0 -0
  336. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/templates/motions/kinetic/cascade.svg.j2 +0 -0
  337. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/templates/motions/kinetic/collapse.svg.j2 +0 -0
  338. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/templates/motions/kinetic/converge.svg.j2 +0 -0
  339. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/templates/motions/kinetic/crash.svg.j2 +0 -0
  340. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/templates/motions/kinetic/drop.svg.j2 +0 -0
  341. {hyperweave-0.2.6 → hyperweave-0.2.8}/src/hyperweave/templates/motions/kinetic/pulse.svg.j2 +0 -0
  342. {hyperweave-0.2.6 → hyperweave-0.2.8}/tests/__init__.py +0 -0
  343. {hyperweave-0.2.6 → hyperweave-0.2.8}/tests/conftest.py +0 -0
  344. {hyperweave-0.2.6 → hyperweave-0.2.8}/tests/fixtures/github_contributions/synthetic.html +0 -0
  345. {hyperweave-0.2.6 → hyperweave-0.2.8}/tests/fixtures/session.json +0 -0
  346. {hyperweave-0.2.6 → hyperweave-0.2.8}/tests/fixtures/session.jsonl +0 -0
  347. {hyperweave-0.2.6 → hyperweave-0.2.8}/tests/helpers.py +0 -0
  348. {hyperweave-0.2.6 → hyperweave-0.2.8}/tests/test_assembler.py +0 -0
  349. {hyperweave-0.2.6 → hyperweave-0.2.8}/tests/test_core.py +0 -0
  350. {hyperweave-0.2.6 → hyperweave-0.2.8}/tests/test_github_scrape.py +0 -0
  351. {hyperweave-0.2.6 → hyperweave-0.2.8}/tests/test_kit.py +0 -0
  352. {hyperweave-0.2.6 → hyperweave-0.2.8}/tests/test_paradigm_dispatch.py +0 -0
  353. {hyperweave-0.2.6 → hyperweave-0.2.8}/tests/test_paradigm_extensibility.py +0 -0
  354. {hyperweave-0.2.6 → hyperweave-0.2.8}/tests/test_proofset.py +0 -0
  355. {hyperweave-0.2.6 → hyperweave-0.2.8}/tests/test_render.py +0 -0
  356. {hyperweave-0.2.6 → hyperweave-0.2.8}/tests/test_serve.py +0 -0
  357. {hyperweave-0.2.6 → hyperweave-0.2.8}/tests/test_serve_live_state.py +0 -0
  358. {hyperweave-0.2.6 → hyperweave-0.2.8}/tests/test_stats_card.py +0 -0
  359. {hyperweave-0.2.6 → hyperweave-0.2.8}/tests/test_timeline.py +0 -0
  360. {hyperweave-0.2.6 → hyperweave-0.2.8}/uv.lock +0 -0
@@ -5,6 +5,49 @@ 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.8] - 2026-04-20
9
+
10
+ ### Fixed
11
+
12
+ - **Star history charts now render readably on large repositories.** Previously, repos with more than ~40,000 stars produced a flat-then-vertical "hockey stick" because GitHub's stargazer REST endpoint hard-caps deep pagination, so the only data reachable was a small cluster of early-history stars followed by a single jump to today. Star history now sources from GitHub's GraphQL API and adapts its sampling window to the repo: small repos continue to get full lifetime views, large repos get a detailed recent-growth curve similar to the one star-history.com shows.
13
+ - **Star-count milestone labels no longer stack on top of each other.** When sampled points clustered temporally, the milestone labels (`500`, `1K`, `5K`, `10K`, ...) would render in a single illegible pile on top of the crossing point. Milestones now maintain a minimum 40-pixel horizontal gap; any labels that would overlap with an already-placed one are dropped in favor of the first (lowest-threshold) milestone in the cluster.
14
+
15
+ ### Changed
16
+
17
+ - **All chart rendering now flows through template partials.** The chart engine's remaining rendering paths — axes, gridlines, polyline, area fill, milestones, and the empty-state overlay — joined the marker layer in Jinja. The engine module returns structured Python data; Jinja partials under `templates/components/` produce the final SVG. No visual change; output is byte-identical for the shipped genomes on unchanged inputs.
18
+
19
+ ### Added
20
+
21
+ - **`fetch_graphql` connector primitive.** New async helper in the connector layer that handles POST with JSON bodies, GitHub bearer-token rotation, circuit-breaker coordination, and SSRF validation — mirroring the existing `fetch_json` contract. This unlocks future migration of the stats card's six REST sub-fetches to a single GraphQL call, and keeps a single canonical place for authenticated-POST plumbing.
22
+
23
+ ### Dev
24
+
25
+ - GraphQL stargazer pipeline is covered by unit tests for the primary path, REST fallback on failure, no-token skip, mega-repo window cap, small-repo history exhaustion, downsampling correctness, and current-UTC now-point stamping.
26
+
27
+ ## [0.2.7] - 2026-04-20
28
+
29
+ ### Fixed
30
+
31
+ - **Star history charts now end at the current date.** On very large repositories (40k+ stars), GitHub caps stargazer pagination at roughly the first 40,000 stars, so the latest sample pulled from the API was often years old and the polyline terminated in the past. The chart now always appends a final data point stamped at the current time, while still reporting the real total star count. Small repos are unaffected.
32
+ - **Session receipt labels are honest again.** The "N corrections" line previously counted every user turn — any pushback, redirect, or elaboration — and rendered them next to tool-failure marks (`✗N`) on the token treemap, which created the impression that the two numbers should reconcile. They're now split into "N user turns" and "N tool errors", with tool-error counts tinted red to match the cell marks. A new legend — `✗N = failed tool calls` — appears above the token map so the red marks are self-explanatory.
33
+ - **Session receipt hero no longer misreports the dominant work phase.** The hero badge previously showed the label of the first detected stage, even when that stage lasted two minutes and a later stage dominated the session. It now uses the stage with the largest share of tool calls, and falls back to `MIXED` when no single stage exceeds 20%.
34
+ - **Rhythm bars stop overflowing their track on long sessions.** On sessions with many stages (~30+), the rightmost rhythm bar on the receipt could render hundreds of pixels past the track's right edge. Receipt and rhythm-strip now share a single layout routine with a proper gap budget and a post-hoc rescale, so bars always fit the track regardless of stage count.
35
+ - **Rhythm bars encode time, not tool-call share.** When the telemetry contract carries start and end timestamps per stage, bar widths are now proportional to stage duration, so the time-axis labels (`0m · 104m · 209m`) actually correspond to bar positions. Bar heights are now uniform; the previous height scaling was effectively noise because most bars hit the minimum-height floor.
36
+ - **Live badges now recognize `building` as a state.** A badge with `value="building"` (or `"rebuilding"` / `"build"`) now renders as the building state instead of falling through to the default `active`. Longer phrases containing the word "build" are not affected — only those three exact tokens.
37
+ - **Deploying `HW_GITHUB_TOKENS` no longer crashes the app.** A vestigial `github_tokens` field on the settings schema was trying to parse the environment variable as JSON on startup, so setting the plain comma-separated secret that the token-rotation code actually expects caused a 500. The field has been removed; the connector reads the secret directly, and plain comma-separated deployments now work as documented.
38
+
39
+ ### Added
40
+
41
+ - **`hw_discover` advertises the `stats`, `chart`, and `timeline` routes.** MCP clients calling `hw_discover(what="url_grammar")` now receive URL patterns, example URLs, and method hints for the three routes that shipped in v0.2.0 but were missing from discovery.
42
+
43
+ ### Changed
44
+
45
+ - **Chart markers render through template partials.** Marker shapes (rect, circle, diamond, and their endpoint variants) now live as Jinja partials under `templates/components/chart-markers/`, matching how the rest of the rendering pipeline handles SVG. No visual change; output is byte-identical for the shipped genomes.
46
+
47
+ ### Dev
48
+
49
+ - GitHub token pool rotation is now covered by unit tests (`HW_GITHUB_TOKENS` comma-separated list, `GITHUB_TOKEN` single-token fallback, whitespace tolerance, empty-env behavior).
50
+
8
51
  ## [0.2.6] - 2026-04-19
9
52
 
10
53
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hyperweave
3
- Version: 0.2.6
3
+ Version: 0.2.8
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.6'
22
- __version_tuple__ = version_tuple = (0, 2, 6)
21
+ __version__ = version = '0.2.8'
22
+ __version_tuple__ = version_tuple = (0, 2, 8)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -337,12 +337,16 @@ def _ctx_chart(spec: ComposeSpec, resolved: ResolvedArtifact, css: dict[str, str
337
337
  ctx["chart_viewport_w"] = 0
338
338
  ctx["chart_viewport_h"] = 0
339
339
  ctx["chart_defs"] = ""
340
- ctx["chart_axes"] = ""
341
- ctx["chart_gridlines"] = ""
342
- ctx["chart_area"] = ""
343
- ctx["chart_polyline"] = ""
344
- ctx["chart_markers"] = ""
345
- ctx["chart_milestones"] = ""
340
+ # Post-v0.2.8: axes / gridlines / milestones / markers all return structured
341
+ # lists; polyline / area / empty_state are dicts or None so templates can
342
+ # use ``{% if %}`` to guard includes without a StrictUndefined trap.
343
+ ctx["chart_axes"] = []
344
+ ctx["chart_gridlines"] = []
345
+ ctx["chart_area"] = None
346
+ ctx["chart_polyline"] = None
347
+ ctx["chart_markers"] = []
348
+ ctx["chart_milestones"] = []
349
+ ctx["chart_empty_state"] = None
346
350
  ctx["data_hw_status"] = "fresh"
347
351
  ctx.update(resolved.frame_context)
348
352
  return ctx
@@ -371,7 +375,7 @@ def _ctx_stats(spec: ComposeSpec, resolved: ResolvedArtifact, css: dict[str, str
371
375
  ctx["embedded_chart_defs"] = ""
372
376
  ctx["embedded_chart_area"] = ""
373
377
  ctx["embedded_chart_polyline"] = ""
374
- ctx["embedded_chart_markers"] = ""
378
+ ctx["embedded_chart_markers"] = []
375
379
  ctx.update(resolved.frame_context)
376
380
  return ctx
377
381
 
@@ -6,6 +6,7 @@ from datetime import datetime
6
6
  from pathlib import Path
7
7
  from typing import TYPE_CHECKING, Any
8
8
 
9
+ from hyperweave.compose.rhythm import layout_rhythm_bars
9
10
  from hyperweave.core.enums import (
10
11
  FrameType,
11
12
  GlyphMode,
@@ -1112,7 +1113,7 @@ def resolve_receipt(
1112
1113
  profile_data: dict[str, Any] = tel.get("profile", {})
1113
1114
  tools_raw = tel.get("tools", {})
1114
1115
  stages_raw: list[dict[str, Any]] = tel.get("stages", [])
1115
- corrections: list[dict[str, Any]] = tel.get("corrections", [])
1116
+ user_events: list[dict[str, Any]] = tel.get("user_events", [])
1116
1117
  agents: list[dict[str, Any]] = tel.get("agents", [])
1117
1118
 
1118
1119
  # ── Normalize tools: contract produces dict keyed by name, templates need list ──
@@ -1122,7 +1123,8 @@ def resolve_receipt(
1122
1123
  tools = list(tools_raw)
1123
1124
 
1124
1125
  # ── Normalize stages: contract produces {label, dominant_class, start, end, tools} ──
1125
- # Templates need {name, pct, tool_class} with percentage proportions
1126
+ # Templates need {name, pct, tool_class}; start/end are preserved so
1127
+ # :func:`layout_rhythm_bars` can lay out time-proportional x/w.
1126
1128
  total_stage_tools = sum(s.get("tools", 1) for s in stages_raw) or 1
1127
1129
  stages: list[dict[str, Any]] = [
1128
1130
  {
@@ -1130,6 +1132,8 @@ def resolve_receipt(
1130
1132
  "pct": round(s.get("tools", 1) / total_stage_tools * 100),
1131
1133
  "label": s.get("label", ""),
1132
1134
  "tool_class": s.get("dominant_class", "explore"),
1135
+ "start": s.get("start"),
1136
+ "end": s.get("end"),
1133
1137
  }
1134
1138
  for s in stages_raw
1135
1139
  ]
@@ -1161,21 +1165,43 @@ def resolve_receipt(
1161
1165
  "Send": "coordinate",
1162
1166
  }
1163
1167
 
1168
+ # ── Dominant phase (drives hero badge + bottom-right phase label) ──
1169
+ # Using stages[0] was the old bug — for a session where the first 2-minute
1170
+ # stage classified as "validation" but the dominant (45% of tool calls)
1171
+ # was "implementation", the hero badge lied. When no single stage owns
1172
+ # at least 20% of the tool calls, fall back to "MIXED" to avoid
1173
+ # overclaiming a dominant phase that doesn't exist.
1174
+ dominant = max(stages, key=lambda s: s.get("pct", 0)) if stages else None
1175
+ dominant_label = (dominant.get("label") or dominant.get("name") or "") if dominant else ""
1176
+ dominant_pct = dominant.get("pct", 0) if dominant else 0
1177
+
1164
1178
  # ── Hero row ──
1165
1179
  hero_headline = f"{_fmt_tok(total_tok)} tokens billed · ${total_cost:.2f}"
1166
1180
  dur_label = f"{int(duration_m)}m" if duration_m else "—"
1167
1181
  hero_subline = f"{model} · {dur_label} · {calls} calls"
1168
- hero_profile = stages[0]["label"].upper() if stages else "SESSION"
1169
- hero_tool_class = stages[0]["tool_class"] if stages else "explore"
1170
- n_corrections = len(corrections)
1182
+ if not dominant:
1183
+ hero_profile = "SESSION"
1184
+ elif dominant_pct < 20:
1185
+ hero_profile = "MIXED"
1186
+ else:
1187
+ hero_profile = dominant_label.upper()
1188
+ hero_tool_class = dominant["tool_class"] if dominant else "explore"
1189
+ # Split "pushbacks" into distinct signals so the card stops labeling them
1190
+ # as one opaque "N corrections" lie. user_events counts every non-continuation
1191
+ # user turn (corrections + redirects + elaborations); tool errors count
1192
+ # failing/blocked tool calls (the red ✗N cell marks reconcile to this).
1193
+ n_user_turns = len(user_events)
1194
+ n_tool_errors = sum(t.get("errors", 0) + t.get("blocked", 0) for t in tools)
1171
1195
  n_agents = len(agents)
1172
- hero_right = [
1173
- f"{_fmt_tok(total_input)} in / {_fmt_tok(total_output)} out",
1196
+ hero_right: list[dict[str, str]] = [
1197
+ {"text": f"{_fmt_tok(total_input)} in / {_fmt_tok(total_output)} out"},
1174
1198
  ]
1175
1199
  if total_cache_read or total_cache_create:
1176
- hero_right.append(f"{_fmt_tok(total_cache_read)} cached / {_fmt_tok(total_cache_create)} written")
1177
- if n_corrections:
1178
- hero_right.append(f"{n_corrections} correction{'s' if n_corrections != 1 else ''}")
1200
+ hero_right.append({"text": f"{_fmt_tok(total_cache_read)} cached / {_fmt_tok(total_cache_create)} written"})
1201
+ if n_user_turns:
1202
+ hero_right.append({"text": f"{n_user_turns} user turn{'s' if n_user_turns != 1 else ''}"})
1203
+ if n_tool_errors:
1204
+ hero_right.append({"text": f"{n_tool_errors} tool errors", "accent": "failing"})
1179
1205
 
1180
1206
  # ── Treemap layout (3-tier, 752px wide, token-proportional) ──
1181
1207
  content_w = 752
@@ -1253,29 +1279,16 @@ def resolve_receipt(
1253
1279
  )
1254
1280
  x += min(w, 180) + 4
1255
1281
 
1256
- # ── Rhythm bars (scale stages to 752px) ──
1257
- total_stage_pct = sum(s.get("pct", 0) for s in stages) or 100
1258
- rhythm_bars: list[dict[str, Any]] = []
1259
- rx = 0
1282
+ # ── Rhythm bars ──
1283
+ # Delegated to the shared helper so receipt + rhythm-strip can't drift.
1284
+ # See src/hyperweave/compose/rhythm.py for the two-pass algorithm.
1260
1285
  bar_area_h = 92
1261
- for s in stages:
1262
- pct = s.get("pct", 0)
1263
- w = max(int(content_w * pct / total_stage_pct), 6)
1264
- h = max(int(bar_area_h * (pct / 50)), 8)
1265
- y = bar_area_h - h
1266
- tc = s.get("tool_class", s.get("name", "explore"))
1267
- rhythm_bars.append({"x": rx, "y": y, "w": w, "h": h, "tool_class": tc})
1268
- rx += w + 2
1286
+ rhythm_bars = layout_rhythm_bars(stages, area_w=content_w, area_h=bar_area_h)
1269
1287
 
1270
1288
  # ── Legend entries ──
1271
1289
  used_classes = sorted({c["tool_class"] for c in treemap_cells}) if treemap_cells else ["explore"]
1272
1290
  treemap_legend = [{"tool_class": tc, "label": tc} for tc in used_classes]
1273
1291
 
1274
- # ── Dominant phase ──
1275
- dominant = max(stages, key=lambda s: s.get("pct", 0)) if stages else {"name": "", "pct": 0}
1276
- dominant_label = dominant.get("label", dominant.get("name", ""))
1277
- dominant_pct = dominant.get("pct", 0)
1278
-
1279
1292
  # ── Metadata band (v0.4): provenance row between rhythm legend and footer ──
1280
1293
  session_id = session.get("id", "")
1281
1294
  session_id_short = session_id[:8].rstrip("-") if session_id else ""
@@ -1307,11 +1320,14 @@ def resolve_receipt(
1307
1320
  metadata_right = " · ".join(metadata_right_parts)
1308
1321
 
1309
1322
  # ── Footer: session stats (left) + brand anchor (right) ──
1323
+ # Same split as hero so nothing in the card claims "corrections" anymore.
1310
1324
  footer_parts = []
1311
- if n_corrections:
1312
- footer_parts.append(f"{n_corrections} corrections")
1325
+ if n_user_turns:
1326
+ footer_parts.append(f"{n_user_turns} user turn{'s' if n_user_turns != 1 else ''}")
1327
+ if n_tool_errors:
1328
+ footer_parts.append(f"{n_tool_errors} tool error{'s' if n_tool_errors != 1 else ''}")
1313
1329
  if n_agents:
1314
- footer_parts.append(f"{n_agents} agents")
1330
+ footer_parts.append(f"{n_agents} agent{'s' if n_agents != 1 else ''}")
1315
1331
  footer_left = " · ".join(footer_parts) if footer_parts else ""
1316
1332
  footer_right = "hyperweave.app"
1317
1333
 
@@ -1325,7 +1341,7 @@ def resolve_receipt(
1325
1341
  "hero_tool_class": hero_tool_class,
1326
1342
  "hero_headline": hero_headline,
1327
1343
  "hero_subline": hero_subline,
1328
- "hero_right_stats": [{"text": t} for t in hero_right],
1344
+ "hero_right_stats": hero_right,
1329
1345
  "treemap_legend": treemap_legend,
1330
1346
  "treemap_cells": treemap_cells,
1331
1347
  "stage_count": len(stages),
@@ -1368,12 +1384,16 @@ def resolve_rhythm_strip(
1368
1384
  tools = list(tools_raw)
1369
1385
 
1370
1386
  # ── Normalize stages ──
1387
+ # start/end preserved so :func:`layout_rhythm_bars` can lay out
1388
+ # time-proportional widths when the contract carries them.
1371
1389
  total_stage_tools = sum(s.get("tools", 1) for s in stages_raw) or 1
1372
1390
  stages: list[dict[str, Any]] = [
1373
1391
  {
1374
1392
  "name": s.get("dominant_class", s.get("label", "explore")),
1375
1393
  "pct": round(s.get("tools", 1) / total_stage_tools * 100),
1376
1394
  "tool_class": s.get("dominant_class", "explore"),
1395
+ "start": s.get("start"),
1396
+ "end": s.get("end"),
1377
1397
  }
1378
1398
  for s in stages_raw
1379
1399
  ]
@@ -1389,38 +1409,10 @@ def resolve_rhythm_strip(
1389
1409
 
1390
1410
  # Rhythm bars — must match template bar_w = sw - stats_w - right_w - 16.
1391
1411
  # With sw=800, stats_w=180, right_w=120: bar_area_w = 484.
1392
- # Budget accounts for 2px gaps between bars so many-stage sessions don't
1393
- # overflow into the right-side loop-status panel.
1412
+ # Delegated to the shared helper see src/hyperweave/compose/rhythm.py.
1394
1413
  bar_area_w = 484
1395
1414
  bar_area_h = 42
1396
- gap_px = 2
1397
- n_stages = len(stages)
1398
- gap_budget = gap_px * max(n_stages - 1, 0)
1399
- available_w = max(bar_area_w - gap_budget, bar_area_w // 2)
1400
- min_bar_w = max(2, available_w // max(n_stages, 1) // 3) if n_stages else 6
1401
- total_stage_pct = sum(s.get("pct", 0) for s in stages) or 100
1402
-
1403
- raw_bars: list[dict[str, Any]] = []
1404
- for s in stages:
1405
- pct = s.get("pct", 0)
1406
- w = max(int(available_w * pct / total_stage_pct), min_bar_w)
1407
- h = max(int(bar_area_h * (pct / 50)), 6)
1408
- y = bar_area_h - h
1409
- tc = s.get("tool_class", "explore")
1410
- raw_bars.append({"w": w, "h": h, "y": y, "tool_class": tc})
1411
-
1412
- # Post-hoc uniform scale if floor-pressure still exceeds budget
1413
- raw_total = sum(b["w"] for b in raw_bars)
1414
- if raw_total > available_w and raw_total > 0:
1415
- scale = available_w / raw_total
1416
- for b in raw_bars:
1417
- b["w"] = max(int(b["w"] * scale), 2)
1418
-
1419
- rhythm_bars: list[dict[str, Any]] = []
1420
- rx = 0
1421
- for b in raw_bars:
1422
- rhythm_bars.append({"x": rx, "y": b["y"], "w": b["w"], "h": b["h"], "tool_class": b["tool_class"]})
1423
- rx += b["w"] + gap_px
1415
+ rhythm_bars = layout_rhythm_bars(stages, area_w=bar_area_w, area_h=bar_area_h)
1424
1416
 
1425
1417
  # Velocity estimate
1426
1418
  vel = int(total_tok / max(duration_m, 1)) if duration_m else 0
@@ -0,0 +1,109 @@
1
+ """Rhythm-bar layout: time-proportional x/w with uniform heights.
2
+
3
+ Shared between :func:`resolve_receipt` and :func:`resolve_rhythm_strip`
4
+ so the receipt's "79-stage bars overflow the track" bug can't diverge
5
+ from rhythm-strip's correct two-pass algorithm.
6
+
7
+ Algorithm (two passes):
8
+ 1. Reserve a gap budget up front so ``n_bars x gap_px`` never steals
9
+ from the width budget. Compute a minimum bar width floor based on
10
+ ``n_bars`` so each bar is still visible under extreme stage counts.
11
+ 2. Raw widths: time-proportional when every stage carries ISO
12
+ ``start``/``end`` timestamps (preferred — the time-axis labels
13
+ then agree with the bar positions); fall back to tool-call-share
14
+ pct when timestamps are missing (legacy contract).
15
+ 3. Post-hoc uniform rescale if the raw sum still exceeds the gap
16
+ budget (happens when the min-bar-width floor dominates).
17
+ 4. Emit ``{x, y, w, h, tool_class}`` with ``h = BAR_HEIGHT`` uniform.
18
+
19
+ Uniform height is intentional. The old resolver's
20
+ ``h = max(int(bar_area_h * (pct / 50)), 8)`` made ~76 of 79 bars hit
21
+ the 8px floor, implying a signal dimension the data didn't carry.
22
+ Three channels (time x category x color) is already enough; the
23
+ fourth (height) had no clear semantic and was therefore noise.
24
+ """
25
+
26
+ from __future__ import annotations
27
+
28
+ from datetime import datetime
29
+ from typing import Any
30
+
31
+ BAR_HEIGHT = 14
32
+ """Uniform rhythm-bar height. Shared by receipt (area_h=92) and rhythm-strip
33
+ (area_h=42): both align the bars flush to the bottom of their track."""
34
+
35
+
36
+ def layout_rhythm_bars(
37
+ stages: list[dict[str, Any]],
38
+ area_w: int,
39
+ area_h: int,
40
+ gap_px: int = 2,
41
+ ) -> list[dict[str, Any]]:
42
+ """Lay out rhythm bars into a bounded track with a gap budget + rescale.
43
+
44
+ Args:
45
+ stages: Normalized stage dicts. Each MAY carry ``start``/``end``
46
+ ISO strings for time-proportional layout; otherwise layout
47
+ falls back to ``pct`` (tool-call share).
48
+ area_w: Track width in pixels.
49
+ area_h: Track height in pixels. Used only for ``y`` (bars align
50
+ bottom-flush); ``h`` is always :data:`BAR_HEIGHT`.
51
+ gap_px: Inter-bar gap in pixels. Reserved upfront in the budget
52
+ so many-stage sessions don't overflow into adjacent panels.
53
+
54
+ Returns:
55
+ One dict per stage: ``{x, y, w, h, tool_class}``. Empty list when
56
+ ``stages`` is empty.
57
+ """
58
+ if not stages:
59
+ return []
60
+
61
+ n = len(stages)
62
+ gap_budget = gap_px * max(n - 1, 0)
63
+ # Cap the worst case: never give up more than half the track to gaps.
64
+ available_w = max(area_w - gap_budget, area_w // 2)
65
+
66
+ has_timestamps = all(s.get("start") and s.get("end") for s in stages)
67
+ if has_timestamps:
68
+ t0 = datetime.fromisoformat(stages[0]["start"])
69
+ t_end = datetime.fromisoformat(stages[-1]["end"])
70
+ total_s = max((t_end - t0).total_seconds(), 1.0)
71
+ raw_w = [
72
+ max(
73
+ int(
74
+ available_w
75
+ * (datetime.fromisoformat(s["end"]) - datetime.fromisoformat(s["start"])).total_seconds()
76
+ / total_s
77
+ ),
78
+ 2,
79
+ )
80
+ for s in stages
81
+ ]
82
+ else:
83
+ total_pct = sum(s.get("pct", 0) for s in stages) or 100
84
+ # Per-bar minimum: 1/3 of the equal-share width so tiny stages
85
+ # stay visible without swallowing the whole budget.
86
+ min_bar_w = max(2, available_w // max(n, 1) // 3)
87
+ raw_w = [max(int(available_w * s.get("pct", 0) / total_pct), min_bar_w) for s in stages]
88
+
89
+ # Post-hoc uniform rescale: if the floor pressure drove the sum over budget.
90
+ raw_total = sum(raw_w)
91
+ if raw_total > available_w and raw_total > 0:
92
+ scale = available_w / raw_total
93
+ raw_w = [max(int(w * scale), 2) for w in raw_w]
94
+
95
+ y = area_h - BAR_HEIGHT
96
+ bars: list[dict[str, Any]] = []
97
+ rx = 0
98
+ for s, w in zip(stages, raw_w, strict=True):
99
+ bars.append(
100
+ {
101
+ "x": rx,
102
+ "y": y,
103
+ "w": w,
104
+ "h": BAR_HEIGHT,
105
+ "tool_class": s.get("tool_class", s.get("dominant_class", "explore")),
106
+ }
107
+ )
108
+ rx += w + gap_px
109
+ return bars
@@ -37,7 +37,11 @@ class HyperWeaveSettings(BaseSettings):
37
37
  data_cache_ttl: int = Field(default=300, description="Data-bound artifact max-age (5 min)")
38
38
 
39
39
  # -- Connectors --
40
- github_tokens: list[str] = Field(default_factory=list, description="GitHub API tokens for rotation")
40
+ # NOTE: GitHub token rotation lives in ``connectors.base._get_github_token``,
41
+ # which reads ``HW_GITHUB_TOKENS`` as a plain comma-separated string directly
42
+ # from ``os.environ``. Do NOT add a ``github_tokens`` Pydantic field here —
43
+ # Pydantic Settings would auto-map the same env var and try to JSON-parse
44
+ # the CSV value, crashing app startup.
41
45
  connect_timeout: float = Field(default=10.0, description="HTTP connect timeout in seconds")
42
46
  total_timeout: float = Field(default=15.0, description="HTTP total timeout in seconds")
43
47
 
@@ -216,3 +216,66 @@ async def fetch_text(
216
216
  merged.update(headers)
217
217
  response = await fetch(url, provider=provider, headers=merged)
218
218
  return response.text
219
+
220
+
221
+ # GraphQL POST
222
+
223
+ _GITHUB_GRAPHQL_URL: str = "https://api.github.com/graphql"
224
+
225
+
226
+ async def fetch_graphql(
227
+ query: str,
228
+ variables: dict[str, Any] | None = None,
229
+ *,
230
+ provider: str = "github",
231
+ url: str = _GITHUB_GRAPHQL_URL,
232
+ ) -> dict[str, Any]:
233
+ """POST a GraphQL query and return the parsed JSON response.
234
+
235
+ Mirrors :func:`fetch_json`'s contract (SSRF validation, per-provider
236
+ circuit breaker, timeouts, GitHub Bearer-token injection) but speaks
237
+ POST with a JSON body. The single GraphQL endpoint for GitHub is
238
+ baked into the default URL so callers don't have to remember it.
239
+
240
+ GraphQL always returns HTTP 200 with a ``data`` field and an optional
241
+ ``errors`` list — HTTP-level failures trip the breaker, but query-level
242
+ errors (invalid field, missing perms) come through as response body
243
+ and are the caller's responsibility to inspect.
244
+ """
245
+ validate_url(url)
246
+
247
+ breaker = get_breaker(provider)
248
+ if not breaker.allow_request():
249
+ raise CircuitOpenError(
250
+ f"Circuit breaker open for provider {provider!r}. Retry after {breaker.recovery_timeout}s."
251
+ )
252
+
253
+ merged_headers: dict[str, str] = {
254
+ "User-Agent": f"HyperWeave/{__version__} (https://hyperweave.app)",
255
+ "Accept": "application/json",
256
+ "Content-Type": "application/json",
257
+ }
258
+ if provider == "github":
259
+ token = _get_github_token()
260
+ if token:
261
+ merged_headers["Authorization"] = f"Bearer {token}"
262
+
263
+ timeout = httpx.Timeout(
264
+ connect=CONNECT_TIMEOUT,
265
+ read=TOTAL_TIMEOUT,
266
+ write=TOTAL_TIMEOUT,
267
+ pool=TOTAL_TIMEOUT,
268
+ )
269
+
270
+ body: dict[str, Any] = {"query": query, "variables": variables or {}}
271
+
272
+ try:
273
+ async with httpx.AsyncClient(timeout=timeout) as client:
274
+ response = await client.post(url, headers=merged_headers, json=body)
275
+ response.raise_for_status()
276
+ breaker.record_success()
277
+ data: dict[str, Any] = response.json()
278
+ return data
279
+ except (httpx.HTTPStatusError, httpx.RequestError) as exc:
280
+ breaker.record_failure()
281
+ raise ConnectorError(f"GraphQL fetch failed for {provider!r}: {exc}") from exc