Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 39 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,43 @@ jobs:
- name: Benchmark smoke (1e5/1e6 — §12 harness runs every phase)
run: .venv/bin/python scripts/bench.py --sizes 1e5,1e6

reflex_compatibility:
name: Reflex compatibility (${{ matrix.reflex-version }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
# 0.9.6 is the declared floor; 0.9.8 is the newest released version
# in the supported <0.10 compatibility window.
reflex-version: ["0.9.6", "0.9.8"]
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
- uses: dtolnay/rust-toolchain@4be7066ada62dd38de10e7b70166bc74ed198c30 # stable
- uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
with:
enable-cache: false
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: "22"
- name: Install exact Reflex compatibility target
run: |
npm ci
uv venv .venv
uv pip install -p .venv/bin/python \
"reflex==${{ matrix.reflex-version }}" \
"numpy>=1.24" "anywidget>=0.9" "pytest>=8" "aiohttp>=3.9" "uvicorn>=0.23"
uv pip install -p .venv/bin/python -e . --no-deps
- name: Verify exact Reflex target and metadata window
run: |
.venv/bin/python -c "import importlib.metadata as m; assert m.version('reflex') == '${{ matrix.reflex-version }}'"
.venv/bin/python -c "from packaging.requirements import Requirement; import importlib.metadata as m; req = next(Requirement(value) for value in m.metadata('xy').get_all('Requires-Dist') if Requirement(value).name == 'reflex'); assert str(req.specifier) == '<0.10,>=0.9.6'"
- name: Component compile and event smoke
run: .venv/bin/pytest -q tests/reflex_adapter/test_component.py::test_component_compiles_with_events
- name: State event and rebuild smoke
run: .venv/bin/pytest -q tests/reflex_adapter/test_state_bridge.py::test_rebuild_reads_session_state

browser_conformance:
name: Accessibility + cross-browser conformance
runs-on: ubuntu-latest
Expand Down Expand Up @@ -645,7 +682,7 @@ jobs:
XY_REQUIRE_CARGO: "1"
run: |
uv venv smoke-native
uv pip install --no-cache -p smoke-native dist/*.tar.gz numpy anywidget "reflex>=0.9.6"
uv pip install --no-cache -p smoke-native dist/*.tar.gz numpy anywidget "reflex>=0.9.6,<0.10"
./smoke-native/bin/python - <<'PY'
import importlib.metadata as metadata

Expand All @@ -666,7 +703,7 @@ jobs:
XY_SKIP_CARGO: "1"
run: |
uv venv smoke-no-rust
uv pip install --no-cache -p smoke-no-rust dist/*.tar.gz numpy anywidget "reflex>=0.9.6"
uv pip install --no-cache -p smoke-no-rust dist/*.tar.gz numpy anywidget "reflex>=0.9.6,<0.10"
./smoke-no-rust/bin/python - <<'PY'
import importlib.metadata as metadata

Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ jobs:
shell: bash
run: |
uv venv smoke
uv pip install -p smoke dist/*.whl numpy anywidget "reflex>=0.9.6"
uv pip install -p smoke dist/*.whl numpy anywidget "reflex>=0.9.6,<0.10"
./smoke/bin/python -c "import importlib.metadata as m, reflex_xy, xy.kernels as k; assert k.BACKEND=='native', k.BACKEND; assert reflex_xy.__version__ == m.version('xy'); print('native', k.__file__)" \
|| ./smoke/Scripts/python.exe -c "import importlib.metadata as m, reflex_xy, xy.kernels as k; assert k.BACKEND=='native', k.BACKEND; assert reflex_xy.__version__ == m.version('xy'); print('native')"
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
Expand Down Expand Up @@ -231,7 +231,7 @@ jobs:
XY_REQUIRE_CARGO: "1"
run: |
uv venv smoke-native
uv pip install --no-cache -p smoke-native dist/*.tar.gz numpy anywidget "reflex>=0.9.6"
uv pip install --no-cache -p smoke-native dist/*.tar.gz numpy anywidget "reflex>=0.9.6,<0.10"
./smoke-native/bin/python - <<'PY'
import importlib.metadata as metadata

Expand All @@ -251,7 +251,7 @@ jobs:
XY_SKIP_CARGO: "1"
run: |
uv venv smoke-no-rust
uv pip install --no-cache -p smoke-no-rust dist/*.tar.gz numpy anywidget "reflex>=0.9.6"
uv pip install --no-cache -p smoke-no-rust dist/*.tar.gz numpy anywidget "reflex>=0.9.6,<0.10"
./smoke-no-rust/bin/python - <<'PY'
import importlib.metadata as metadata

Expand Down
3 changes: 2 additions & 1 deletion docs/advanced/runtime-and-deployment.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ interaction math.

The integration is experimental and ships inside the published `xy`
distribution. Install `xy[reflex]` so the environment also selects a supported
Reflex version, then import it as `reflex_xy`. See
Reflex version (`>=0.9.6,<0.10`), then import it as `reflex_xy`. The supported
window is tested at both its minimum and newest released version in CI. See
[Deployment Recipes](/docs/xy/guides/deployment-recipes/).

## Choose a data tier
Expand Down
6 changes: 3 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@ dependencies = [

[project.optional-dependencies]
# The adapter source ships in every xy wheel so the framework integration and
# render client can never drift. The extra only selects the supported Reflex
# floor; importing plain `xy` remains framework-free.
# render client can never drift. The extra selects the tested Reflex window;
# importing plain `xy` remains framework-free.
reflex = [
"reflex>=0.9.6",
"reflex>=0.9.6,<0.10",
]

[project.urls]
Expand Down
31 changes: 28 additions & 3 deletions scripts/verify_ci_workflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,11 @@
"sdist",
"wheels",
"install_without_rust",
"reflex_compatibility",
}
REQUIRED_CODSPEED_JOBS = {"benchmarks"}
REQUIRED_RELEASE_JOBS = {"wheels", "sdist", "publish", "wasm"}
REFLEX_REQUIREMENT = "reflex>=0.9.6,<0.10"


def _job_blocks(text: str) -> dict[str, str]:
Expand Down Expand Up @@ -870,6 +872,29 @@ def validate_ci_workflow(path: Path = DEFAULT_CI_WORKFLOW) -> list[str]:
"locked Reflex development environment",
"uv sync --locked --extra reflex --group dev",
)
_require_job_contains(
errors,
jobs,
"reflex_compatibility",
"CI",
"minimum and maximum released Reflex compatibility matrix",
"fail-fast: false",
'reflex-version: ["0.9.6", "0.9.8"]',
'reflex==${{ matrix.reflex-version }}',
"<0.10,>=0.9.6",
"test_component.py::test_component_compiles_with_events",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The new Reflex compatibility gate can pass with no active Reflex install or smoke tests because it searches the raw job text, including comments and inactive YAML, rather than executable named steps. Structural checks of the install and smoke-test steps would keep CI from silently losing this coverage.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At scripts/verify_ci_workflow.py, line 885:

<comment>The new Reflex compatibility gate can pass with no active Reflex install or smoke tests because it searches the raw job text, including comments and inactive YAML, rather than executable named steps. Structural checks of the install and smoke-test steps would keep CI from silently losing this coverage.</comment>

<file context>
@@ -870,6 +872,29 @@ def validate_ci_workflow(path: Path = DEFAULT_CI_WORKFLOW) -> list[str]:
+        'reflex-version: ["0.9.6", "0.9.8"]',
+        'reflex==${{ matrix.reflex-version }}',
+        "<0.10,>=0.9.6",
+        "test_component.py::test_component_compiles_with_events",
+        "test_state_bridge.py::test_rebuild_reads_session_state",
+    )
</file context>

"test_state_bridge.py::test_rebuild_reads_session_state",
)
try:
project_metadata = (ROOT / "pyproject.toml").read_text(encoding="utf-8")
except OSError as exc:
errors.append(f"cannot read project metadata for Reflex compatibility gate: {exc}")
else:
if f'"{REFLEX_REQUIREMENT}"' not in project_metadata:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The metadata check does not prove that xy[reflex] is bounded: an unrelated occurrence of the exact requirement anywhere in pyproject.toml can make it pass. Parsing project.optional-dependencies.reflex and validating that entry directly would ensure the published extra, not a decoy string, stays within the tested window.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At scripts/verify_ci_workflow.py, line 893:

<comment>The metadata check does not prove that `xy[reflex]` is bounded: an unrelated occurrence of the exact requirement anywhere in `pyproject.toml` can make it pass. Parsing `project.optional-dependencies.reflex` and validating that entry directly would ensure the published extra, not a decoy string, stays within the tested window.</comment>

<file context>
@@ -870,6 +872,29 @@ def validate_ci_workflow(path: Path = DEFAULT_CI_WORKFLOW) -> list[str]:
+    except OSError as exc:
+        errors.append(f"cannot read project metadata for Reflex compatibility gate: {exc}")
+    else:
+        if f'"{REFLEX_REQUIREMENT}"' not in project_metadata:
+            errors.append(
+                "pyproject.toml must bound the xy[reflex] extra to "
</file context>

errors.append(
"pyproject.toml must bound the xy[reflex] extra to "
f"{REFLEX_REQUIREMENT!r}"
)
Comment on lines +888 to +897

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Validate the reflex extra structurally.

Line 893 only searches for the requirement string in the full TOML text. It passes if the requirement moves to another extra, a base dependency, or a comment. Plain xy could then install Reflex, or xy[reflex] could omit it.

Parse pyproject.toml and require project.optional-dependencies.reflex to contain exactly REFLEX_REQUIREMENT.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/verify_ci_workflow.py` around lines 888 - 897, Update the project
metadata validation around REFLEX_REQUIREMENT to parse pyproject.toml as TOML
and inspect project.optional-dependencies.reflex directly. Require that the
reflex extra exists and contains exactly REFLEX_REQUIREMENT, rejecting
placements in other dependency groups, base dependencies, comments, or
additional reflex requirements while preserving the existing read-error
reporting.

_require_job_contains(
errors,
jobs,
Expand Down Expand Up @@ -1120,7 +1145,7 @@ def validate_ci_workflow(path: Path = DEFAULT_CI_WORKFLOW) -> list[str]:
"Rust-backed sdist install contract",
"XY_REQUIRE_CARGO",
"uv pip install --no-cache",
'"reflex>=0.9.6"',
f'"{REFLEX_REQUIREMENT}"',
"import reflex_xy",
"import xy.kernels as kernels",
'kernels.BACKEND == "native"',
Expand Down Expand Up @@ -1266,7 +1291,7 @@ def validate_release_workflow(path: Path = DEFAULT_RELEASE_WORKFLOW) -> list[str
"scripts/verify_wheel.py",
"--expect-native",
"Install-size budget (<= 15 MB)",
'"reflex>=0.9.6"',
f'"{REFLEX_REQUIREMENT}"',
"import importlib.metadata as m, reflex_xy",
"assert reflex_xy.__version__ == m.version('xy')",
"assert k.BACKEND=='native'",
Expand Down Expand Up @@ -1331,7 +1356,7 @@ def validate_release_workflow(path: Path = DEFAULT_RELEASE_WORKFLOW) -> list[str
"Rust-backed release sdist install contract",
"XY_REQUIRE_CARGO",
"uv pip install --no-cache",
'"reflex>=0.9.6"',
f'"{REFLEX_REQUIREMENT}"',
"import reflex_xy",
"import xy.kernels as kernels",
'kernels.BACKEND == "native"',
Expand Down
10 changes: 7 additions & 3 deletions spec/process/production-readiness.md
Original file line number Diff line number Diff line change
Expand Up @@ -392,7 +392,7 @@ Before tagging a release:
the documented native-core error. Wheel
`METADATA` must keep `Name: xy`, `Requires-Python: >=3.11`,
`anywidget>=0.9`, and `numpy>=1.24` as base requirements, plus
`Provides-Extra: reflex` and `reflex>=0.9.6` guarded by that extra. The wheel
`Provides-Extra: reflex` and `reflex>=0.9.6,<0.10` guarded by that extra. The wheel

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The new bounded Reflex contract (reflex>=0.9.6,<0.10) isn't propagated consistently in this file: the sdist release gate in the "Release-Blocking Gates" table (line ~92) still states the sdist PKG-INFO must include reflex>=0.9.6 with no upper bound, contradicting the window introduced here. The confirming gate code also still fullmatches the unbounded form: scripts/artifact_metadata.py _is_exact_reflex_extra uses re.fullmatch(r"\s*reflex\s*>=\s*0\.9\.6\s*;\s*extra...") with no ,<0.10, so it would reject the now-documented bounded Requires-Dist: reflex>=0.9.6,<0.10; extra == "reflex". Update the sdist gate description (and reconcile the verifying script) so the documented window and its enforcement agree.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At spec/process/production-readiness.md, line 395:

<comment>The new bounded Reflex contract (`reflex>=0.9.6,<0.10`) isn't propagated consistently in this file: the sdist release gate in the "Release-Blocking Gates" table (line ~92) still states the sdist `PKG-INFO` must include `reflex>=0.9.6` with no upper bound, contradicting the window introduced here. The confirming gate code also still fullmatches the unbounded form: `scripts/artifact_metadata.py` `_is_exact_reflex_extra` uses `re.fullmatch(r"\s*reflex\s*>=\s*0\.9\.6\s*;\s*extra...")` with no `,<0.10`, so it would reject the now-documented bounded `Requires-Dist: reflex>=0.9.6,<0.10; extra == "reflex"`. Update the sdist gate description (and reconcile the verifying script) so the documented window and its enforcement agree.</comment>

<file context>
@@ -392,7 +392,7 @@ Before tagging a release:
   `METADATA` must keep `Name: xy`, `Requires-Python: >=3.11`,
   `anywidget>=0.9`, and `numpy>=1.24` as base requirements, plus
-  `Provides-Extra: reflex` and `reflex>=0.9.6` guarded by that extra. The wheel
+  `Provides-Extra: reflex` and `reflex>=0.9.6,<0.10` guarded by that extra. The wheel
   must contain `reflex_xy` and `XYChart.jsx`, and `RECORD` must list every
   archive file exactly once with matching `sha256` and size fields. Wheels
</file context>

must contain `reflex_xy` and `XYChart.jsx`, and `RECORD` must list every
archive file exactly once with matching `sha256` and size fields. Wheels
and the sdist remain distribution/build-input-only: docs, tests, benchmarks,
Expand All @@ -405,8 +405,12 @@ Every `xy` release carries the `reflex_xy` Python package and JSX wrapper. The
wrapper links to the render client in the same installed distribution, so
client, kernel, and framework bridge share one version. Plain `xy` must not
install Reflex; `xy[reflex]` must install the declared supported floor.
Release smoke tests install Reflex, import `reflex_xy`, and assert that its
reported version matches the `xy` distribution version.
The supported Reflex window is `>=0.9.6,<0.10`. CI runs the component compile
and state-event rebuild smokes against both the minimum `0.9.6` and the newest
released `0.9.8` in that window. Widen the extra only after adding the new
released version to that matrix and passing both smokes. Release smoke tests
install the bounded extra, import `reflex_xy`, and assert that its reported
version matches the `xy` distribution version.

## Hardening Backlog

Expand Down