diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index ee01f8ed..94fd8806 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -160,9 +160,6 @@ jobs:
- name: Public API coherence
run: .venv/bin/python scripts/check_public_api.py
- - name: Claim guardrails
- run: .venv/bin/python scripts/check_claim_guardrails.py
-
- name: Lint (ruff)
run: |
.venv/bin/ruff check .
diff --git a/CLAUDE.md b/CLAUDE.md
index f3baeb12..a19fc500 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -11,6 +11,32 @@ release change. A change is incomplete while its affected specification is
missing, stale, or inconsistent with the implementation; resolve discrepancies
instead of treating the implementation alone as authoritative.
+## Product North Star
+
+XY is being built to outperform every competing charting library and become
+the best overall charting system for Python. That goal spans every chart type
+and every data scale, from a handful of values to billions of rows, across the
+two dimensions users should not have to trade off: performance and
+customization.
+
+Treat every competitor lead as a concrete product gap. Work that affects a
+user-visible capability should:
+
+- compare XY with the relevant leaders, including Matplotlib, Seaborn, Plotly,
+ Bokeh, Altair, Datashader, HoloViews/hvPlot, and emerging alternatives;
+- add or extend reproducible evidence across small, medium, large, and massive
+ data, covering startup, build and render time, interaction, memory, payload
+ and export size, and multi-chart applications where applicable;
+- update the capability matrix and visual examples when the improvement is
+ about chart breadth or customization rather than timing; and
+- commit the environment, raw results, output contracts, and reproduction
+ commands needed to inspect the win and catch regressions.
+
+The goal is not to win one large-scatter benchmark. XY should become the
+library users choose for ordinary charts, massive data, every chart family,
+notebooks, applications, static output, performance, and complete design
+control.
+
## Layout
- `src/` — Rust core, **minimal external crates** (C ABI; one cdylib per
@@ -107,5 +133,3 @@ PRs, or code. Set `git config user.name/user.email` to the human author
cache (§27). NaN never reaches vertex buffers (§19).
- f32 uploads are offset-encoded; tick/hover math stays f64 (§4/§16).
- Every decimation/tier decision is recorded in the spec, never silent (§28).
-- Claims are mode-scoped and benchmarked (§2); update README numbers from
- `scripts/bench.py`, don't invent them.
diff --git a/Makefile b/Makefile
index 53167a6f..f5aedcde 100644
--- a/Makefile
+++ b/Makefile
@@ -7,7 +7,7 @@ WHEEL ?=
BENCHMARK_JSON ?= benchmark.json
BENCHMARK_KIND ?= auto
-.PHONY: help setup setup-browser check check-full check-browser check-conformance check-docs check-examples check-security check-errors check-api check-import check-ci check-claims check-benchmark-harness check-pyplot check-pyplot-speed check-sdist check-wheel check-artifacts check-benchmark-report list-checks test lint format typecheck public-api python-floor js-check rust-check abi-smoke
+.PHONY: help setup setup-browser check check-full check-browser check-conformance check-docs check-examples check-security check-errors check-api check-import check-ci check-benchmark-harness check-pyplot check-pyplot-speed check-sdist check-wheel check-artifacts check-benchmark-report list-checks test lint format typecheck public-api python-floor js-check rust-check abi-smoke
help:
@printf '%s\n' \
@@ -19,14 +19,13 @@ help:
' make check-full run JS, Rust, and ABI gates too' \
' make check-browser run browser smokes (set CHROMIUM=/path/to/chrome)' \
' make check-conformance run accessibility + Chromium/Firefox/WebKit conformance' \
- ' make check-docs run docs examples and public claim guardrails' \
+ ' make check-docs run documentation examples' \
' make check-examples run canonical API examples and Reflex asset registry checks' \
' make check-security run standalone HTML safety and client text-sink checks' \
' make check-errors run public error, LOD, and mutation-safety tests' \
' make check-api run lazy public API and type-surface checks' \
' make check-import run import-time and dependency-boundary checks' \
' make check-ci run CI/release workflow invariant checks' \
- ' make check-claims run public performance-claim guardrails' \
' make check-benchmark-harness run benchmark metadata/report/regression tests' \
' make check-pyplot run the matplotlib-shim suite and compatibility corpus' \
' make check-pyplot-speed enforce the per-family 10x static-PNG target (requires .[bench])' \
@@ -76,7 +75,7 @@ check-conformance:
node scripts/browser_conformance.mjs
check-docs:
- $(PYTHON) scripts/verify_local.py --only examples,claim_guardrails
+ $(PYTHON) scripts/verify_local.py --only examples
check-examples:
$(PYTHON) scripts/verify_local.py --only examples
@@ -99,9 +98,6 @@ check-import:
check-ci:
$(PYTHON) scripts/verify_local.py --only ci_workflow
-check-claims:
- $(PYTHON) scripts/verify_local.py --only claim_guardrails
-
check-benchmark-harness:
$(PYTHON) scripts/verify_local.py --only benchmark_harness
diff --git a/README.md b/README.md
index 2bfcd053..38ad7434 100644
--- a/README.md
+++ b/README.md
@@ -2,10 +2,6 @@
-
- Try it live: a million points in your browser →
-
-
@@ -13,25 +9,36 @@
-XY is an actively evolving, early-alpha Python charting library for large,
-interactive datasets. Its Rust core and WebGL2 renderer keep work bounded by
-what the screen can show; find guides, API reference, and examples in the
-[documentation](https://reflex.dev/docs/xy/).
+
+ Small data should be effortless. Large data should stay interactive.
+ Neither should limit the design.
+
+
+XY is an actively evolving, early-alpha project with one ambition: become the
+fastest and most customizable charting library for Python — across every chart
+type and every scale, from a handful of values to billions of rows.
+
+Small datasets render directly and exactly. As data grows, XY's Rust core,
+binary transport, and WebGL2 renderer reduce work to what the screen can show,
+then bring detail back as you zoom. The same composable API controls marks,
+axes, annotations, legends, tooltips, interactions, themes, responsive layout,
+and HTML, raster, and vector exports.
## Highlights
-- **Built for large data.** Reduces long lines and dense scatters to what the screen can show, and brings detail back as you zoom.
+- **Designed for every scale.** Ordinary datasets take the direct path; large datasets automatically use decimated or density representations.
+- **Customize every layer.** Control marks and composition in Python, then style chart chrome through stable CSS and Tailwind hooks.
- **Declarative interface.** Compose marks and guides, or use the familiar `xy.pyplot`.
- **Interactive by default.** Pan, zoom, hover, select, and inspect exact source rows.
- **One chart, many outputs.** Use notebooks or export HTML, raster, and vector formats.
-- **Built for apps.** Embed responsive charts and style them with CSS or Tailwind.
+- **Built for apps.** Embed responsive charts and style them with Python, CSS, or Tailwind.
## Is XY for me?
-XY is a great fit for teams that want to explore large 2D datasets in Python,
-share interactive notebook results, or ship self-contained charts on the web.
-Build charts once, then display them in notebooks and apps or export them as
-HTML, images, and vector graphics.
+XY is for Python users who want one charting system for everyday plots, deeply
+custom application visuals, and data too large for conventional browser chart
+stacks. Build charts once, then display them in notebooks and apps or export
+them as self-contained HTML, images, and vector graphics.
## Installation
@@ -42,7 +49,7 @@ pip install xy
uv add xy
```
-## Getting started
+## Getting started: one API, every scale
A chart is a container plus the marks inside it. Any sequence works — plain
Python lists need no NumPy:
@@ -57,7 +64,8 @@ chart = xy.line_chart(xy.line([1, 2, 3, 4, 5], [120, 180, 165, 240, 310]))
chart # notebooks render it
```
-The same API scales. Chart a hundred million points as a density surface:
+The data size changes; the chart model does not. The same API scales to a
+hundred million points as a density surface:
@@ -117,25 +125,20 @@ plt.show()
The shim intentionally covers common plotting workflows rather than every
matplotlib feature. See the [compatibility guide](spec/matplotlib/compat.md).
-## Benchmarks
-
-
-
-
-
-In the recorded 10-million-point baseline, XY produced a static PNG in 0.023 s
-versus 2.8 s for Matplotlib and 9.6 s for Plotly, and reached first interactive
-render 16–20× sooner.
+## Customize every layer
-The committed launch baseline uses identical seeded data, a 900×420 output,
-and three isolated cold runs. See the
-[launch report](benchmarks/launch_baselines/xy-0.1.0/macos-arm64-m5-pro/report.md)
-and [benchmark runbook](benchmarks/README.md) for the environment,
-methodology, and raw results.
+Performance is not a preset. Use Python to control data marks and chart
+composition, then style browser chrome through stable CSS and Tailwind hooks.
-## Styling
+- **Marks:** data-driven color and size, custom colormaps, opacity, symbols,
+ gradients, strokes, and curves.
+- **Guides:** axes, ticks, grids, annotations, legends, colorbars, and tooltips.
+- **Interaction:** pan, zoom, hover, selection, crosshairs, callbacks, and
+ linked charts.
+- **Layout and output:** layering, facets, responsive dimensions, themes, and
+ HTML, raster, or vector export.
-Customize marks and chart chrome with Python, CSS, or Tailwind. See the [styling guide](docs/styling/index.md).
+See the [styling guide](docs/styling/index.md) for patterns and examples.
What each mechanism reaches — per property, per chrome slot, per renderer, and
where it stops — is the [capability matrix](spec/api/capability-matrix.md),
@@ -150,6 +153,30 @@ chart = xy.line_chart(
)
```
+## Benchmarks
+
+
+
+
+
+In the recorded 10-million-point baseline, XY produced a static PNG in 0.023 s
+versus 2.8 s for Matplotlib and 9.6 s for Plotly, and reached first interactive
+render 16–20× sooner.
+
+That baseline is one proof point, not the finish line. XY is being built to
+outperform every competing charting library across small-data startup,
+massive-data rendering, interaction, memory, payload and export size,
+multi-chart applications, chart breadth, and customization. The committed
+benchmark program, raw results, capability matrix, and visual examples are how
+the project measures and proves that goal.
+
+The committed launch baseline uses identical seeded data, a 900×420 output,
+and three isolated cold runs. See the
+[launch report](benchmarks/launch_baselines/xy-0.1.0/macos-arm64-m5-pro/report.md),
+[benchmark runbook](benchmarks/README.md), and
+[competitive benchmark specification](spec/benchmarks/results.md) for the
+environment, methodology, raw results, and full evidence program.
+
## Embed XY in a Reflex app
With the `reflex-xy` adapter, any XY chart becomes a regular Reflex component.
@@ -266,8 +293,10 @@ the chart gallery, guides, and API reference. The repository also includes
## Roadmap
-XY is 2D-first: broad chart coverage on top of the binary transport and
-screen-bounded rendering, before any 3D work. Queued next, no dates implied:
+XY is headed toward every chart family. The current implementation sequence
+builds broad 2D coverage on top of the binary transport and screen-bounded
+renderer, then extends the same performance and customization goals to
+geographic, 3D, and volume visualization. Queued next, no dates implied:
- **Categorical distributions** — strip, swarm, beeswarm, boxen, rug
- **Regression diagnostics** — trendline, residual, QQ, PP
@@ -278,6 +307,7 @@ screen-bounded rendering, before any 3D work. Queued next, no dates implied:
- **Treemap, sunburst, and icicle**
- **Radar / polar and gauge** — needs polar axes first
- **Slope, bump, and dumbbell**
+- **3D and volume** — scatter, surfaces, meshes, isosurfaces, and volumetric views
The full ranked backlog is in the [chart roadmap](spec/api/chart-roadmap.md).
Want a chart or feature that isn't listed?
diff --git a/docs/advanced/index.md b/docs/advanced/index.md
index 03402dae..ed897713 100644
--- a/docs/advanced/index.md
+++ b/docs/advanced/index.md
@@ -103,8 +103,7 @@ does not automatically become a browser object:
- Retained GPU buffers and local interaction avoid rebuilding the chart on
every pointer movement.
-This is a focused design, not a claim that every chart is faster. Compare the
-same data, output, and rendering mode. The
+This focused design is the foundation for making every chart faster. The
[benchmark snapshot](/docs/xy/overview/benchmarks/) publishes those contracts
and records when XY uses a reduced representation.
diff --git a/docs/api-reference/contributing.md b/docs/api-reference/contributing.md
index 63d3a743..67a1f9ab 100644
--- a/docs/api-reference/contributing.md
+++ b/docs/api-reference/contributing.md
@@ -38,17 +38,13 @@ import error rather than silently selecting another backend.
| --- | --- |
| Fast local verification | `make check` |
| Production-facing change | `make check-full` |
-| Public docs, examples, or claims | `make check-docs` |
+| Public docs and examples | `make check-docs` |
| Public exports or annotations | `make check-api` |
| Lazy imports and dependency boundaries | `make check-import` |
| Validation and mutation behavior | `make check-errors` |
| Standalone HTML and text safety | `make check-security` |
| Browser lifecycle and interaction | `make check-browser CHROMIUM=/path/to/chrome` |
-Run `make check-claims` before publishing performance prose. Every comparison
-must name the chart type, data size and shape, representation mode, backend,
-render target, and whether browser time-to-first-render is included.
-
## Adding a Chart Type
A complete chart-family contribution normally spans:
diff --git a/docs/core-concepts/large-data-and-performance.md b/docs/core-concepts/large-data-and-performance.md
index 119ca434..7ebb2fdb 100644
--- a/docs/core-concepts/large-data-and-performance.md
+++ b/docs/core-concepts/large-data-and-performance.md
@@ -78,9 +78,8 @@ There are two different scaling regimes:
2. The resulting wire payload, WebGL geometry, and static SVG/native-PNG scene
are bounded by the chosen viewport representation.
-This is why a “cost scales with pixels, not points” slogan needs qualification:
-it describes the output side after reduction, not the complete data-to-pixels
-pipeline.
+The output side after reduction scales with pixels; the complete data-to-pixels
+pipeline still includes row-dependent ingest and reduction work.
## Inspect memory and tier decisions
diff --git a/docs/overview/benchmarks.md b/docs/overview/benchmarks.md
index 803bffaa..08538a1e 100644
--- a/docs/overview/benchmarks.md
+++ b/docs/overview/benchmarks.md
@@ -5,18 +5,18 @@ description: Inspect XY's recorded launch benchmark with its exact output contra
# Benchmarks
-XY's large-data claim is about the representation sent to a fixed-size output,
-not about drawing every source row as an individual marker. The committed 0.1.0
-launch baseline measures identical seeded scatter data at 900×420 pixels on an
-Apple M5 Pro with 64 GiB RAM. Each successful cell below is the mean of three
-isolated cold runs. The machine name and memory are copied verbatim from the
-committed environment record.
+XY's large-data architecture reduces source rows to the representation useful
+for a fixed-size output instead of drawing every row as an individual marker.
+The committed 0.1.0 launch baseline measures identical seeded scatter data at
+900×420 pixels on an Apple M5 Pro with 64 GiB RAM. Each successful cell below
+is the mean of three isolated cold runs. The machine name and memory are copied
+verbatim from the committed environment record.
> **How to read this comparison.** XY switches dense scatter output to a
> screen-bounded density representation, while the default Plotly and
> Matplotlib paths retain every marker. These results compare each library's
-> default user-visible outcome at the same output size; they do not claim that
-> the libraries send identical geometry to the renderer.
+> default user-visible outcome at the same output size; the libraries send
+> different geometry to the renderer.
## Snapshot at 10 million points
@@ -121,8 +121,9 @@ frozen:
- **Release and hardware tracking:** immutable release directories plus
clearly separated macOS hardware-WebGL and CI SwiftShader results.
-Until those artifacts are published, this page intentionally keeps its
-headline claims scoped to the committed launch scatter.
+The launch scatter is the first committed proof point. The competitive
+benchmark program expands it across these workloads, chart families,
+competitors, and environments.
## Inspect and reproduce the evidence
@@ -148,9 +149,7 @@ uv run --project "$BASELINE" --frozen python benchmarks/bench_launch_scatter.py
--chrome "$CHROME" --out launch-scatter-default.json
```
-One machine and three runs describe that recorded environment, not every
-machine or workload. New comparisons should retain chart type, data size,
-representation, backend, output target, and browser-TTFR status rather than
-shortening these results to a universal “faster than” claim. For the rendering
-model behind the numbers, read
+Each new comparison records its chart type, data size, representation, backend,
+output target, and browser-TTFR status so the result can be reproduced and
+improved. For the rendering model behind the numbers, read
[Large data and performance](/docs/xy/core-concepts/large-data-and-performance/).
diff --git a/scripts/check_claim_guardrails.py b/scripts/check_claim_guardrails.py
deleted file mode 100644
index f7d4f4d2..00000000
--- a/scripts/check_claim_guardrails.py
+++ /dev/null
@@ -1,243 +0,0 @@
-#!/usr/bin/env python3
-"""Check public docs for broad, unqualified performance claims.
-
-This is intentionally a small guardrail, not a full natural-language judge. It
-flags phrases that are easy to post publicly and hard to defend, while allowing
-measured benchmark rows and policy text that names mode/data/backend context.
-"""
-
-from __future__ import annotations
-
-import argparse
-import re
-import sys
-from dataclasses import dataclass
-from pathlib import Path
-from typing import Optional
-
-ROOT = Path(__file__).resolve().parents[1]
-DEFAULT_DOCS = (
- "README.md",
- "CLAUDE.md",
- "pyproject.toml",
- "SECURITY.md",
- "CONTRIBUTING.md",
- "spec/api/api-examples.md",
- "spec/benchmarks/results.md",
- "spec/api/chart-roadmap.md",
- "spec/process/contributing.md",
- "spec/process/production-readiness.md",
-)
-
-BROAD_SUPERLATIVE_RE = re.compile(
- r"\b("
- r"faster\s+charting\s+engine|"
- r"fastest(?:\s+charting\s+library)?|"
- r"best\s+(?:python\s+)?(?:charting|plotting|graphing|data)\s+(?:library|engine)|"
- r"best\s+(?:at|for)\s+rendering|"
- r"most\s+performant(?:\s+(?:python\s+)?(?:charting|plotting|graphing|data)\s+"
- r"(?:library|engine))?|"
- r"better\s+than\s+(?:all|every|any)|"
- r"faster\s+than\s+(?:all|every|any)|"
- r"faster\s+than\s+everything|"
- r"more\s+performant\s+than\s+(?:all|every|any|everything)|"
- r"blow\s+(?:them|it|everyone)\s+away"
- r")\b",
- re.IGNORECASE,
-)
-# Customization is the second axis people overclaim on, and it went unguarded
-# while performance was fenced in. These shapes are unqualifiable: the styling
-# surface is a bounded, enumerated subset (`spec/api/capability-matrix.md`), so
-# "anything" and "most" are wrong however the sentence is framed.
-CUSTOMIZATION_SUPERLATIVE_RE = re.compile(
- r"\b("
- r"most\s+(?:customi[sz]able|themeable|styl(?:e)?able|extensible|flexible)|"
- r"(?:fully|completely|infinitely|endlessly|totally)\s+customi[sz]able|"
- r"customi[sz]e\s+(?:everything|anything)|"
- r"style\s+(?:everything|anything)|"
- r"unlimited\s+(?:styling|customi[sz]ation)|"
- r"(?:more|as)\s+(?:customi[sz]able|themeable|extensible)\s+than\s+"
- r"(?:all|every|any|everything|anything)"
- r")\b",
- re.IGNORECASE,
-)
-COMPARATIVE_RE = re.compile(
- r"\b(?:faster\s+than|beats?|outperforms?)\s+"
- r"(?:plotly|matplotlib|bokeh|altair|datashader|holoviews|hvplot|seaborn)\b",
- re.IGNORECASE,
-)
-LARGE_RENDER_RE = re.compile(
- r"\brenders?\s+(?:\d[\d,]*(?:\.\d+)?\s*)?(?:k|m|b|million|billion)?[- ]?points\b",
- re.IGNORECASE,
-)
-NUMERIC_PERFORMANCE_RE = re.compile(
- r"\b\d+(?:\.\d+)?\s*x\s+(?:faster|slower|smaller|larger|less|more)\b",
- re.IGNORECASE,
-)
-# The project has lived under earlier names (charts-exp, personal forks).
-# Public docs must point at the canonical repository — a stale URL here means
-# security reports, clones, and badges land on the wrong repo.
-STALE_REPO_RE = re.compile(r"github\.com/Alek99|app\.codspeed\.io/Alek99|charts-exp", re.IGNORECASE)
-
-POLICY_WORDS = re.compile(
- r"\b(do not|don't|never|no amount of|must|should|guardrail|policy|claim|goal|planned|target|"
- r"blurry|rather than|not\s+(?:a|the|safe|same|one)|without naming|"
- r"needs qualification)\b",
- re.IGNORECASE,
-)
-QUALIFIER_GROUPS = (
- re.compile(r"\b(?:\d[\d,]*(?:\.\d+)?\s*)?(?:k|m|b|million|billion|points|rows|n=)\b", re.I),
- re.compile(
- r"\b(?:direct|density|decimated|adaptive|sampled|screen-bounded|exact markers?)\b", re.I
- ),
- re.compile(
- r"\b(?:native|numpy|backend|browser|chromium|swiftshader|render target|"
- r"webgl|svg|png|to pixels)\b",
- re.I,
- ),
- re.compile(
- r"\b(?:benchmark|measured|documented|ttfr|payload|memory|ms|mb|gb|artifact)\b", re.I
- ),
- re.compile(r"\b(?:chart type|data size|mode|row|table|spec/benchmarks/results\.md)\b", re.I),
-)
-
-
-@dataclass(frozen=True)
-class Finding:
- path: Path
- line: int
- message: str
- text: str
-
- def format(self) -> str:
- rel = self.path.relative_to(ROOT) if self.path.is_relative_to(ROOT) else self.path
- return f"{rel}:{self.line}: {self.message}: {self.text.strip()}"
-
-
-def _line_window(lines: list[str], index: int, radius: int = 2) -> str:
- start = max(0, index - radius)
- end = min(len(lines), index + radius + 1)
- return "\n".join(lines[start:end])
-
-
-def _is_policy_or_negative_context(text: str) -> bool:
- return bool(POLICY_WORDS.search(text))
-
-
-def _has_claim_qualifiers(text: str) -> bool:
- return sum(1 for pattern in QUALIFIER_GROUPS if pattern.search(text)) >= 3
-
-
-def _findings_for_file(path: Path) -> list[Finding]:
- lines = path.read_text(encoding="utf-8").splitlines()
- findings: list[Finding] = []
- for index, line in enumerate(lines):
- window = _line_window(lines, index)
- if STALE_REPO_RE.search(line):
- # No policy-context exemption: a stale repo identity is always wrong.
- findings.append(
- Finding(
- path,
- index + 1,
- "stale repository identity; use github.com/reflex-dev/xy",
- line,
- )
- )
- if BROAD_SUPERLATIVE_RE.search(line) and not _is_policy_or_negative_context(window):
- findings.append(
- Finding(
- path,
- index + 1,
- "broad superlative needs measured scope or policy framing",
- line,
- )
- )
- if CUSTOMIZATION_SUPERLATIVE_RE.search(line) and not _is_policy_or_negative_context(window):
- findings.append(
- Finding(
- path,
- index + 1,
- "customization superlative is not defensible from the capability matrix",
- line,
- )
- )
- if COMPARATIVE_RE.search(line) and not (
- _is_policy_or_negative_context(window) or _has_claim_qualifiers(window)
- ):
- findings.append(
- Finding(
- path,
- index + 1,
- "comparative performance claim needs chart/data/backend/render context",
- line,
- )
- )
- if LARGE_RENDER_RE.search(line) and not (
- _is_policy_or_negative_context(window) or _has_claim_qualifiers(window)
- ):
- findings.append(
- Finding(
- path,
- index + 1,
- "large-point rendering claim needs mode context",
- line,
- )
- )
- if NUMERIC_PERFORMANCE_RE.search(line):
- # Benchmark tables often carry the measurement context a few lines
- # above the rows; give this rule a wider view than sentence-level
- # comparative claims while still catching slogan-style multipliers.
- metric_window = _line_window(lines, index, radius=10)
- if not (
- _is_policy_or_negative_context(metric_window)
- or _has_claim_qualifiers(metric_window)
- ):
- findings.append(
- Finding(
- path,
- index + 1,
- "numeric performance multiplier needs measured benchmark context",
- line,
- )
- )
- return findings
-
-
-def check_claims(paths: list[Path]) -> list[Finding]:
- findings: list[Finding] = []
- for path in paths:
- if not path.exists():
- findings.append(Finding(path, 0, "document does not exist", ""))
- continue
- findings.extend(_findings_for_file(path))
- return findings
-
-
-def _default_paths() -> list[Path]:
- paths = [ROOT / item for item in DEFAULT_DOCS]
- public_docs = (
- path
- for path in sorted((ROOT / "docs").rglob("*.md"))
- if "app" not in path.relative_to(ROOT / "docs").parts
- )
- return list(dict.fromkeys((*paths, *public_docs)))
-
-
-def main(argv: Optional[list[str]] = None) -> int:
- parser = argparse.ArgumentParser(description=__doc__)
- parser.add_argument("paths", nargs="*", type=Path, help="docs to scan")
- args = parser.parse_args(argv)
-
- paths = args.paths or _default_paths()
- findings = check_claims([path if path.is_absolute() else ROOT / path for path in paths])
- if findings:
- print("performance claim guardrail failed:", file=sys.stderr)
- for finding in findings:
- print(f"- {finding.format()}", file=sys.stderr)
- return 1
- print(f"performance claim guardrail OK: {len(paths)} document(s)")
- return 0
-
-
-if __name__ == "__main__":
- raise SystemExit(main())
diff --git a/scripts/verify_ci_workflow.py b/scripts/verify_ci_workflow.py
index e1b4bf28..d23b1d94 100644
--- a/scripts/verify_ci_workflow.py
+++ b/scripts/verify_ci_workflow.py
@@ -329,7 +329,6 @@ def validate_ci_workflow(path: Path = DEFAULT_CI_WORKFLOW) -> list[str]:
"hard production gates",
"scripts/verify_ci_workflow.py",
"scripts/check_public_api.py",
- "scripts/check_claim_guardrails.py",
"ruff check .",
"scripts/smoke_render.py",
"Browser lifecycle smoke",
diff --git a/scripts/verify_local.py b/scripts/verify_local.py
index b59303fe..052d883e 100644
--- a/scripts/verify_local.py
+++ b/scripts/verify_local.py
@@ -75,11 +75,6 @@ def _base_checks(
(py, "-m", "pytest", "-q", "tests/test_import.py", "tests/test_dependencies.py"),
requires_modules=("pytest",),
),
- Check(
- "claim_guardrails",
- "public performance-claim guardrails",
- (py, "scripts/check_claim_guardrails.py"),
- ),
Check(
"capability_matrix",
"generated capability matrix is current",
@@ -87,7 +82,7 @@ def _base_checks(
),
Check(
"benchmark_harness",
- "benchmark metadata, report, regression, and claim guardrail tests",
+ "benchmark metadata, report, and regression tests",
(
py,
"-m",
@@ -97,7 +92,6 @@ def _base_checks(
"tests/test_bench_pyplot_vs_matplotlib.py",
"tests/test_verify_benchmark_report.py",
"tests/test_check_regressions.py",
- "tests/test_claim_guardrails.py",
),
requires_modules=("pytest",),
),
@@ -288,7 +282,6 @@ def _base_checks(
QUICK_CHECKS = (
"python_floor",
"public_api",
- "claim_guardrails",
"capability_matrix",
"ci_workflow",
"ruff_check",
diff --git a/scripts/verify_sdist.py b/scripts/verify_sdist.py
index e0fdb2ab..51d0eb86 100644
--- a/scripts/verify_sdist.py
+++ b/scripts/verify_sdist.py
@@ -104,7 +104,6 @@
"examples/reflex/xy_reflex_demo/__init__.py",
"examples/reflex/xy_reflex_demo/xy_reflex_demo.py",
"scripts/check_public_api.py",
- "scripts/check_claim_guardrails.py",
"scripts/gen_capability_matrix.py",
"scripts/check_python_floor.py",
"scripts/check_regressions.py",
@@ -120,7 +119,6 @@
"src/kernels.rs",
"src/lib.rs",
"tests/test_public_api.py",
- "tests/test_claim_guardrails.py",
"tests/test_benchmark_environment.py",
"tests/test_bench_pyplot_vs_matplotlib.py",
"tests/test_check_regressions.py",
@@ -397,7 +395,8 @@ def verify_sdist(path: str) -> None:
"make check-sdist",
"make check-examples",
"make check-benchmark-report",
- "Performance Claims",
+ "Competitive Evidence",
+ "outperform every competing charting library",
},
)
_require_file_contains(
diff --git a/spec/README.md b/spec/README.md
index bde52d7d..669ca289 100644
--- a/spec/README.md
+++ b/spec/README.md
@@ -29,8 +29,8 @@ The public surface: what callers can build, style, export, and interact with.
`python/xy/styling/capabilities.py`.
Regenerate with `scripts/gen_capability_matrix.py --write`; the test suite
fails if it is stale.
-- [`chart-roadmap.md`](api/chart-roadmap.md) — the single 2D-first chart-type
- coverage backlog, prioritized by popularity and primitive reuse.
+- [`chart-roadmap.md`](api/chart-roadmap.md) — the staged chart-type coverage
+ backlog, from core 2D through geographic, 3D, and volume visualization.
- [`export.md`](api/export.md) — how a figure becomes bytes: one entry point
across five image formats, deterministic engine choice, browser-free default.
- [`interaction.md`](api/interaction.md) — the authority on which browser
diff --git a/spec/api/chart-roadmap.md b/spec/api/chart-roadmap.md
index 25f5e51d..25ffb8f0 100644
--- a/spec/api/chart-roadmap.md
+++ b/spec/api/chart-roadmap.md
@@ -1,11 +1,13 @@
# Chart Type Roadmap
-This is the single chart-type roadmap for xy. It is intentionally
-**2D-first**: the library should become broad enough for Plotly-class analytics,
-finance, science, operations, and dashboard use cases before spending product
-energy on 3D/volume rendering. Nothing in this section implies immediate
-implementation; it is the coverage backlog we will pull from as the primitives
-land.
+This is the single chart-type roadmap for xy. The destination is every chart
+family: analytical and statistical charts, finance, science, operations,
+dashboards, geographic views, 3D, and volume rendering. Delivery is staged:
+the current implementation builds broad 2D coverage first because those shared
+primitives unlock the most use cases, then carries the same performance and
+customization goals into additional coordinate systems. Nothing in this
+section implies immediate implementation; it is the coverage backlog we will
+pull from as the primitives land.
The roadmap prioritizes chart types by two signals:
@@ -14,10 +16,10 @@ The roadmap prioritizes chart types by two signals:
2. **xy fit:** chart types where the engine can win on large data,
binary transport, WebGL rendering, and screen-bounded aggregation.
-The near-term wedge is large data, but the long-term product goal is broader:
-xy should become a **Plotly-class general-purpose charting library** for
-analytics, science, finance, operations, and dashboards. Performance is the
-entry point, not the boundary of the product.
+The near-term wedge is large data, but the product goal is broader: xy should
+outperform every competing charting library across chart breadth, data scale,
+performance, and customization. Performance is the entry point, not the
+boundary of the product.
The current implemented surface is **line**, **scatter**, **area**,
**histogram**, **bar/column**, **heatmap**, **error bars/bands**,
@@ -83,7 +85,7 @@ These sources are used as popularity proxies, not as exact usage telemetry:
| [Bokeh gallery](https://docs.bokeh.org/en/latest/docs/gallery.html) | line, scatter, bar, patches, image, heatmap, contour, candlestick, maps, network, linked brushing, streaming |
| Internal design dossier, section 28 and 24 | bar/histogram, heatmap, box/violin, candlestick/OHLC as important compatibility targets |
-## 2D-First Priority Roadmap
+## Staged Chart Coverage Roadmap
### P0 - Already in place
@@ -209,18 +211,19 @@ depth: strip/swarm/boxen/rug distributions, regression diagnostics, richer
| 32 | Packed bubble / Venn / Euler | Compatibility and presentation charts. | Layout algorithms and label placement dominate. |
| 33 | Pictorial bar / item chart / image markers / text marks | ECharts/Highcharts compatibility polish. | Symbol systems and asset handling. |
-### P6 - 2D domains to defer until core breadth is strong
+### P6 - Additional domains after core breadth is strong
| Chart family | Reason to defer |
|---|---|
| Maps / choropleth / geo scatter / routes / density maps | 2D, but requires projection, tile, geography data, and viewport semantics; valuable as a later domain package. |
-| 3D scatter / surface / volume | Explicitly outside the 2D-first roadmap; different rendering and interaction model. |
+| 3D scatter / surface / mesh / volume | Requires dedicated camera, projection, lighting, depth, picking, and interaction primitives; part of the product destination after the shared 2D foundations are broad. |
### P7 - Long-term breadth target
The long-term target is not only "fast large scatter." It is an expansive
-library with enough chart breadth to be used across industries the way Plotly is
-used today, while keeping xy' transport and rendering model underneath.
+library that leads across industries and chart families, including geographic,
+3D, and volume visualization, while keeping xy's transport and rendering model
+underneath.
| Industry / use case | Chart coverage needed |
|---|---|
diff --git a/spec/benchmarks/methodology.md b/spec/benchmarks/methodology.md
index 94cb341b..04c49c9b 100644
--- a/spec/benchmarks/methodology.md
+++ b/spec/benchmarks/methodology.md
@@ -10,10 +10,10 @@ are published.
## 0. The three rules
-1. **Mode-scoped claims only** (§2 of the dossier, already policy): "10M
- density in X ms" is a different claim from "10M individually-styled
- markers." Output labels every row `direct | decimated | density | sampled`
- — a reader can never mistake an aggregate result for a raw-marker result.
+1. **Record the rendering mode.** "10M density in X ms" and "10M
+ individually-styled markers in X ms" are different workloads. Output labels
+ every row `direct | decimated | density | sampled`, preserving the result
+ needed to compare and improve each path.
2. **Same-work comparisons.** Each competitor renders the *same visual
contract*, not the same API call: if XY aggregates at 10M, the
fair Plotly comparison is Plotly failing/succeeding at raw markers AND
diff --git a/spec/benchmarks/results.md b/spec/benchmarks/results.md
index 1b92238c..93c1812c 100644
--- a/spec/benchmarks/results.md
+++ b/spec/benchmarks/results.md
@@ -1,8 +1,9 @@
-# Scatter benchmark: xy vs Python charting libraries
+# Competitive benchmark program
-The same scatter (a correlated 2-D cloud) at growing point counts, across
-popular Python charting libraries, on four factors: **how many points each can
-render, how fast, how much memory, and the render payload size.**
+The recorded launch comparison uses the same scatter (a correlated 2-D cloud)
+at growing point counts across popular Python charting libraries, measuring
+four factors: **how many points each can render, how fast, how much memory, and
+the render payload size.**
The cross-library harness lives in `benchmarks/bench_vs.py` and includes
optional adapters for xy, matplotlib, seaborn, Plotly, Bokeh, Altair,
@@ -25,12 +26,32 @@ python benchmarks/bench_vs.py
The xy-only arm also runs with no dependencies via
`benchmarks/bench_scatter_native.py`.
+## Competitive product goal
+
+XY's product goal is to outperform every competing charting library across
+user-visible performance, chart breadth, and customization. The benchmark
+program is the evidence system for that goal, not a showcase for one favorable
+workload.
+
+Every major competitor should be represented wherever it leads or defines
+user expectations. Comparisons should cover small through massive datasets,
+exact and aggregated rendering, static and interactive output, cold and warm
+paths, interaction latency, memory, transport and export size, and multi-chart
+applications. Environments, raw machine-readable results, output contracts,
+and reproduction commands belong in the repository so every result can be
+inspected and rerun.
+
+Timing alone cannot prove chart breadth or design control. Those dimensions
+use the generated capability matrix, matched visual examples, renderer
+coverage, and compatibility corpora as evidence. When another library is
+better on a relevant workload or capability, record the loss as a product gap,
+add a reproducible comparison, and keep it visible until XY wins it.
+
## Benchmark categories and goals
-The performance story should be measured by mode, not with one blanket
-"fastest charting library" number. A small exact scatter, a 10M density view, a
-large line, and a 30-chart dashboard stress different parts of the system. These
-are the categories we track or plan to add to CI.
+Performance is tracked by mode and workload. A small exact scatter, a 10M
+density view, a large line, and a 30-chart dashboard stress different parts of
+the system. These are the categories we track or plan to add to CI.
The stable category IDs live in `benchmarks/categories.py`. CI's benchmark JSON
artifacts (`benchmark.json`, `line.json`, `install.json`, `install-fresh.json`,
@@ -191,25 +212,6 @@ count. Partial rows retain their timing and memory metrics. `bench_workflows.py`
updates, and separate HTML/SVG/native-PNG/Chromium-PNG export rows. All three emit schema-versioned JSON with
environment metadata and benchmark category IDs.
-## Copyable claim taxonomy
-
-Use these shapes when turning benchmark rows into public prose, release notes,
-or posts. The goal is to make every public claim reproducible from a row in
-this document or from a verified JSON artifact.
-
-| Claim shape | Safe wording pattern | Required context |
-|---|---|---|
-| Payload/prep comparison | "In the native backend benchmark, histogram payload prep for 10k values / 200 bins was 17.3x faster than Plotly." | chart type, workload, backend, compared library, metric |
-| Browser first paint | "For the measured Chrome TTFR row, the 10k-value histogram first painted 5.0x faster than Plotly." | browser/render target, workload, chart type, TTFR included |
-| Large scatter overview | "The 100M scatter overview uses density mode with a 258 KB wire payload; it is not drawing 100M exact markers." | mode, point count, payload, exact-vs-aggregate wording |
-| Line decimation | "The 10M line benchmark ships an M4-decimated ~60 KB payload while preserving the extrema oracle." | mode, point count, payload, correctness oracle |
-| Install/import footprint | "In the install-footprint benchmark, cold import was 6.4 ms for the measured distribution." | benchmark name, metric, measured distribution |
-
-Do not shorten those into broad slogans such as "xy is faster than
-Plotly" or "renders 10M points" without the row context. If a sentence does not
-name the chart type, workload, mode, backend, metric, and render target where
-they matter, it is not ready to publish.
-
## `xy.pyplot` versus Matplotlib/Agg
`benchmarks/bench_pyplot_vs_matplotlib.py` runs the same Matplotlib-style calls
@@ -1010,6 +1012,5 @@ falls over. This is the path xy exists to replace.
- xy memory is Python-side `tracemalloc` peak; its GPU/native bytes are
separate and itemized by `Figure.memory_report()` (§27). The payload column is
the transport cost across the kernel→browser boundary.
-- Single CI machine, one run; treat as order-of-magnitude, not a spec. Re-run in
- CI (`benchmark` job) or locally. No universal claims — every number is
- mode-scoped (dossier §2/§31).
+- Single CI machine, one run; re-run in CI (`benchmark` job) or locally and
+ expand the committed baselines across hardware, workloads, and competitors.
diff --git a/spec/process/contributing.md b/spec/process/contributing.md
index 4d710a5c..1a5a8488 100644
--- a/spec/process/contributing.md
+++ b/spec/process/contributing.md
@@ -14,6 +14,22 @@ the hard-won production invariants while the chart surface grows.
the asset. Do not patch only the generated HTML unless you are applying an
emergency local fix and immediately back-porting it to the generator.
+## Competitive Evidence
+
+XY's product north star is to outperform every competing charting library
+across performance, chart breadth, customization, and data scale. Feature work
+should move that comparison forward and leave evidence behind:
+
+- benchmark the affected workload against the relevant best-in-class
+ alternatives at representative small and large data sizes;
+- preserve the benchmark environment, raw report, and reproduction command;
+- update the generated capability matrix, compatibility corpus, or matched
+ visual examples for breadth and customization work; and
+- add regression coverage for the user-visible result that created the win.
+
+See `spec/benchmarks/results.md` for the competitive evidence program and
+tracked benchmark categories.
+
## Local Checks
Install the dev environment and build the required native core:
@@ -97,13 +113,6 @@ regression comparison scripts, or report-schema validation, run:
make check-benchmark-harness
```
-When you edit public docs, package metadata, benchmark summaries, or anything
-that could become a public performance claim, run:
-
-```bash
-make check-claims
-```
-
When you edit `spec/api/api-examples.md` or the Reflex example dashboard
registry/assets, run:
@@ -260,20 +269,3 @@ Start with the smallest reusable primitive surface:
- Composition API wrapper if the chart is user-facing.
- Example app card with normal-size data, not only a stress demo.
- Benchmark row only when the comparison methodology is honest.
-
-## Performance Claims
-
-Do not write broad claims like "faster than Plotly" without naming:
-
-- chart type
-- data size and shape
-- backend (always `native`; there is no NumPy fallback)
-- render target
-- whether browser time-to-first-render is included
-- whether the result is exact markers, decimated geometry, density, or adaptive
-
-Numeric multipliers such as "10x faster" or "5x smaller" need the same measured
-benchmark context.
-
-When in doubt, phrase it as a measured row from `spec/benchmarks/results.md`, not as a
-universal product claim, and run `make check-claims` before publishing.
diff --git a/spec/process/production-readiness.md b/spec/process/production-readiness.md
index 9ee5049a..35e5c73b 100644
--- a/spec/process/production-readiness.md
+++ b/spec/process/production-readiness.md
@@ -1,8 +1,8 @@
# Production Readiness
This is the release bar for xy while the core renderer is still moving.
-It separates hard gates from advisory measurements so performance claims,
-packaging promises, and API stability do not depend on memory or vibes.
+It separates hard gates from advisory measurements so packaging promises and
+API stability do not depend on memory or vibes.
## Current Contract
@@ -46,12 +46,11 @@ integration are still experimental and may change before a 1.0 release.
## Accessibility and Cross-Browser Conformance Status
-The current conformance tier is intentionally narrower than a claim of full
-WCAG parity or pixel-identical output across browsers. The browser client now
-ships a parallel semantic chart region and generated trace/axis summary, a
-polite live region for hover and keyboard readouts, focusable direct-point
-navigation with Arrow/Home/End keys, named toolbar controls with toggle state,
-visible focus styling, reduced-motion behavior, and forced-colors affordances.
+The current conformance tier covers a parallel semantic chart region and
+generated trace/axis summary, a polite live region for hover and keyboard
+readouts, focusable direct-point navigation with Arrow/Home/End keys, named
+toolbar controls with toggle state, visible focus styling, reduced-motion
+behavior, and forced-colors affordances.
CI runs the same focused chart in Playwright Chromium, Firefox, and WebKit. It
checks those semantics and interactions in every engine, compares WebGL output
@@ -59,22 +58,19 @@ with a coarse per-channel perceptual signature, and compares DOM chrome through
layout boxes rather than browser-font glyph pixels. The gate does **not** yet
cover aggregated-bin keyboard navigation, a view-as-table escape hatch,
screen-reader/OS combinations, every chart family, or full-page screenshot
-parity. Until those surfaces have dedicated evidence, neither full
-accessibility parity nor broad perceptual cross-browser consistency is a safe
-public claim. Run the focused tier locally with `make check-conformance` after
+parity. Run the focused tier locally with `make check-conformance` after
installing all three engines with
`npx playwright install chromium firefox webkit`.
## Release-Blocking Gates
-These must pass before publishing or making a broad performance claim.
+These must pass before publishing.
| Area | Gate | Command or evidence |
|---|---|---|
| Python floor | `pyproject.toml`, Ruff, docs, syntax, and annotations stay on the Python 3.11+ floor | `python scripts/check_python_floor.py` |
| Public API | `__all__`, lazy exports, `__version__`, the source `py.typed` marker, focused type-surface tests, and fresh-process import-time budget stay coherent | `make check-api` |
| Import-time budget | `xy.__init__`, `dir(xy)`, export helpers, chart construction, and `.widget()` keep their lazy import boundaries | `make check-import` |
-| Claim guardrails | Public docs and package metadata avoid broad, unqualified performance claims | `make check-claims` |
| CI/release workflows | Hard gates, non-blocking benchmarks, best-effort benchmark artifact upload/download, trusted publishing, and no-Rust clear-error jobs stay wired | `make check-ci` |
| HTML export safety | Inline JSON/script escaping, atomic path writes, hostile user strings, and browser client text-node insertion stay protected | `make check-security` |
| Python tests | Native backend passes | `pytest -q` |
@@ -260,8 +256,8 @@ dependency boundaries, widget/export boundaries, or backend import setup:
make check-import
```
-Use this before turning a generated benchmark JSON file into docs, release
-notes, or a public claim:
+Use this to validate generated benchmark JSON before publication or downstream
+analysis:
```bash
make check-benchmark-report BENCHMARK_JSON=benchmark.json BENCHMARK_KIND=scatter-vs
@@ -275,13 +271,6 @@ tests:
make check-benchmark-harness
```
-Use this after editing public docs, package metadata, benchmark summaries,
-release notes, or other performance-claim surfaces:
-
-```bash
-make check-claims
-```
-
Browser smoke and package artifact verification need a built bundle, Chromium,
and wheel/sdist outputs. The interaction gate's real-wall-clock worker probe
also uses the pinned development-only Playwright driver; install it once with
@@ -406,11 +395,6 @@ Before tagging a release:
are sdist-only.
- Confirm the wheel size budget is still below 15 MB.
- Confirm `spec/api/api-examples.md` runs against the tagged API.
-- Confirm package metadata uses measured, scoped language rather than broad
- "faster/best" positioning.
-- Confirm performance claims mention chart type, mode, backend, data size, and
- browser TTFR status.
-
### reflex-xy releases
The adapter is a pure-Python distribution — no native core, no JS build, no
@@ -447,52 +431,6 @@ Before the first adapter release (and after any change to its pipeline):
carry a copy of the render client — the client links out of the installed
xy package at app compile so it can never drift from the kernel.
-## Claim Guardrails
-
-Safe claims:
-
-- xy avoids JSON-number payloads for core chart data.
-- Large scatter overview rendering is screen-bounded when using density mode.
-- The native backend is substantially faster than pure Python/NumPy for kernel
- work covered by the benchmarks.
-- Current 2D core chart benchmarks beat Plotly on the measured payload-prep,
- payload-size, and TTFR rows documented in `spec/benchmarks/results.md`.
-
-Claims that need qualification:
-
-- "Renders 10M points" must say whether the chart is drawing exact markers or a
- density/adaptive representation.
-- "Faster than Plotly" must name the chart type, data size, render target,
- backend, and whether browser TTFR is included.
-- "Install without Rust" means a published wheel (which carries the native core)
- installs with no toolchain; a source build still requires Rust, and a
- platform with no native core raises a clear ImportError rather than degrading.
-
-Not yet safe:
-
-- Plotly-level chart breadth.
-- Full accessibility parity.
-- Cross-browser/perceptual rendering conformance.
-- Exact-marker interaction for every possible 100M-point zoom path.
-- Production Reflex state integration as a first-class API.
-- More than ~12 charts *simultaneously in view* holding live WebGL contexts.
- Browsers cap live contexts per page (~16 in Chrome); the render client's
- context governor keeps xy inside a budget (default 12) by having
- the least-recently-visible off-screen chart release its context and
- re-acquire on scroll-into-view. Measured (`benchmarks/bench_dashboard.py`,
- 2026-07-09, Chrome/macOS): 10/20/50-chart dashboards are all fully usable —
- every chart nonblank when visited, recovery p95 ~8 ms, heap sublinear
- (28 MB at 50 charts) — but a layout keeping more than the budget visible
- at once can still hit browser-side eviction, so do not claim unbounded
- simultaneous live charts. The browser cap is process-wide (shared across a
- tab's iframes), so the governor shares one budget across **same-origin**
- frames over a `BroadcastChannel` (§18): a chart-per-iframe page (the
- `examples/fastapi` gallery) stays under the cap instead of flooding the
- console with "Too many active WebGL contexts". Cross-origin and
- `sandbox`-without-`allow-same-origin` frames cannot share the channel and
- fall back to per-document budgeting — many such isolated frames in one tab
- can still collectively exceed the cap.
-
## Hardening Backlog
Keep pushing these in low-conflict increments:
diff --git a/tests/test_claim_guardrails.py b/tests/test_claim_guardrails.py
deleted file mode 100644
index 3ae5024b..00000000
--- a/tests/test_claim_guardrails.py
+++ /dev/null
@@ -1,175 +0,0 @@
-from __future__ import annotations
-
-import importlib.util
-import sys
-from pathlib import Path
-
-
-def _load_claim_module():
- path = Path(__file__).resolve().parents[1] / "scripts" / "check_claim_guardrails.py"
- spec = importlib.util.spec_from_file_location("check_claim_guardrails", path)
- assert spec is not None and spec.loader is not None
- module = importlib.util.module_from_spec(spec)
- sys.modules[spec.name] = module
- spec.loader.exec_module(module)
- return module
-
-
-check_claim_guardrails = _load_claim_module()
-
-
-def _write(tmp_path: Path, text: str) -> Path:
- path = tmp_path / "doc.md"
- path.write_text(text, encoding="utf-8")
- return path
-
-
-def test_claim_guardrail_rejects_broad_fastest_claim(tmp_path: Path) -> None:
- path = _write(tmp_path, "xy is the fastest charting library.\n")
-
- findings = check_claim_guardrails.check_claims([path])
-
- assert any("broad superlative" in finding.message for finding in findings)
-
-
-def test_claim_guardrail_rejects_broad_best_charting_claim(tmp_path: Path) -> None:
- path = _write(tmp_path, "xy is the best Python charting library.\n")
-
- findings = check_claim_guardrails.check_claims([path])
-
- assert any("broad superlative" in finding.message for finding in findings)
-
-
-def test_claim_guardrail_rejects_most_performant_claim(tmp_path: Path) -> None:
- path = _write(tmp_path, "xy is the most performant graphing engine.\n")
-
- findings = check_claim_guardrails.check_claims([path])
-
- assert any("broad superlative" in finding.message for finding in findings)
-
-
-def test_claim_guardrail_rejects_broad_more_performant_claim(tmp_path: Path) -> None:
- path = _write(tmp_path, "xy is more performant than every data library.\n")
-
- findings = check_claim_guardrails.check_claims([path])
-
- assert any("broad superlative" in finding.message for finding in findings)
-
-
-def test_claim_guardrail_rejects_broad_package_metadata_claim(tmp_path: Path) -> None:
- path = _write(
- tmp_path,
- 'description = "A faster charting engine: native Rust core, binary transport"\n',
- )
-
- findings = check_claim_guardrails.check_claims([path])
-
- assert any("broad superlative" in finding.message for finding in findings)
-
-
-def test_claim_guardrail_rejects_unqualified_plotly_claim(tmp_path: Path) -> None:
- path = _write(tmp_path, "xy is faster than Plotly.\n")
-
- findings = check_claim_guardrails.check_claims([path])
-
- assert any("comparative performance" in finding.message for finding in findings)
-
-
-def test_claim_guardrail_accepts_measured_qualified_plotly_claim(tmp_path: Path) -> None:
- path = _write(
- tmp_path,
- (
- "In the smoke benchmark, the native backend is faster than Plotly "
- "for the 100k histogram payload-prep row with TTFR measured.\n"
- ),
- )
-
- findings = check_claim_guardrails.check_claims([path])
-
- assert findings == []
-
-
-def test_claim_guardrail_rejects_unqualified_numeric_multiplier(tmp_path: Path) -> None:
- path = _write(tmp_path, "xy is 10x faster and 5x smaller.\n")
-
- findings = check_claim_guardrails.check_claims([path])
-
- assert any("numeric performance multiplier" in finding.message for finding in findings)
-
-
-def test_claim_guardrail_accepts_measured_numeric_multiplier(tmp_path: Path) -> None:
- path = _write(
- tmp_path,
- (
- "Measured in the native backend benchmark with Chrome TTFR.\n\n"
- "| Chart | Workload | Payload-prep vs Plotly | Payload reduction |\n"
- "|---|---:|---:|---:|\n"
- "| Histogram | 100k values / 200 bins | 303x faster | 348x smaller |\n"
- ),
- )
-
- findings = check_claim_guardrails.check_claims([path])
-
- assert findings == []
-
-
-def test_claim_guardrail_accepts_policy_text(tmp_path: Path) -> None:
- path = _write(
- tmp_path,
- 'Do not write broad claims like "faster than Plotly" without naming chart type.\n',
- )
-
- findings = check_claim_guardrails.check_claims([path])
-
- assert findings == []
-
-
-def test_claim_guardrail_requires_mode_for_large_point_rendering(tmp_path: Path) -> None:
- path = _write(tmp_path, "xy renders 100M points.\n")
-
- findings = check_claim_guardrails.check_claims([path])
-
- assert any("large-point rendering" in finding.message for finding in findings)
-
-
-def test_claim_guardrail_accepts_mode_scoped_large_point_rendering(tmp_path: Path) -> None:
- path = _write(
- tmp_path,
- "The benchmark measured 100M points in density mode on the native backend.\n",
- )
-
- findings = check_claim_guardrails.check_claims([path])
-
- assert findings == []
-
-
-def test_claim_guardrail_rejects_stale_repo_identity(tmp_path: Path) -> None:
- path = _write(
- tmp_path,
- "Report issues at https://github.com/Alek99/charts-exp/security/advisories/new\n",
- )
-
- findings = check_claim_guardrails.check_claims([path])
-
- assert any("stale repository identity" in finding.message for finding in findings)
-
-
-def test_claim_guardrail_rejects_customization_superlatives(tmp_path: Path) -> None:
- # The styling surface is a bounded, enumerated subset — see
- # spec/api/capability-matrix.md — so these shapes are wrong however the
- # sentence is framed, and no amount of shipping earns them.
- for line in (
- "XY is the most customizable charting library.\n",
- "XY is fully customizable.\n",
- "Style anything you like.\n",
- "Customize everything about your chart.\n",
- "More extensible than any Python plotting library.\n",
- ):
- findings = check_claim_guardrails.check_claims([_write(tmp_path, line)])
- assert any("customization superlative" in f.message for f in findings), line
-
-
-def test_claim_guardrail_scans_the_readme(tmp_path: Path) -> None:
- # The README was outside the default set, which is where a slogan is most
- # likely to be written and least likely to be reviewed.
- assert "README.md" in check_claim_guardrails.DEFAULT_DOCS
diff --git a/tests/test_verify_ci_workflow.py b/tests/test_verify_ci_workflow.py
index 2220aca7..8e5725e5 100644
--- a/tests/test_verify_ci_workflow.py
+++ b/tests/test_verify_ci_workflow.py
@@ -346,23 +346,6 @@ def test_codspeed_workflow_rejects_non_strict_native_install(tmp_path: Path) ->
)
-def test_ci_workflow_rejects_missing_claim_guardrail_gate(tmp_path: Path) -> None:
- workflow = Path(".github/workflows/ci.yml").read_text(encoding="utf-8")
- path = tmp_path / "ci.yml"
- path.write_text(
- workflow.replace(
- " - name: Claim guardrails\n"
- " run: .venv/bin/python scripts/check_claim_guardrails.py\n\n",
- "",
- ),
- encoding="utf-8",
- )
-
- errors = verify_ci_workflow.validate_workflow(path)
-
- assert any("test job" in error and "check_claim_guardrails" in error for error in errors)
-
-
def test_ci_workflow_rejects_missing_interaction_stress_smoke(tmp_path: Path) -> None:
workflow = Path(".github/workflows/ci.yml").read_text(encoding="utf-8")
path = tmp_path / "ci.yml"
diff --git a/tests/test_verify_local.py b/tests/test_verify_local.py
index 7e41a0b5..cc1547d7 100644
--- a/tests/test_verify_local.py
+++ b/tests/test_verify_local.py
@@ -161,7 +161,6 @@ def test_benchmark_harness_check_is_known_as_targeted_gate() -> None:
assert "tests/test_benchmark_environment.py" in command
assert "tests/test_verify_benchmark_report.py" in command
assert "tests/test_check_regressions.py" in command
- assert "tests/test_claim_guardrails.py" in command
assert selected[0].requires_modules == ("pytest",)
@@ -337,7 +336,7 @@ def test_list_output_includes_check_requirements(capsys: pytest.CaptureFixture[s
def test_dry_run_prints_selected_commands_without_running(capsys) -> None:
- rc = verify_local.main(["--dry-run", "--only", "python_floor,public_api,claim_guardrails"])
+ rc = verify_local.main(["--dry-run", "--only", "python_floor,public_api"])
out = capsys.readouterr().out
assert rc == 0
@@ -345,8 +344,6 @@ def test_dry_run_prints_selected_commands_without_running(capsys) -> None:
assert "scripts/check_python_floor.py" in out
assert "public_api" in out
assert "scripts/check_public_api.py" in out
- assert "claim_guardrails" in out
- assert "scripts/check_claim_guardrails.py" in out
def test_dry_run_includes_ci_workflow_gate(capsys) -> None:
@@ -532,9 +529,9 @@ def test_makefile_exposes_docs_verification_shortcut() -> None:
makefile = (ROOT / "Makefile").read_text(encoding="utf-8")
assert "check-docs:" in makefile
- assert "scripts/verify_local.py --only examples,claim_guardrails" in makefile
+ assert "scripts/verify_local.py --only examples" in makefile
assert "make check-docs" in makefile
- assert "docs examples and public claim guardrails" in makefile
+ assert "run documentation examples" in makefile
def test_makefile_exposes_security_verification_shortcut() -> None:
@@ -582,15 +579,6 @@ def test_makefile_exposes_ci_workflow_verification_shortcut() -> None:
assert "CI/release workflow invariant checks" in makefile
-def test_makefile_exposes_claim_guardrail_shortcut() -> None:
- makefile = (ROOT / "Makefile").read_text(encoding="utf-8")
-
- assert "check-claims:" in makefile
- assert "scripts/verify_local.py --only claim_guardrails" in makefile
- assert "make check-claims" in makefile
- assert "public performance-claim guardrails" in makefile
-
-
def test_node_version_parser_accepts_v_prefixed_versions() -> None:
assert verify_local._node_major("v18.19.1\n") == 18
assert verify_local._node_major("20.11.0") == 20
diff --git a/tests/test_verify_sdist.py b/tests/test_verify_sdist.py
index 8a52bf4b..462da9ea 100644
--- a/tests/test_verify_sdist.py
+++ b/tests/test_verify_sdist.py
@@ -60,8 +60,8 @@
"make check-benchmark-report.\n"
"Run make check-examples for spec/api/api-examples.md and "
"the example apps.\n\n"
- "## Performance Claims\n\n"
- "Claims need benchmark context.\n" + ("contributing padding\n" * 100)
+ "## Competitive Evidence\n\n"
+ "XY aims to outperform every competing charting library.\n" + ("contributing padding\n" * 100)
)
CI_YML = (
"name: CI\n"