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.
- {hyperweave-0.2.8 → hyperweave-0.2.9}/CHANGELOG.md +11 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/PKG-INFO +1 -1
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/_version.py +2 -2
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/connectors/github.py +131 -99
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/render/chart_engine.py +86 -26
- {hyperweave-0.2.8 → hyperweave-0.2.9}/tests/test_chart_engine.py +110 -20
- {hyperweave-0.2.8 → hyperweave-0.2.9}/tests/test_connectors.py +155 -69
- {hyperweave-0.2.8 → hyperweave-0.2.9}/.dockerignore +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/.github/workflows/ci.yml +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/.github/workflows/deploy.yml +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/.github/workflows/publish.yml +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/.gitignore +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/.pre-commit-config.yaml +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/Dockerfile +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/LICENSE +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/README.md +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/assets/buttons/button-liquid.svg +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/assets/cards/card-butterfly.svg +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/assets/cards/card-python.svg +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/assets/cards/card-sunflower.svg +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/assets/cards/card-waves.svg +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/assets/examples/brutalist-emerald/badge_critical.svg +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/assets/examples/brutalist-emerald/badge_passing.svg +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/assets/examples/brutalist-emerald/badge_warning.svg +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/assets/examples/brutalist-emerald/banner.svg +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/assets/examples/brutalist-emerald/icons/discord.svg +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/assets/examples/brutalist-emerald/icons/notion.svg +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/assets/examples/brutalist-emerald/icons/reddit.svg +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/assets/examples/brutalist-emerald/icons/spotify.svg +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/assets/examples/brutalist-emerald/icons/youtube.svg +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/assets/examples/brutalist-emerald/marquee_counter.svg +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/assets/examples/brutalist-emerald/marquee_horizontal.svg +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/assets/examples/brutalist-emerald/marquee_vertical.svg +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/assets/examples/brutalist-emerald/profile-cards/chart_stars_full.svg +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/assets/examples/brutalist-emerald/profile-cards/stats.svg +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/assets/examples/brutalist-emerald/strip.svg +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/assets/examples/chrome-horizon/badge_critical.svg +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/assets/examples/chrome-horizon/badge_passing.svg +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/assets/examples/chrome-horizon/badge_warning.svg +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/assets/examples/chrome-horizon/banner.svg +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/assets/examples/chrome-horizon/icons/bluesky.svg +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/assets/examples/chrome-horizon/icons/github.svg +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/assets/examples/chrome-horizon/icons/instagram.svg +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/assets/examples/chrome-horizon/icons/mastodon.svg +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/assets/examples/chrome-horizon/icons/x.svg +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/assets/examples/chrome-horizon/marquee_counter.svg +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/assets/examples/chrome-horizon/marquee_horizontal.svg +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/assets/examples/chrome-horizon/marquee_vertical.svg +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/assets/examples/chrome-horizon/profile-cards/chart_stars_full.svg +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/assets/examples/chrome-horizon/profile-cards/stats.svg +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/assets/examples/chrome-horizon/strip.svg +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/assets/examples/telemetry/master_card.svg +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/assets/examples/telemetry/receipt.svg +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/assets/examples/telemetry/rhythm_strip.svg +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/assets/footers/inneraura-footer-liquid.svg +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/assets/footers/inneraura-footer-purple.svg +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/assets/footers/inneraura-footer.svg +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/assets/hyperweave-banner.svg +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/assets/icons/cobalt-sapphire-discord.svg +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/assets/icons/cobalt-sapphire-docs.svg +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/assets/icons/cobalt-sapphire-github.svg +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/assets/icons/cobalt-sapphire-instagram.svg +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/assets/icons/cobalt-sapphire-linkedin.svg +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/assets/icons/cobalt-sapphire-tiktok.svg +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/assets/icons/cobalt-sapphire-x.svg +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/assets/icons/cobalt-sapphire-youtube.svg +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/assets/marquees/badge-showcase-triple.svg +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/assets/marquees/genome-marquee-triple.svg +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/assets/marquees/sample-badges.svg +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/assets/mintlify-assets/404.svg +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/assets/mintlify-assets/callout-icons.svg +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/assets/mintlify-assets/divider.svg +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/assets/mintlify-assets/favicon.svg +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/assets/mintlify-assets/hero-banner.svg +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/assets/mintlify-assets/hyperweave-banner-v2.svg +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/assets/mintlify-assets/hyperweave-navbar-logo.svg +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/assets/mintlify-assets/loader.svg +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/assets/mintlify-assets/og-image.svg +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/assets/timelines/hyperweave-roadmap.svg +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/docs/assets/favicon.svg +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/docs/assets/hyperweave-banner.svg +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/docs/assets/hyperweave-navbar-logo.svg +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/docs/assets/og-image.svg +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/docs/explanation/architecture.mdx +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/docs/explanation/camo-compatibility.mdx +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/docs/explanation/cim-compliance.mdx +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/docs/explanation/genome-profile-system.mdx +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/docs/explanation/metadata-tiers.mdx +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/docs/how-to/add-motion-to-badges.mdx +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/docs/how-to/create-session-receipts.mdx +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/docs/how-to/set-up-claude-code-hooks.mdx +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/docs/how-to/use-live-data-badges.mdx +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/docs/introduction.mdx +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/docs/mint.json +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/docs/quickstart.mdx +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/docs/reference/cli.mdx +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/docs/reference/compose-spec.mdx +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/docs/reference/genomes.mdx +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/docs/reference/http-api.mdx +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/docs/reference/mcp-tools.mdx +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/docs/reference/motions.mdx +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/docs/reference/telemetry-contract.mdx +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/docs/spec/hyperweave-protocol.mdx +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/docs/tutorials/build-an-artifact-kit.mdx +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/docs/tutorials/compose-your-first-badge.mdx +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/docs/tutorials/install-session-telemetry.mdx +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/fly.toml +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/hooks/install.py +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/hooks/session_end.sh +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/justfile +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/pyproject.toml +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/scripts/extract_font_metrics.py +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/scripts/extract_glyphs.py +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/scripts/generate_hw_compliant.py +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/scripts/generate_proofset.py +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/scripts/stress_test.py +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/__init__.py +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/__main__.py +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/cli.py +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/compose/__init__.py +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/compose/assembler.py +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/compose/context.py +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/compose/engine.py +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/compose/lanes.py +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/compose/resolver.py +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/compose/resolvers/__init__.py +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/compose/resolvers/chart.py +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/compose/resolvers/stats.py +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/compose/resolvers/timeline.py +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/compose/rhythm.py +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/compose/validate_paradigms.py +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/config/__init__.py +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/config/genome_validator.py +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/config/loader.py +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/config/registry.py +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/config/settings.py +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/connectors/__init__.py +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/connectors/arxiv.py +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/connectors/base.py +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/connectors/cache.py +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/connectors/rest.py +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/core/__init__.py +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/core/color.py +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/core/contracts.py +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/core/enums.py +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/core/font_metrics.py +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/core/models.py +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/core/paradigm.py +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/core/schema.py +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/core/state.py +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/core/text.py +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/core/thresholds.py +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/data/css/accessibility.css +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/data/css/bridge.css +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/data/css/expression.css +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/data/css/phase-colors.css +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/data/css/status.css +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/data/css/telemetry.css +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/data/font-metrics/inter.json +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/data/font-metrics/jetbrains-mono.json +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/data/font-metrics/orbitron.json +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/data/fonts/jetbrains-mono.b64 +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/data/fonts/jetbrains-mono.meta.json +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/data/fonts/orbitron.b64 +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/data/fonts/orbitron.meta.json +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/data/genomes/brutalist-emerald.json +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/data/genomes/chrome-horizon.json +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/data/genomes/telemetry-void.json +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/data/glyphs.json +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/data/motions/border/chromatic-pulse.yaml +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/data/motions/border/corner-trace.yaml +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/data/motions/border/dual-orbit.yaml +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/data/motions/border/entanglement.yaml +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/data/motions/border/rimrun.yaml +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/data/motions/kinetic/bars.yaml +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/data/motions/kinetic/breach.yaml +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/data/motions/kinetic/broadcast.yaml +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/data/motions/kinetic/cascade.yaml +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/data/motions/kinetic/collapse.yaml +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/data/motions/kinetic/converge.yaml +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/data/motions/kinetic/crash.yaml +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/data/motions/kinetic/drop.yaml +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/data/motions/kinetic/pulse.yaml +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/data/motions/static.yaml +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/data/paradigms/brutalist.yaml +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/data/paradigms/chrome.yaml +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/data/paradigms/default.yaml +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/data/policies/normal.json +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/data/policies/permissive.json +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/data/policies/ungoverned.json +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/data/profiles/brutalist.contract.json +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/data/profiles/brutalist.yaml +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/data/profiles/chrome.contract.json +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/data/profiles/chrome.yaml +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/data/specimens.yaml +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/data/telemetry/model-pricing.yaml +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/data/telemetry/stage-config.yaml +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/data/telemetry/stage-labels.yaml +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/data/telemetry/tool-classes.yaml +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/data/telemetry/tool-colors.yaml +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/data/terminals/arrow.json +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/data/terminals/aurora.json +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/data/terminals/beacon.json +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/data/terminals/bracket3d.json +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/data/terminals/chevron.json +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/data/terminals/crosshair.json +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/data/terminals/diamond.json +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/data/terminals/rocket.json +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/data/terminals/rules/bar.json +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/data/terminals/rules/dashed.json +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/data/terminals/rules/five-wave.json +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/data/terminals/rules/spectral.json +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/data/terminals/rules/straight.json +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/data/terminals/rules/wave.json +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/data/terminals/stijl.json +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/kit.py +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/mcp/__init__.py +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/mcp/__main__.py +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/mcp/server.py +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/py.typed +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/render/__init__.py +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/render/fonts.py +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/render/glyphs.py +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/render/motion.py +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/render/templates.py +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/serve/__init__.py +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/serve/app.py +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/telemetry/__init__.py +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/telemetry/capture.py +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/telemetry/contract.py +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/telemetry/corrections.py +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/telemetry/cost.py +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/telemetry/models.py +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/telemetry/parser.py +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/telemetry/stages.py +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/components/chart-area.svg.j2 +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/components/chart-axes.svg.j2 +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/components/chart-empty-state.svg.j2 +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/components/chart-gridlines.svg.j2 +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/components/chart-markers/circle.svg.j2 +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/components/chart-markers/diamond.svg.j2 +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/components/chart-markers/endpoint-diamond.svg.j2 +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/components/chart-markers/endpoint-rect.svg.j2 +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/components/chart-markers/rect.svg.j2 +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/components/chart-milestone.svg.j2 +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/components/chart-polyline.svg.j2 +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/components/filter.svg.j2 +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/components/glyph-inline.svg.j2 +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/components/glyph.svg.j2 +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/components/gradient.svg.j2 +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/components/metadata.svg.j2 +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/components/metric.svg.j2 +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/components/phase-segment.svg.j2 +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/components/rule.svg.j2 +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/components/status.svg.j2 +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/components/terminal.svg.j2 +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/components/treemap.svg.j2 +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/document.svg.j2 +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/error-badge.svg.j2 +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/frames/badge/brutalist-content.j2 +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/frames/badge/brutalist-defs.j2 +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/frames/badge/chrome-content.j2 +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/frames/badge/chrome-defs.j2 +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/frames/badge/default-content.j2 +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/frames/badge/default-defs.j2 +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/frames/badge.svg.j2 +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/frames/banner/brutalist-defs.j2 +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/frames/banner/chrome-content.j2 +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/frames/banner/chrome-defs.j2 +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/frames/banner/default-content.j2 +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/frames/banner/default-defs.j2 +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/frames/banner.svg.j2 +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/frames/catalog.svg.j2 +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/frames/chart/brutalist-content.j2 +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/frames/chart/brutalist-defs.j2 +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/frames/chart/chrome-content.j2 +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/frames/chart/chrome-defs.j2 +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/frames/chart.svg.j2 +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/frames/divider.svg.j2 +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/frames/icon/brutalist-content.j2 +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/frames/icon/brutalist-defs.j2 +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/frames/icon/chrome-content.j2 +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/frames/icon/chrome-defs.j2 +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/frames/icon/default-content.j2 +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/frames/icon/default-defs.j2 +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/frames/icon.svg.j2 +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/frames/marquee-counter/brutalist-content.j2 +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/frames/marquee-counter/brutalist-defs.j2 +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/frames/marquee-counter/chrome-content.j2 +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/frames/marquee-counter/chrome-defs.j2 +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/frames/marquee-counter.svg.j2 +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/frames/marquee-horizontal/brutalist-content.j2 +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/frames/marquee-horizontal/brutalist-defs.j2 +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/frames/marquee-horizontal/chrome-content.j2 +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/frames/marquee-horizontal/chrome-defs.j2 +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/frames/marquee-horizontal.svg.j2 +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/frames/marquee-vertical/brutalist-content.j2 +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/frames/marquee-vertical/brutalist-defs.j2 +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/frames/marquee-vertical/chrome-content.j2 +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/frames/marquee-vertical/chrome-defs.j2 +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/frames/marquee-vertical.svg.j2 +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/frames/master-card.svg.j2 +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/frames/receipt.svg.j2 +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/frames/rhythm-strip.svg.j2 +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/frames/stats/brutalist-content.j2 +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/frames/stats/brutalist-defs.j2 +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/frames/stats/chrome-content.j2 +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/frames/stats/chrome-defs.j2 +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/frames/stats.svg.j2 +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/frames/strip/brutalist-content.j2 +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/frames/strip/brutalist-defs.j2 +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/frames/strip/brutalist-status.j2 +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/frames/strip/chrome-content.j2 +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/frames/strip/chrome-defs.j2 +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/frames/strip/chrome-status.j2 +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/frames/strip/default-content.j2 +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/frames/strip/default-defs.j2 +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/frames/strip/default-status.j2 +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/frames/strip.svg.j2 +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/frames/timeline/default-content.j2 +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/frames/timeline.svg.j2 +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/motions/border/chromatic-pulse.svg.j2 +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/motions/border/corner-trace.svg.j2 +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/motions/border/dual-orbit.svg.j2 +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/motions/border/entanglement.svg.j2 +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/motions/border/rimrun.svg.j2 +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/motions/kinetic/bars.svg.j2 +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/motions/kinetic/breach.svg.j2 +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/motions/kinetic/broadcast.svg.j2 +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/motions/kinetic/cascade.svg.j2 +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/motions/kinetic/collapse.svg.j2 +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/motions/kinetic/converge.svg.j2 +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/motions/kinetic/crash.svg.j2 +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/motions/kinetic/drop.svg.j2 +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/src/hyperweave/templates/motions/kinetic/pulse.svg.j2 +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/tests/__init__.py +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/tests/conftest.py +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/tests/fixtures/github_contributions/synthetic.html +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/tests/fixtures/session.json +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/tests/fixtures/session.jsonl +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/tests/helpers.py +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/tests/test_assembler.py +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/tests/test_chart_frame.py +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/tests/test_core.py +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/tests/test_github_scrape.py +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/tests/test_kit.py +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/tests/test_mcp.py +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/tests/test_paradigm_dispatch.py +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/tests/test_paradigm_extensibility.py +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/tests/test_proofset.py +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/tests/test_render.py +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/tests/test_resolver_rhythm.py +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/tests/test_serve.py +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/tests/test_serve_live_state.py +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/tests/test_state_inference.py +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/tests/test_stats_card.py +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/tests/test_telemetry.py +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/tests/test_telemetry_integration.py +0 -0
- {hyperweave-0.2.8 → hyperweave-0.2.9}/tests/test_timeline.py +0 -0
- {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.
|
|
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.
|
|
22
|
-
__version_tuple__ = version_tuple = (0, 2,
|
|
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-
|
|
155
|
-
#
|
|
156
|
-
#
|
|
157
|
-
#
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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(
|
|
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).
|
|
203
|
-
#
|
|
204
|
-
#
|
|
205
|
-
#
|
|
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
|
-
|
|
248
|
+
sample_count: int = _DEFAULT_SAMPLE_COUNT,
|
|
225
249
|
) -> dict[str, Any]:
|
|
226
|
-
"""
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
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
|
-
|
|
284
|
-
|
|
285
|
-
|
|
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
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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
|
-
|
|
517
|
-
|
|
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
|
-
|
|
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
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
|
461
|
-
"""Data
|
|
460
|
+
def test_x_date_labels_monthly_granularity_for_year_one_span(sample_viewport: Viewport) -> None:
|
|
461
|
+
"""Data spanning ~10 months → monthly 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 =
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
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
|
|
480
|
-
"""
|
|
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 =
|
|
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
|
-
|
|
491
|
-
assert
|
|
492
|
-
|
|
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[
|
|
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
|
|
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
|
|
590
|
+
assert _build_x_date_labels([], vp) == []
|
|
501
591
|
|
|
502
592
|
|
|
503
593
|
# ── Zero-time-span defense (bug 2 reproducer) ────────────────────────
|