From 2cf45efba013acabe86b4adbc23fc8a3baa9c2df Mon Sep 17 00:00:00 2001 From: thekevinbot <248217931+thekevinbot@users.noreply.github.com> Date: Mon, 5 Oct 2026 12:42:27 -0400 Subject: [PATCH 01/11] Probe the version the release-built npm addon reports The release-ci precheck builds the napi addon through the same matrix as the release. A new npm-addon-version job loads the linux-x64 addon and fails when its CLI reports the version committed in packages/rust/Cargo.toml, i.e. when the planned version was never stamped into the crate the CLI reads its version from. --- .github/workflows/release-ci.yml | 30 +++++ internals/checks/src/checks/cli.py | 2 + internals/checks/src/checks/cli_test.py | 4 + .../src/checks/npm_addon_version/__init__.py | 0 .../checks/npm_addon_version/addon_names.py | 7 + .../npm_addon_version/addon_names_test.py | 10 ++ .../src/checks/npm_addon_version/cli.py | 31 +++++ .../src/checks/npm_addon_version/cli_test.py | 36 +++++ .../npm_addon_version/committed_version.py | 11 ++ .../committed_version_test.py | 15 +++ .../src/checks/npm_addon_version/gate.py | 72 ++++++++++ .../src/checks/npm_addon_version/gate_test.py | 124 ++++++++++++++++++ .../tests/e2e/npm_addon_version_test.py | 51 +++++++ 13 files changed, 393 insertions(+) create mode 100644 internals/checks/src/checks/npm_addon_version/__init__.py create mode 100644 internals/checks/src/checks/npm_addon_version/addon_names.py create mode 100644 internals/checks/src/checks/npm_addon_version/addon_names_test.py create mode 100644 internals/checks/src/checks/npm_addon_version/cli.py create mode 100644 internals/checks/src/checks/npm_addon_version/cli_test.py create mode 100644 internals/checks/src/checks/npm_addon_version/committed_version.py create mode 100644 internals/checks/src/checks/npm_addon_version/committed_version_test.py create mode 100644 internals/checks/src/checks/npm_addon_version/gate.py create mode 100644 internals/checks/src/checks/npm_addon_version/gate_test.py create mode 100644 internals/checks/tests/e2e/npm_addon_version_test.py diff --git a/.github/workflows/release-ci.yml b/.github/workflows/release-ci.yml index e8050e9db..4c4f54be9 100644 --- a/.github/workflows/release-ci.yml +++ b/.github/workflows/release-ci.yml @@ -125,3 +125,33 @@ jobs: - name: Probe extension loading through the installed wheel run: uv run --project internals/checks dirsql-checks wheel-extension-load --dist-dir dist/ + + # Every other tier builds the addon from the committed manifests, so only + # the release-shape build can show whether the planned version reached the + # crate the CLI reads its `--version` from. Skips cleanly when the plan + # built no dirsql-npm addon for this PR. + npm-addon-version: + name: npm addon version probe + needs: precheck + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - uses: actions/download-artifact@v8 + with: + pattern: dirsql-npm-linux-x64-gnu + path: dist/ + merge-multiple: true + + - name: Install Node + uses: actions/setup-node@v7 + with: + node-version: "24" + + - name: Install uv + uses: astral-sh/setup-uv@v7 + with: + python-version: "3.13" + + - name: Assert the addon's CLI reports the stamped release version + run: uv run --project internals/checks dirsql-checks npm-addon-version --dist-dir dist/ diff --git a/internals/checks/src/checks/cli.py b/internals/checks/src/checks/cli.py index 198cdcd94..db278c080 100644 --- a/internals/checks/src/checks/cli.py +++ b/internals/checks/src/checks/cli.py @@ -12,6 +12,7 @@ from checks.attestation_guard.cli import cli as attestation_guard from checks.changelog_gate.cli import cli as changelog_gate from checks.declared_deps.cli import cli as declared_deps +from checks.npm_addon_version.cli import cli as npm_addon_version from checks.preflight.cli import cli as preflight from checks.release_globs.cli import cli as release_globs from checks.wheel_extension_load.cli import cli as wheel_extension_load @@ -26,6 +27,7 @@ def main() -> None: main.add_command(attestation_guard, name="attestation-guard") main.add_command(changelog_gate, name="changelog-gate") main.add_command(declared_deps, name="declared-deps") +main.add_command(npm_addon_version, name="npm-addon-version") main.add_command(preflight, name="preflight") main.add_command(release_globs, name="release-globs") main.add_command(wheel_extension_load, name="wheel-extension-load") diff --git a/internals/checks/src/checks/cli_test.py b/internals/checks/src/checks/cli_test.py index 914151384..d9812ed38 100644 --- a/internals/checks/src/checks/cli_test.py +++ b/internals/checks/src/checks/cli_test.py @@ -32,3 +32,7 @@ def test_preflight_is_registered(): def test_release_globs_is_registered(): assert "release-globs" in main.commands + + +def test_npm_addon_version_is_registered(): + assert "npm-addon-version" in main.commands diff --git a/internals/checks/src/checks/npm_addon_version/__init__.py b/internals/checks/src/checks/npm_addon_version/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/internals/checks/src/checks/npm_addon_version/addon_names.py b/internals/checks/src/checks/npm_addon_version/addon_names.py new file mode 100644 index 000000000..c04e23d9c --- /dev/null +++ b/internals/checks/src/checks/npm_addon_version/addon_names.py @@ -0,0 +1,7 @@ +"""The napi addons among a dist directory's entries.""" + +from __future__ import annotations + + +def addon_names(names) -> list[str]: + return sorted(name for name in names if name.endswith(".node")) diff --git a/internals/checks/src/checks/npm_addon_version/addon_names_test.py b/internals/checks/src/checks/npm_addon_version/addon_names_test.py new file mode 100644 index 000000000..9a5731a50 --- /dev/null +++ b/internals/checks/src/checks/npm_addon_version/addon_names_test.py @@ -0,0 +1,10 @@ +from checks.npm_addon_version.addon_names import addon_names + + +def describe_addon_names(): + def filters_to_addons_sorted(): + names = ["b.node", "package.json", "a.node"] + assert addon_names(names) == ["a.node", "b.node"] + + def empty_when_no_addons(): + assert addon_names(["dirsql.whl", "notes.txt"]) == [] diff --git a/internals/checks/src/checks/npm_addon_version/cli.py b/internals/checks/src/checks/npm_addon_version/cli.py new file mode 100644 index 000000000..f6f8a59ff --- /dev/null +++ b/internals/checks/src/checks/npm_addon_version/cli.py @@ -0,0 +1,31 @@ +"""The npm-addon-version check -- repo-only. + +Backs `dirsql-checks npm-addon-version`: probes that the release-built napi +addon's CLI reports the stamped release version, not the committed literal. +""" + +from __future__ import annotations + +import click + +from checks.npm_addon_version.gate import ProbeError, run + + +@click.command() +@click.option( + "--dist-dir", + required=True, + help="Directory containing the built napi addon to probe (may be empty/absent).", +) +@click.option( + "--manifest", + default="packages/rust/Cargo.toml", + help="Manifest of the crate whose version the CLI prints.", +) +def cli(dist_dir: str, manifest: str) -> None: + try: + code = run(dist_dir, manifest) + except ProbeError as err: + click.echo(f"npm-addon-version: {err}", err=True) + raise SystemExit(1) from err + raise SystemExit(code) diff --git a/internals/checks/src/checks/npm_addon_version/cli_test.py b/internals/checks/src/checks/npm_addon_version/cli_test.py new file mode 100644 index 000000000..859a7cf35 --- /dev/null +++ b/internals/checks/src/checks/npm_addon_version/cli_test.py @@ -0,0 +1,36 @@ +"""Colocated unit tests for the npm-addon-version command (isolation -- no +`CliRunner`). Driven through `.callback`; `run` is mocked at its import site. +""" + +from unittest import mock + +import pytest + +from checks.npm_addon_version.cli import ProbeError, cli + + +def test_exits_with_runs_return_code(): + with mock.patch("checks.npm_addon_version.cli.run", return_value=0) as run: + with pytest.raises(SystemExit) as exc_info: + cli.callback(dist_dir="dist/", manifest="Cargo.toml") + run.assert_called_once_with("dist/", "Cargo.toml") + assert exc_info.value.code == 0 + + +def test_probe_error_prints_diagnostic_and_exits_one(capsys): + with mock.patch( + "checks.npm_addon_version.cli.run", + side_effect=ProbeError("unstamped"), + ): + with pytest.raises(SystemExit) as exc_info: + cli.callback(dist_dir="dist/", manifest="Cargo.toml") + assert exc_info.value.code == 1 + assert "npm-addon-version: unstamped" in capsys.readouterr().err + + +def test_declares_a_required_dist_dir_and_a_defaulted_manifest(): + dist_dir, manifest = cli.params + assert dist_dir.name == "dist_dir" + assert dist_dir.required is True + assert manifest.name == "manifest" + assert manifest.default == "packages/rust/Cargo.toml" diff --git a/internals/checks/src/checks/npm_addon_version/committed_version.py b/internals/checks/src/checks/npm_addon_version/committed_version.py new file mode 100644 index 000000000..cf2321297 --- /dev/null +++ b/internals/checks/src/checks/npm_addon_version/committed_version.py @@ -0,0 +1,11 @@ +"""The `[package].version` literal committed in a crate manifest.""" + +from __future__ import annotations + +from collections.abc import Callable + +from ..files.read_config import read_config + + +def committed_version(manifest: str, config: Callable[[str], dict] = read_config) -> str: + return config(manifest)["package"]["version"] diff --git a/internals/checks/src/checks/npm_addon_version/committed_version_test.py b/internals/checks/src/checks/npm_addon_version/committed_version_test.py new file mode 100644 index 000000000..48c86e298 --- /dev/null +++ b/internals/checks/src/checks/npm_addon_version/committed_version_test.py @@ -0,0 +1,15 @@ +from unittest import mock + +from checks.npm_addon_version.committed_version import committed_version + + +def describe_committed_version(): + def reads_the_package_version(): + config = mock.Mock(return_value={"package": {"version": "0.2.7"}}) + assert committed_version("Cargo.toml", config) == "0.2.7" + config.assert_called_once_with("Cargo.toml") + + def it_defaults_to_the_real_toml_reader(tmp_path): + manifest = tmp_path / "Cargo.toml" + manifest.write_text('[package]\nname = "x"\nversion = "1.2.3"\n') + assert committed_version(str(manifest)) == "1.2.3" diff --git a/internals/checks/src/checks/npm_addon_version/gate.py b/internals/checks/src/checks/npm_addon_version/gate.py new file mode 100644 index 000000000..d80891dca --- /dev/null +++ b/internals/checks/src/checks/npm_addon_version/gate.py @@ -0,0 +1,72 @@ +"""Probe the version the release-built npm addon's CLI reports. + +Loads the Release Precheck matrix's linux-x64 napi addon -- built by the same +`_matrix.yml` as the published one -- and runs its `runCli(["--version"])`. The +pipeline must stamp the planned release version into the crate whose +`CARGO_PKG_VERSION` the CLI prints. Every planned version is newer than the +literal committed in that crate's manifest, so an addon still reporting the +literal was built without the stamp. +""" + +from __future__ import annotations + +import os +import subprocess +from collections.abc import Callable + +from checks.probe.probe_error import ProbeError +from checks.probe.require_zero import require_zero +from checks.wheel_extension_load.list_names import list_names + +from .addon_names import addon_names +from .committed_version import committed_version + +PRINT_VERSION = "process.exitCode = require(process.argv[1]).runCli(['--version'])" + + +def run( + dist_dir: str, + manifest: str, + runner=subprocess.run, + listdir=os.listdir, + committed: Callable[[str], str] = committed_version, +) -> int: + addons = addon_names(list_names(dist_dir, listdir)) + if len(addons) > 1: + raise ProbeError( + f"expected exactly one napi addon to probe, saw {addons}. " + "Tighten the download-artifact pattern in release-ci.yml so only " + "the linux-x64 addon lands in the probe's dist dir." + ) + if not addons: + print( + f"No napi addon under {dist_dir} -- the precheck matrix planned no " + "dirsql-npm build for this PR; version probe skipped." + ) + return 0 + (addon_name,) = addons + addon = os.path.abspath(os.path.join(dist_dir, addon_name)) + + probe = runner( + ["node", "-e", PRINT_VERSION, addon], + stdin=subprocess.DEVNULL, + capture_output=True, + text=True, + ) + require_zero( + probe, + f"`runCli(['--version'])` failed: stdout={probe.stdout!r}, stderr={probe.stderr!r}", + ) + reported = probe.stdout.strip() + literal = committed(manifest) + if reported == f"dirsql {literal}": + raise ProbeError( + f"the npm addon's CLI reports `{reported}`, the version committed in " + f"{manifest}: the release pipeline did not stamp the planned version " + "into the crate the CLI reads its version from, so `npx dirsql " + "--version` would misreport every release. putitoutthere stamps a " + "napi build only when the crate's Cargo.toml sits at the npm " + "package's `path` in putitoutthere.toml." + ) + print(f"ok npm-addon-version: {addon_name} reports `{reported}`") + return 0 diff --git a/internals/checks/src/checks/npm_addon_version/gate_test.py b/internals/checks/src/checks/npm_addon_version/gate_test.py new file mode 100644 index 000000000..81faba5e8 --- /dev/null +++ b/internals/checks/src/checks/npm_addon_version/gate_test.py @@ -0,0 +1,124 @@ +import os +from unittest import mock + +import pytest + +from checks.npm_addon_version.gate import ( + PRINT_VERSION, + ProbeError, + addon_names, + committed_version, + list_names, + require_zero, + run, +) + +ADDON = "dirsql.linux-x64-gnu.node" + + +def _result(returncode=0, stdout="", stderr=""): + return mock.Mock(returncode=returncode, stdout=stdout, stderr=stderr) + + +def _run_with(runner, literal="0.2.7"): + return run( + "dist", + "Cargo.toml", + runner=runner, + listdir=mock.Mock(return_value=[ADDON]), + committed=mock.Mock(return_value=literal), + ) + + +def describe_collaborators(): + def it_resolves_the_probe_error_from_the_shared_probe_package(): + assert ProbeError.__module__ == "checks.probe.probe_error" + + def it_resolves_require_zero_from_the_shared_probe_package(): + assert require_zero.__module__ == "checks.probe.require_zero" + + def it_resolves_list_names_from_the_wheel_probe(): + assert list_names.__module__ == "checks.wheel_extension_load.list_names" + + def it_resolves_addon_names_from_its_own_module(): + assert addon_names.__module__ == "checks.npm_addon_version.addon_names" + + def it_defaults_to_reading_the_committed_version(): + assert run.__defaults__[-1] is committed_version + + +def describe_run(): + def skips_cleanly_when_no_addon(capsys): + runner = mock.Mock() + listdir = mock.Mock(return_value=["package.json"]) + assert run("dist", "Cargo.toml", runner=runner, listdir=listdir) == 0 + runner.assert_not_called() + assert "version probe skipped" in capsys.readouterr().out + + def multiple_addons_raise_with_fix_instructions(): + runner = mock.Mock() + listdir = mock.Mock(return_value=["b.node", "a.node"]) + with pytest.raises(ProbeError) as exc_info: + run("dist", "Cargo.toml", runner=runner, listdir=listdir) + message = str(exc_info.value) + assert "['a.node', 'b.node']" in message + assert "release-ci.yml" in message + runner.assert_not_called() + + def runs_the_addon_cli_version_through_node(): + runner = mock.Mock(return_value=_result(0, stdout="dirsql 0.4.72\n")) + committed = mock.Mock(return_value="0.2.7") + assert ( + run( + "dist", + "Cargo.toml", + runner=runner, + listdir=mock.Mock(return_value=[ADDON]), + committed=committed, + ) + == 0 + ) + runner.assert_called_once_with( + ["node", "-e", PRINT_VERSION, os.path.abspath(os.path.join("dist", ADDON))], + stdin=mock.ANY, + capture_output=True, + text=True, + ) + assert runner.call_args.kwargs["stdin"] is not None + committed.assert_called_once_with("Cargo.toml") + + def the_script_prints_the_cli_version_and_keeps_its_exit_code(): + assert PRINT_VERSION == ( + "process.exitCode = require(process.argv[1]).runCli(['--version'])" + ) + + def a_failing_cli_raises_with_its_output(): + runner = mock.Mock(return_value=_result(1, stdout="o", stderr="no addon")) + with pytest.raises(ProbeError) as exc_info: + _run_with(runner) + assert str(exc_info.value) == ( + "`runCli(['--version'])` failed: stdout='o', stderr='no addon'" + ) + + def the_committed_literal_raises_the_unstamped_diagnosis(): + runner = mock.Mock(return_value=_result(0, stdout="dirsql 0.2.7\n")) + with pytest.raises(ProbeError) as exc_info: + _run_with(runner) + message = str(exc_info.value) + assert message.startswith( + "the npm addon's CLI reports `dirsql 0.2.7`, the version committed in Cargo.toml" + ) + assert "did not stamp the planned version" in message + assert "putitoutthere.toml" in message + + def a_version_merely_containing_the_literal_passes(): + runner = mock.Mock(return_value=_result(0, stdout="dirsql 10.2.7\n")) + assert _run_with(runner) == 0 + + def success_reports_the_stamped_version(capsys): + runner = mock.Mock(return_value=_result(0, stdout="dirsql 0.4.72\n")) + assert _run_with(runner) == 0 + assert ( + f"ok npm-addon-version: {ADDON} reports `dirsql 0.4.72`" + in capsys.readouterr().out + ) diff --git a/internals/checks/tests/e2e/npm_addon_version_test.py b/internals/checks/tests/e2e/npm_addon_version_test.py new file mode 100644 index 000000000..7ca727898 --- /dev/null +++ b/internals/checks/tests/e2e/npm_addon_version_test.py @@ -0,0 +1,51 @@ +"""E2E test for `dirsql-checks npm-addon-version` through the real CLI. + +No mocking of any kind: spawns the packaged `dirsql-checks` console script as +a subprocess against a real dist directory. The full probe (load a built +addon, run its CLI) runs in CI's release-ci `npm-addon-version` job against +the precheck matrix's addon; here the cheap no-addon and multiple-addon +contracts are exercised end to end. +""" + +from __future__ import annotations + +import shutil +import subprocess + + +def _cli() -> str: + dirsql_checks = shutil.which("dirsql-checks") + assert dirsql_checks is not None, ( + "`dirsql-checks` console script not on PATH -- run " + "`uv run --project internals/checks pytest tests/e2e` " + "or `uv sync --project internals/checks`" + ) + return dirsql_checks + + +def describe_dirsql_checks_npm_addon_version(): + def it_skips_cleanly_when_the_dist_dir_has_no_addon(tmp_path): + (tmp_path / "package.json").write_text("{}") + + proc = subprocess.run( + [_cli(), "npm-addon-version", "--dist-dir", str(tmp_path)], + capture_output=True, + text=True, + ) + + assert proc.returncode == 0, proc.stderr + assert "version probe skipped" in proc.stdout + + def it_fails_with_fix_instructions_on_multiple_addons(tmp_path): + (tmp_path / "a.node").write_text("") + (tmp_path / "b.node").write_text("") + + proc = subprocess.run( + [_cli(), "npm-addon-version", "--dist-dir", str(tmp_path)], + capture_output=True, + text=True, + ) + + assert proc.returncode == 1 + assert "expected exactly one napi addon" in proc.stderr + assert "release-ci.yml" in proc.stderr From ae6cdd7f8d27aaa334de63cbba14ea3fece4d86c Mon Sep 17 00:00:00 2001 From: thekevinbot <248217931+thekevinbot@users.noreply.github.com> Date: Mon, 5 Oct 2026 12:42:29 -0400 Subject: [PATCH 02/11] e2e attestation for claude/1297-npm-version --- .../checks/e2e-attestations/claude-1297-npm-version.json | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 internals/checks/e2e-attestations/claude-1297-npm-version.json diff --git a/internals/checks/e2e-attestations/claude-1297-npm-version.json b/internals/checks/e2e-attestations/claude-1297-npm-version.json new file mode 100644 index 000000000..49b4adc32 --- /dev/null +++ b/internals/checks/e2e-attestations/claude-1297-npm-version.json @@ -0,0 +1,7 @@ +{ + "command": "uv run python -m pytest tests/e2e -q", + "ran_at": 1791218549, + "exit_code": 0, + "commit": "2cf45efba013acabe86b4adbc23fc8a3baa9c2df", + "branch": "claude/1297-npm-version" +} From ae3e2d5a3cca3200954fa6646ad2be0599d0b9e1 Mon Sep 17 00:00:00 2001 From: thekevinbot <248217931+thekevinbot@users.noreply.github.com> Date: Mon, 5 Oct 2026 13:01:37 -0400 Subject: [PATCH 03/11] Colocate the napi crate manifest with package.json putitoutthere stamps the planned version into a napi crate only when its Cargo.toml sits at the npm package path. Ours lived at packages/ts/napi/Cargo.toml, so the stamp was skipped and the addon's CLI reported the committed core literal 0.2.7. Moving the manifest to packages/ts/Cargo.toml lets the release stamp it and, through the path dependency, the core crate whose version the CLI prints, the same way the maturin path already does for PyPI. --- .github/workflows/dirsql-rust-ci.yml | 5 +++- .github/workflows/dirsql-typescript-ci.yml | 2 +- Cargo.toml | 2 +- agents/reference/e2e-attestation.md | 4 +-- packages/ts/{napi => }/Cargo.toml | 4 +++ .../ts/changelog.d/2026-10-05-npm-version.md | 2 ++ .../ts/migrations.d/2026-10-05-npm-version.md | 27 +++++++++++++++++++ packages/ts/package.json | 8 +++--- packages/ts/testing-conventions.toml | 7 ++--- packages/ts/tools/stage-platform.ts | 4 +-- 10 files changed, 51 insertions(+), 14 deletions(-) rename packages/ts/{napi => }/Cargo.toml (86%) create mode 100644 packages/ts/changelog.d/2026-10-05-npm-version.md create mode 100644 packages/ts/migrations.d/2026-10-05-npm-version.md diff --git a/.github/workflows/dirsql-rust-ci.yml b/.github/workflows/dirsql-rust-ci.yml index 9d389994e..a1149a6ad 100644 --- a/.github/workflows/dirsql-rust-ci.yml +++ b/.github/workflows/dirsql-rust-ci.yml @@ -1,6 +1,7 @@ name: dirsql Rust CI -# The binding crates fan in (packages/ts/napi, packages/python/src): they are +# The binding crates fan in (packages/ts/napi + packages/ts/Cargo.toml, +# packages/python/src): they are # cargo workspace members, so the workspace-wide unit/lint jobs compile them. # The root testing-conventions.toml carries this lane's config and the # [rust.coverage] floors the coverage job reads. @@ -10,6 +11,7 @@ on: paths: - 'packages/rust/**' - 'packages/ts/napi/**' + - 'packages/ts/Cargo.toml' - 'packages/python/src/**' - 'Cargo.toml' - 'Cargo.lock' @@ -22,6 +24,7 @@ on: - '!packages/rust/migrations.d/**' - '!packages/rust/README.md' - 'packages/ts/napi/**' + - 'packages/ts/Cargo.toml' - 'packages/python/src/**' - 'Cargo.toml' - 'Cargo.lock' diff --git a/.github/workflows/dirsql-typescript-ci.yml b/.github/workflows/dirsql-typescript-ci.yml index a91086a06..d30c9248c 100644 --- a/.github/workflows/dirsql-typescript-ci.yml +++ b/.github/workflows/dirsql-typescript-ci.yml @@ -113,7 +113,7 @@ jobs: build: # Builds the napi addon once and shares it with unit/binding/distcheck # below instead of each job/matrix-cell rebuilding it independently. - # The addon is N-API (napi6, see packages/ts/napi/Cargo.toml), which is + # The addon is N-API (napi6, see packages/ts/Cargo.toml), which is # ABI-stable across Node majors, so one build serves the whole Node # 20/22/24 matrix. name: Build native module diff --git a/Cargo.toml b/Cargo.toml index 928e1feef..85be71918 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["packages/python", "packages/rust", "packages/ts/napi"] +members = ["packages/python", "packages/rust", "packages/ts"] # Test-only loadable-extension fixture, built on demand by # packages/rust/tests/extensions.rs. Excluded so it isn't built/linted by # `cargo build --workspace` / `cargo clippy --workspace`. diff --git a/agents/reference/e2e-attestation.md b/agents/reference/e2e-attestation.md index cc7582007..9908fbf05 100644 --- a/agents/reference/e2e-attestation.md +++ b/agents/reference/e2e-attestation.md @@ -27,7 +27,7 @@ Receipts are append-only in practice -- a merged branch's receipt stays in the d **Everything compiled into the binding is in scope -- CI-enforced via `[e2e].extra_scope`.** The shared Rust core (`packages/rust/src`) is compiled into both bindings but lives in neither binding's subtree, and `e2e verify --scope` requires the scope to be a **descendant of the caller's `path`** -- so a binding caller cannot point its own `--scope` at the core. That case is solved by the `e2e verify --extra-scope` / `--exclude` flags (upstream #333): an `[e2e]` block declares the extra roots, `detect.py` reads it, and the reusable `e2e-verify` job passes it through. Pure config; the former bespoke `e2e_core_freshness.py` gate is not needed. Two roots per binding: - **The core, `packages/rust/src` -- puts BOTH bindings in scope** (#337). **After any shared-core change, attest each binding** (`just e2e-attest-python`, `just e2e-attest-ts`) -- CI enforces this, it is no longer a by-hand-only promise. **`cli`-only** core source (`packages/rust/src/cli/**`, `packages/rust/src/bin/**`) is feature-gated out of the binding *libraries*, but since #721 each launcher calls the core's `run_cli` **in-process through its binding** -- a CLI change alters what those suites exercise, so it demands a receipt from both bindings like any other core change (no `exclude` carve-out). -- **The binding crate itself -- puts ONLY its own package in scope** (#933): `packages/python/src` (the PyO3 glue) for python, `packages/ts/napi` (the napi-rs crate, manifest and `build.rs` included) for typescript. Same #721 argument one layer out: `run_cli` in each binding does argv framing, GIL detach / exit-code handling, and runs in every e2e case, so a binding-only diff changes what the suites exercise with no core change at all. A napi change has no bearing on the python suite and never demands a python receipt, and vice versa. +- **The binding crate itself -- puts ONLY its own package in scope** (#933): `packages/python/src` (the PyO3 glue) for python, `packages/ts/napi` plus `packages/ts/Cargo.toml` (the napi-rs crate, `build.rs` included; the manifest sits at the package root so the release stamps its version) for typescript. Same #721 argument one layer out: `run_cli` in each binding does argv framing, GIL detach / exit-code handling, and runs in every e2e case, so a binding-only diff changes what the suites exercise with no core change at all. A napi change has no bearing on the python suite and never demands a python receipt, and vice versa. **That per-package split is why `packages/python` and `packages/ts` each carry their own `testing-conventions.toml`** (passed explicitly as the lane's `config:`, the pattern `internals/checks` established in #550). `[e2e]` is a single global table applied to every caller reading a given config, so "core + *my* binding" is not expressible in one shared file: declaring both bindings at the repo root would make a napi change demand a python receipt (and force `packages/ts/napi/**` into the python lane's path triggers to enforce it at all). The root `[e2e]` block stays as the default for a caller with no config of its own. @@ -41,7 +41,7 @@ Agents must run the full e2e suite locally before any `git push` that includes a **"Substantial" means any change touching:** - `packages/rust/**` (Rust core) -- `packages/ts/napi/**` (napi-rs binding crate) +- `packages/ts/napi/**` and `packages/ts/Cargo.toml` (napi-rs binding crate) - `packages/python/src/**` (excluding files matching `*_test.py`) - `packages/ts/src/**` (excluding files matching `*.test.ts` / `*.spec.ts`) - Any shared SDK runtime code reachable from the above diff --git a/packages/ts/napi/Cargo.toml b/packages/ts/Cargo.toml similarity index 86% rename from packages/ts/napi/Cargo.toml rename to packages/ts/Cargo.toml index 7047e651d..b53bee232 100644 --- a/packages/ts/napi/Cargo.toml +++ b/packages/ts/Cargo.toml @@ -3,8 +3,12 @@ name = "dirsql-napi" version.workspace = true edition.workspace = true publish = false +# Colocated with package.json because the release only stamps the planned +# version into a napi crate whose manifest sits at the npm package path. +build = "napi/build.rs" [lib] +path = "napi/src/lib.rs" crate-type = ["cdylib"] [dependencies] diff --git a/packages/ts/changelog.d/2026-10-05-npm-version.md b/packages/ts/changelog.d/2026-10-05-npm-version.md new file mode 100644 index 000000000..6e1e89449 --- /dev/null +++ b/packages/ts/changelog.d/2026-10-05-npm-version.md @@ -0,0 +1,2 @@ +**Fixed** `npx dirsql --version` reports the npm package's version. It used to +print `0.2.7`, the unreleased crate version, on every release. diff --git a/packages/ts/migrations.d/2026-10-05-npm-version.md b/packages/ts/migrations.d/2026-10-05-npm-version.md new file mode 100644 index 000000000..275f5c6d4 --- /dev/null +++ b/packages/ts/migrations.d/2026-10-05-npm-version.md @@ -0,0 +1,27 @@ +### npm: `dirsql --version` reports the published version + +#### Summary + +The npm CLI's `--version` printed `dirsql 0.2.7` whatever version was +installed. It now prints the version of the installed `dirsql` package, as +the PyPI build already did. Nothing breaks. + +#### Required changes + +_None._ + +#### Deprecations removed + +_None._ + +#### Behavior changes without code changes + +- `npx dirsql --version` prints `dirsql ` instead of + `dirsql 0.2.7`. + +#### Verification + +```sh +npx dirsql@latest --version +# expected: dirsql +``` diff --git a/packages/ts/package.json b/packages/ts/package.json index 95788a0c1..5d23f9fa5 100644 --- a/packages/ts/package.json +++ b/packages/ts/package.json @@ -44,7 +44,7 @@ }, "wireit": { "napi:build": { - "command": "napi build --release --manifest-path napi/Cargo.toml --output-dir .", + "command": "napi build --release --manifest-path Cargo.toml --output-dir .", "output": ["dirsql.node", "index.d.ts"] }, "tsc": { @@ -66,7 +66,7 @@ "src/platforms.json", "napi/src/**/*.rs", "napi/build.rs", - "napi/Cargo.toml", + "Cargo.toml", "../rust/src/**/*.rs", "../rust/Cargo.toml" ], @@ -76,11 +76,11 @@ "dependencies": ["napi:build", "tsc", "stage:platform"] }, "build:debug": { - "command": "napi build --manifest-path napi/Cargo.toml --output-dir . && tsc -p tsconfig.build.json", + "command": "napi build --manifest-path Cargo.toml --output-dir . && tsc -p tsconfig.build.json", "files": [ "napi/src/**/*.rs", "napi/build.rs", - "napi/Cargo.toml", + "Cargo.toml", "../rust/src/**/*.rs", "src/**/*.ts", "src/**/*.json", diff --git a/packages/ts/testing-conventions.toml b/packages/ts/testing-conventions.toml index f952e4388..515e16764 100644 --- a/packages/ts/testing-conventions.toml +++ b/packages/ts/testing-conventions.toml @@ -4,11 +4,12 @@ # start staling this package. # # The napi crate is in scope because the npm launcher runs the CLI in-process -# through it (#721), so every e2e case crosses it -- the whole crate dir, since -# Cargo.toml and build.rs decide what lands in the addon. packages/python/src is +# through it (#721), so every e2e case crosses it -- the whole crate dir plus its +# manifest at the package root, since Cargo.toml and build.rs decide what lands +# in the addon. packages/python/src is # not: it cannot reach this suite. [e2e] -extra_scope = ["packages/rust/src", "packages/ts/napi"] +extra_scope = ["packages/rust/src", "packages/ts/napi", "packages/ts/Cargo.toml"] # The live TypeScript floors; the root file carries no `[typescript]` table. [typescript.coverage] diff --git a/packages/ts/tools/stage-platform.ts b/packages/ts/tools/stage-platform.ts index aed9275b7..2eec6fb31 100644 --- a/packages/ts/tools/stage-platform.ts +++ b/packages/ts/tools/stage-platform.ts @@ -138,7 +138,7 @@ function stageOne(args: StageOneArgs): StageResult["staged"][number] { // instead of `dirsql.node`. Without it the cross-build's output // collides with the host's napi:build file at the package root. // `--manifest-path`/`--output-dir` point napi at the colocated - // `dirsql-napi` crate (packages/ts/napi) while still dropping the + // `dirsql-napi` crate (packages/ts/Cargo.toml) while still dropping the // artifact into this package (packages/ts) root. const cross = spawn( "npx", @@ -148,7 +148,7 @@ function stageOne(args: StageOneArgs): StageResult["staged"][number] { "--release", "--platform", "--manifest-path", - "napi/Cargo.toml", + "Cargo.toml", "--output-dir", ".", "--target", From 983d64bc8507c9d75e940a941ae550f012ac9249 Mon Sep 17 00:00:00 2001 From: thekevinbot <248217931+thekevinbot@users.noreply.github.com> Date: Mon, 5 Oct 2026 13:02:02 -0400 Subject: [PATCH 04/11] e2e attestation for claude/1297-npm-version --- packages/ts/e2e-attestations/claude-1297-npm-version.json | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 packages/ts/e2e-attestations/claude-1297-npm-version.json diff --git a/packages/ts/e2e-attestations/claude-1297-npm-version.json b/packages/ts/e2e-attestations/claude-1297-npm-version.json new file mode 100644 index 000000000..68f4227eb --- /dev/null +++ b/packages/ts/e2e-attestations/claude-1297-npm-version.json @@ -0,0 +1,7 @@ +{ + "command": "pnpm test:e2e", + "ran_at": 1791219722, + "exit_code": 0, + "commit": "ae3e2d5a3cca3200954fa6646ad2be0599d0b9e1", + "branch": "claude/1297-npm-version" +} From 56c960fed47a8a0768b4b643b94f7a773521e7ec Mon Sep 17 00:00:00 2001 From: thekevinbot <248217931+thekevinbot@users.noreply.github.com> Date: Mon, 5 Oct 2026 13:21:18 -0400 Subject: [PATCH 05/11] Pin the npm addon version probe to an exact match --- internals/checks/src/checks/npm_addon_version/gate_test.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/internals/checks/src/checks/npm_addon_version/gate_test.py b/internals/checks/src/checks/npm_addon_version/gate_test.py index 81faba5e8..c2094dcba 100644 --- a/internals/checks/src/checks/npm_addon_version/gate_test.py +++ b/internals/checks/src/checks/npm_addon_version/gate_test.py @@ -115,6 +115,10 @@ def a_version_merely_containing_the_literal_passes(): runner = mock.Mock(return_value=_result(0, stdout="dirsql 10.2.7\n")) assert _run_with(runner) == 0 + def a_version_sorting_before_the_literal_passes(): + runner = mock.Mock(return_value=_result(0, stdout="dirsql 0.10.0\n")) + assert _run_with(runner) == 0 + def success_reports_the_stamped_version(capsys): runner = mock.Mock(return_value=_result(0, stdout="dirsql 0.4.72\n")) assert _run_with(runner) == 0 From 2dd2b6eceb909c12a711d71cd86c72dd4e37fa4c Mon Sep 17 00:00:00 2001 From: thekevinbot <248217931+thekevinbot@users.noreply.github.com> Date: Mon, 5 Oct 2026 14:19:34 -0400 Subject: [PATCH 06/11] Assert the napi manifest sits at the npm package path Replaces the npm addon version probe with the one invariant the release depends on, and returns every other file to main so the assertion runs against main's layout. --- .github/workflows/dirsql-rust-ci.yml | 5 +- .github/workflows/dirsql-typescript-ci.yml | 2 +- .github/workflows/release-ci.yml | 30 ---- Cargo.toml | 2 +- agents/reference/e2e-attestation.md | 4 +- .../claude-1297-npm-version.json | 7 - internals/checks/src/checks/cli.py | 2 - internals/checks/src/checks/cli_test.py | 4 - .../src/checks/npm_addon_version/__init__.py | 0 .../checks/npm_addon_version/addon_names.py | 7 - .../npm_addon_version/addon_names_test.py | 10 -- .../src/checks/npm_addon_version/cli.py | 31 ----- .../src/checks/npm_addon_version/cli_test.py | 36 ----- .../npm_addon_version/committed_version.py | 11 -- .../committed_version_test.py | 15 -- .../src/checks/npm_addon_version/gate.py | 72 ---------- .../src/checks/npm_addon_version/gate_test.py | 128 ------------------ .../tests/e2e/npm_addon_version_test.py | 51 ------- .../tests/integration/release_globs_test.py | 14 ++ .../ts/changelog.d/2026-10-05-npm-version.md | 2 - .../claude-1297-npm-version.json | 7 - .../ts/migrations.d/2026-10-05-npm-version.md | 27 ---- packages/ts/{ => napi}/Cargo.toml | 4 - packages/ts/package.json | 8 +- packages/ts/testing-conventions.toml | 7 +- packages/ts/tools/stage-platform.ts | 4 +- 26 files changed, 28 insertions(+), 462 deletions(-) delete mode 100644 internals/checks/e2e-attestations/claude-1297-npm-version.json delete mode 100644 internals/checks/src/checks/npm_addon_version/__init__.py delete mode 100644 internals/checks/src/checks/npm_addon_version/addon_names.py delete mode 100644 internals/checks/src/checks/npm_addon_version/addon_names_test.py delete mode 100644 internals/checks/src/checks/npm_addon_version/cli.py delete mode 100644 internals/checks/src/checks/npm_addon_version/cli_test.py delete mode 100644 internals/checks/src/checks/npm_addon_version/committed_version.py delete mode 100644 internals/checks/src/checks/npm_addon_version/committed_version_test.py delete mode 100644 internals/checks/src/checks/npm_addon_version/gate.py delete mode 100644 internals/checks/src/checks/npm_addon_version/gate_test.py delete mode 100644 internals/checks/tests/e2e/npm_addon_version_test.py delete mode 100644 packages/ts/changelog.d/2026-10-05-npm-version.md delete mode 100644 packages/ts/e2e-attestations/claude-1297-npm-version.json delete mode 100644 packages/ts/migrations.d/2026-10-05-npm-version.md rename packages/ts/{ => napi}/Cargo.toml (86%) diff --git a/.github/workflows/dirsql-rust-ci.yml b/.github/workflows/dirsql-rust-ci.yml index a1149a6ad..9d389994e 100644 --- a/.github/workflows/dirsql-rust-ci.yml +++ b/.github/workflows/dirsql-rust-ci.yml @@ -1,7 +1,6 @@ name: dirsql Rust CI -# The binding crates fan in (packages/ts/napi + packages/ts/Cargo.toml, -# packages/python/src): they are +# The binding crates fan in (packages/ts/napi, packages/python/src): they are # cargo workspace members, so the workspace-wide unit/lint jobs compile them. # The root testing-conventions.toml carries this lane's config and the # [rust.coverage] floors the coverage job reads. @@ -11,7 +10,6 @@ on: paths: - 'packages/rust/**' - 'packages/ts/napi/**' - - 'packages/ts/Cargo.toml' - 'packages/python/src/**' - 'Cargo.toml' - 'Cargo.lock' @@ -24,7 +22,6 @@ on: - '!packages/rust/migrations.d/**' - '!packages/rust/README.md' - 'packages/ts/napi/**' - - 'packages/ts/Cargo.toml' - 'packages/python/src/**' - 'Cargo.toml' - 'Cargo.lock' diff --git a/.github/workflows/dirsql-typescript-ci.yml b/.github/workflows/dirsql-typescript-ci.yml index d30c9248c..a91086a06 100644 --- a/.github/workflows/dirsql-typescript-ci.yml +++ b/.github/workflows/dirsql-typescript-ci.yml @@ -113,7 +113,7 @@ jobs: build: # Builds the napi addon once and shares it with unit/binding/distcheck # below instead of each job/matrix-cell rebuilding it independently. - # The addon is N-API (napi6, see packages/ts/Cargo.toml), which is + # The addon is N-API (napi6, see packages/ts/napi/Cargo.toml), which is # ABI-stable across Node majors, so one build serves the whole Node # 20/22/24 matrix. name: Build native module diff --git a/.github/workflows/release-ci.yml b/.github/workflows/release-ci.yml index 4c4f54be9..e8050e9db 100644 --- a/.github/workflows/release-ci.yml +++ b/.github/workflows/release-ci.yml @@ -125,33 +125,3 @@ jobs: - name: Probe extension loading through the installed wheel run: uv run --project internals/checks dirsql-checks wheel-extension-load --dist-dir dist/ - - # Every other tier builds the addon from the committed manifests, so only - # the release-shape build can show whether the planned version reached the - # crate the CLI reads its `--version` from. Skips cleanly when the plan - # built no dirsql-npm addon for this PR. - npm-addon-version: - name: npm addon version probe - needs: precheck - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 - - - uses: actions/download-artifact@v8 - with: - pattern: dirsql-npm-linux-x64-gnu - path: dist/ - merge-multiple: true - - - name: Install Node - uses: actions/setup-node@v7 - with: - node-version: "24" - - - name: Install uv - uses: astral-sh/setup-uv@v7 - with: - python-version: "3.13" - - - name: Assert the addon's CLI reports the stamped release version - run: uv run --project internals/checks dirsql-checks npm-addon-version --dist-dir dist/ diff --git a/Cargo.toml b/Cargo.toml index 85be71918..928e1feef 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["packages/python", "packages/rust", "packages/ts"] +members = ["packages/python", "packages/rust", "packages/ts/napi"] # Test-only loadable-extension fixture, built on demand by # packages/rust/tests/extensions.rs. Excluded so it isn't built/linted by # `cargo build --workspace` / `cargo clippy --workspace`. diff --git a/agents/reference/e2e-attestation.md b/agents/reference/e2e-attestation.md index 9908fbf05..cc7582007 100644 --- a/agents/reference/e2e-attestation.md +++ b/agents/reference/e2e-attestation.md @@ -27,7 +27,7 @@ Receipts are append-only in practice -- a merged branch's receipt stays in the d **Everything compiled into the binding is in scope -- CI-enforced via `[e2e].extra_scope`.** The shared Rust core (`packages/rust/src`) is compiled into both bindings but lives in neither binding's subtree, and `e2e verify --scope` requires the scope to be a **descendant of the caller's `path`** -- so a binding caller cannot point its own `--scope` at the core. That case is solved by the `e2e verify --extra-scope` / `--exclude` flags (upstream #333): an `[e2e]` block declares the extra roots, `detect.py` reads it, and the reusable `e2e-verify` job passes it through. Pure config; the former bespoke `e2e_core_freshness.py` gate is not needed. Two roots per binding: - **The core, `packages/rust/src` -- puts BOTH bindings in scope** (#337). **After any shared-core change, attest each binding** (`just e2e-attest-python`, `just e2e-attest-ts`) -- CI enforces this, it is no longer a by-hand-only promise. **`cli`-only** core source (`packages/rust/src/cli/**`, `packages/rust/src/bin/**`) is feature-gated out of the binding *libraries*, but since #721 each launcher calls the core's `run_cli` **in-process through its binding** -- a CLI change alters what those suites exercise, so it demands a receipt from both bindings like any other core change (no `exclude` carve-out). -- **The binding crate itself -- puts ONLY its own package in scope** (#933): `packages/python/src` (the PyO3 glue) for python, `packages/ts/napi` plus `packages/ts/Cargo.toml` (the napi-rs crate, `build.rs` included; the manifest sits at the package root so the release stamps its version) for typescript. Same #721 argument one layer out: `run_cli` in each binding does argv framing, GIL detach / exit-code handling, and runs in every e2e case, so a binding-only diff changes what the suites exercise with no core change at all. A napi change has no bearing on the python suite and never demands a python receipt, and vice versa. +- **The binding crate itself -- puts ONLY its own package in scope** (#933): `packages/python/src` (the PyO3 glue) for python, `packages/ts/napi` (the napi-rs crate, manifest and `build.rs` included) for typescript. Same #721 argument one layer out: `run_cli` in each binding does argv framing, GIL detach / exit-code handling, and runs in every e2e case, so a binding-only diff changes what the suites exercise with no core change at all. A napi change has no bearing on the python suite and never demands a python receipt, and vice versa. **That per-package split is why `packages/python` and `packages/ts` each carry their own `testing-conventions.toml`** (passed explicitly as the lane's `config:`, the pattern `internals/checks` established in #550). `[e2e]` is a single global table applied to every caller reading a given config, so "core + *my* binding" is not expressible in one shared file: declaring both bindings at the repo root would make a napi change demand a python receipt (and force `packages/ts/napi/**` into the python lane's path triggers to enforce it at all). The root `[e2e]` block stays as the default for a caller with no config of its own. @@ -41,7 +41,7 @@ Agents must run the full e2e suite locally before any `git push` that includes a **"Substantial" means any change touching:** - `packages/rust/**` (Rust core) -- `packages/ts/napi/**` and `packages/ts/Cargo.toml` (napi-rs binding crate) +- `packages/ts/napi/**` (napi-rs binding crate) - `packages/python/src/**` (excluding files matching `*_test.py`) - `packages/ts/src/**` (excluding files matching `*.test.ts` / `*.spec.ts`) - Any shared SDK runtime code reachable from the above diff --git a/internals/checks/e2e-attestations/claude-1297-npm-version.json b/internals/checks/e2e-attestations/claude-1297-npm-version.json deleted file mode 100644 index 49b4adc32..000000000 --- a/internals/checks/e2e-attestations/claude-1297-npm-version.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "command": "uv run python -m pytest tests/e2e -q", - "ran_at": 1791218549, - "exit_code": 0, - "commit": "2cf45efba013acabe86b4adbc23fc8a3baa9c2df", - "branch": "claude/1297-npm-version" -} diff --git a/internals/checks/src/checks/cli.py b/internals/checks/src/checks/cli.py index db278c080..198cdcd94 100644 --- a/internals/checks/src/checks/cli.py +++ b/internals/checks/src/checks/cli.py @@ -12,7 +12,6 @@ from checks.attestation_guard.cli import cli as attestation_guard from checks.changelog_gate.cli import cli as changelog_gate from checks.declared_deps.cli import cli as declared_deps -from checks.npm_addon_version.cli import cli as npm_addon_version from checks.preflight.cli import cli as preflight from checks.release_globs.cli import cli as release_globs from checks.wheel_extension_load.cli import cli as wheel_extension_load @@ -27,7 +26,6 @@ def main() -> None: main.add_command(attestation_guard, name="attestation-guard") main.add_command(changelog_gate, name="changelog-gate") main.add_command(declared_deps, name="declared-deps") -main.add_command(npm_addon_version, name="npm-addon-version") main.add_command(preflight, name="preflight") main.add_command(release_globs, name="release-globs") main.add_command(wheel_extension_load, name="wheel-extension-load") diff --git a/internals/checks/src/checks/cli_test.py b/internals/checks/src/checks/cli_test.py index d9812ed38..914151384 100644 --- a/internals/checks/src/checks/cli_test.py +++ b/internals/checks/src/checks/cli_test.py @@ -32,7 +32,3 @@ def test_preflight_is_registered(): def test_release_globs_is_registered(): assert "release-globs" in main.commands - - -def test_npm_addon_version_is_registered(): - assert "npm-addon-version" in main.commands diff --git a/internals/checks/src/checks/npm_addon_version/__init__.py b/internals/checks/src/checks/npm_addon_version/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/internals/checks/src/checks/npm_addon_version/addon_names.py b/internals/checks/src/checks/npm_addon_version/addon_names.py deleted file mode 100644 index c04e23d9c..000000000 --- a/internals/checks/src/checks/npm_addon_version/addon_names.py +++ /dev/null @@ -1,7 +0,0 @@ -"""The napi addons among a dist directory's entries.""" - -from __future__ import annotations - - -def addon_names(names) -> list[str]: - return sorted(name for name in names if name.endswith(".node")) diff --git a/internals/checks/src/checks/npm_addon_version/addon_names_test.py b/internals/checks/src/checks/npm_addon_version/addon_names_test.py deleted file mode 100644 index 9a5731a50..000000000 --- a/internals/checks/src/checks/npm_addon_version/addon_names_test.py +++ /dev/null @@ -1,10 +0,0 @@ -from checks.npm_addon_version.addon_names import addon_names - - -def describe_addon_names(): - def filters_to_addons_sorted(): - names = ["b.node", "package.json", "a.node"] - assert addon_names(names) == ["a.node", "b.node"] - - def empty_when_no_addons(): - assert addon_names(["dirsql.whl", "notes.txt"]) == [] diff --git a/internals/checks/src/checks/npm_addon_version/cli.py b/internals/checks/src/checks/npm_addon_version/cli.py deleted file mode 100644 index f6f8a59ff..000000000 --- a/internals/checks/src/checks/npm_addon_version/cli.py +++ /dev/null @@ -1,31 +0,0 @@ -"""The npm-addon-version check -- repo-only. - -Backs `dirsql-checks npm-addon-version`: probes that the release-built napi -addon's CLI reports the stamped release version, not the committed literal. -""" - -from __future__ import annotations - -import click - -from checks.npm_addon_version.gate import ProbeError, run - - -@click.command() -@click.option( - "--dist-dir", - required=True, - help="Directory containing the built napi addon to probe (may be empty/absent).", -) -@click.option( - "--manifest", - default="packages/rust/Cargo.toml", - help="Manifest of the crate whose version the CLI prints.", -) -def cli(dist_dir: str, manifest: str) -> None: - try: - code = run(dist_dir, manifest) - except ProbeError as err: - click.echo(f"npm-addon-version: {err}", err=True) - raise SystemExit(1) from err - raise SystemExit(code) diff --git a/internals/checks/src/checks/npm_addon_version/cli_test.py b/internals/checks/src/checks/npm_addon_version/cli_test.py deleted file mode 100644 index 859a7cf35..000000000 --- a/internals/checks/src/checks/npm_addon_version/cli_test.py +++ /dev/null @@ -1,36 +0,0 @@ -"""Colocated unit tests for the npm-addon-version command (isolation -- no -`CliRunner`). Driven through `.callback`; `run` is mocked at its import site. -""" - -from unittest import mock - -import pytest - -from checks.npm_addon_version.cli import ProbeError, cli - - -def test_exits_with_runs_return_code(): - with mock.patch("checks.npm_addon_version.cli.run", return_value=0) as run: - with pytest.raises(SystemExit) as exc_info: - cli.callback(dist_dir="dist/", manifest="Cargo.toml") - run.assert_called_once_with("dist/", "Cargo.toml") - assert exc_info.value.code == 0 - - -def test_probe_error_prints_diagnostic_and_exits_one(capsys): - with mock.patch( - "checks.npm_addon_version.cli.run", - side_effect=ProbeError("unstamped"), - ): - with pytest.raises(SystemExit) as exc_info: - cli.callback(dist_dir="dist/", manifest="Cargo.toml") - assert exc_info.value.code == 1 - assert "npm-addon-version: unstamped" in capsys.readouterr().err - - -def test_declares_a_required_dist_dir_and_a_defaulted_manifest(): - dist_dir, manifest = cli.params - assert dist_dir.name == "dist_dir" - assert dist_dir.required is True - assert manifest.name == "manifest" - assert manifest.default == "packages/rust/Cargo.toml" diff --git a/internals/checks/src/checks/npm_addon_version/committed_version.py b/internals/checks/src/checks/npm_addon_version/committed_version.py deleted file mode 100644 index cf2321297..000000000 --- a/internals/checks/src/checks/npm_addon_version/committed_version.py +++ /dev/null @@ -1,11 +0,0 @@ -"""The `[package].version` literal committed in a crate manifest.""" - -from __future__ import annotations - -from collections.abc import Callable - -from ..files.read_config import read_config - - -def committed_version(manifest: str, config: Callable[[str], dict] = read_config) -> str: - return config(manifest)["package"]["version"] diff --git a/internals/checks/src/checks/npm_addon_version/committed_version_test.py b/internals/checks/src/checks/npm_addon_version/committed_version_test.py deleted file mode 100644 index 48c86e298..000000000 --- a/internals/checks/src/checks/npm_addon_version/committed_version_test.py +++ /dev/null @@ -1,15 +0,0 @@ -from unittest import mock - -from checks.npm_addon_version.committed_version import committed_version - - -def describe_committed_version(): - def reads_the_package_version(): - config = mock.Mock(return_value={"package": {"version": "0.2.7"}}) - assert committed_version("Cargo.toml", config) == "0.2.7" - config.assert_called_once_with("Cargo.toml") - - def it_defaults_to_the_real_toml_reader(tmp_path): - manifest = tmp_path / "Cargo.toml" - manifest.write_text('[package]\nname = "x"\nversion = "1.2.3"\n') - assert committed_version(str(manifest)) == "1.2.3" diff --git a/internals/checks/src/checks/npm_addon_version/gate.py b/internals/checks/src/checks/npm_addon_version/gate.py deleted file mode 100644 index d80891dca..000000000 --- a/internals/checks/src/checks/npm_addon_version/gate.py +++ /dev/null @@ -1,72 +0,0 @@ -"""Probe the version the release-built npm addon's CLI reports. - -Loads the Release Precheck matrix's linux-x64 napi addon -- built by the same -`_matrix.yml` as the published one -- and runs its `runCli(["--version"])`. The -pipeline must stamp the planned release version into the crate whose -`CARGO_PKG_VERSION` the CLI prints. Every planned version is newer than the -literal committed in that crate's manifest, so an addon still reporting the -literal was built without the stamp. -""" - -from __future__ import annotations - -import os -import subprocess -from collections.abc import Callable - -from checks.probe.probe_error import ProbeError -from checks.probe.require_zero import require_zero -from checks.wheel_extension_load.list_names import list_names - -from .addon_names import addon_names -from .committed_version import committed_version - -PRINT_VERSION = "process.exitCode = require(process.argv[1]).runCli(['--version'])" - - -def run( - dist_dir: str, - manifest: str, - runner=subprocess.run, - listdir=os.listdir, - committed: Callable[[str], str] = committed_version, -) -> int: - addons = addon_names(list_names(dist_dir, listdir)) - if len(addons) > 1: - raise ProbeError( - f"expected exactly one napi addon to probe, saw {addons}. " - "Tighten the download-artifact pattern in release-ci.yml so only " - "the linux-x64 addon lands in the probe's dist dir." - ) - if not addons: - print( - f"No napi addon under {dist_dir} -- the precheck matrix planned no " - "dirsql-npm build for this PR; version probe skipped." - ) - return 0 - (addon_name,) = addons - addon = os.path.abspath(os.path.join(dist_dir, addon_name)) - - probe = runner( - ["node", "-e", PRINT_VERSION, addon], - stdin=subprocess.DEVNULL, - capture_output=True, - text=True, - ) - require_zero( - probe, - f"`runCli(['--version'])` failed: stdout={probe.stdout!r}, stderr={probe.stderr!r}", - ) - reported = probe.stdout.strip() - literal = committed(manifest) - if reported == f"dirsql {literal}": - raise ProbeError( - f"the npm addon's CLI reports `{reported}`, the version committed in " - f"{manifest}: the release pipeline did not stamp the planned version " - "into the crate the CLI reads its version from, so `npx dirsql " - "--version` would misreport every release. putitoutthere stamps a " - "napi build only when the crate's Cargo.toml sits at the npm " - "package's `path` in putitoutthere.toml." - ) - print(f"ok npm-addon-version: {addon_name} reports `{reported}`") - return 0 diff --git a/internals/checks/src/checks/npm_addon_version/gate_test.py b/internals/checks/src/checks/npm_addon_version/gate_test.py deleted file mode 100644 index c2094dcba..000000000 --- a/internals/checks/src/checks/npm_addon_version/gate_test.py +++ /dev/null @@ -1,128 +0,0 @@ -import os -from unittest import mock - -import pytest - -from checks.npm_addon_version.gate import ( - PRINT_VERSION, - ProbeError, - addon_names, - committed_version, - list_names, - require_zero, - run, -) - -ADDON = "dirsql.linux-x64-gnu.node" - - -def _result(returncode=0, stdout="", stderr=""): - return mock.Mock(returncode=returncode, stdout=stdout, stderr=stderr) - - -def _run_with(runner, literal="0.2.7"): - return run( - "dist", - "Cargo.toml", - runner=runner, - listdir=mock.Mock(return_value=[ADDON]), - committed=mock.Mock(return_value=literal), - ) - - -def describe_collaborators(): - def it_resolves_the_probe_error_from_the_shared_probe_package(): - assert ProbeError.__module__ == "checks.probe.probe_error" - - def it_resolves_require_zero_from_the_shared_probe_package(): - assert require_zero.__module__ == "checks.probe.require_zero" - - def it_resolves_list_names_from_the_wheel_probe(): - assert list_names.__module__ == "checks.wheel_extension_load.list_names" - - def it_resolves_addon_names_from_its_own_module(): - assert addon_names.__module__ == "checks.npm_addon_version.addon_names" - - def it_defaults_to_reading_the_committed_version(): - assert run.__defaults__[-1] is committed_version - - -def describe_run(): - def skips_cleanly_when_no_addon(capsys): - runner = mock.Mock() - listdir = mock.Mock(return_value=["package.json"]) - assert run("dist", "Cargo.toml", runner=runner, listdir=listdir) == 0 - runner.assert_not_called() - assert "version probe skipped" in capsys.readouterr().out - - def multiple_addons_raise_with_fix_instructions(): - runner = mock.Mock() - listdir = mock.Mock(return_value=["b.node", "a.node"]) - with pytest.raises(ProbeError) as exc_info: - run("dist", "Cargo.toml", runner=runner, listdir=listdir) - message = str(exc_info.value) - assert "['a.node', 'b.node']" in message - assert "release-ci.yml" in message - runner.assert_not_called() - - def runs_the_addon_cli_version_through_node(): - runner = mock.Mock(return_value=_result(0, stdout="dirsql 0.4.72\n")) - committed = mock.Mock(return_value="0.2.7") - assert ( - run( - "dist", - "Cargo.toml", - runner=runner, - listdir=mock.Mock(return_value=[ADDON]), - committed=committed, - ) - == 0 - ) - runner.assert_called_once_with( - ["node", "-e", PRINT_VERSION, os.path.abspath(os.path.join("dist", ADDON))], - stdin=mock.ANY, - capture_output=True, - text=True, - ) - assert runner.call_args.kwargs["stdin"] is not None - committed.assert_called_once_with("Cargo.toml") - - def the_script_prints_the_cli_version_and_keeps_its_exit_code(): - assert PRINT_VERSION == ( - "process.exitCode = require(process.argv[1]).runCli(['--version'])" - ) - - def a_failing_cli_raises_with_its_output(): - runner = mock.Mock(return_value=_result(1, stdout="o", stderr="no addon")) - with pytest.raises(ProbeError) as exc_info: - _run_with(runner) - assert str(exc_info.value) == ( - "`runCli(['--version'])` failed: stdout='o', stderr='no addon'" - ) - - def the_committed_literal_raises_the_unstamped_diagnosis(): - runner = mock.Mock(return_value=_result(0, stdout="dirsql 0.2.7\n")) - with pytest.raises(ProbeError) as exc_info: - _run_with(runner) - message = str(exc_info.value) - assert message.startswith( - "the npm addon's CLI reports `dirsql 0.2.7`, the version committed in Cargo.toml" - ) - assert "did not stamp the planned version" in message - assert "putitoutthere.toml" in message - - def a_version_merely_containing_the_literal_passes(): - runner = mock.Mock(return_value=_result(0, stdout="dirsql 10.2.7\n")) - assert _run_with(runner) == 0 - - def a_version_sorting_before_the_literal_passes(): - runner = mock.Mock(return_value=_result(0, stdout="dirsql 0.10.0\n")) - assert _run_with(runner) == 0 - - def success_reports_the_stamped_version(capsys): - runner = mock.Mock(return_value=_result(0, stdout="dirsql 0.4.72\n")) - assert _run_with(runner) == 0 - assert ( - f"ok npm-addon-version: {ADDON} reports `dirsql 0.4.72`" - in capsys.readouterr().out - ) diff --git a/internals/checks/tests/e2e/npm_addon_version_test.py b/internals/checks/tests/e2e/npm_addon_version_test.py deleted file mode 100644 index 7ca727898..000000000 --- a/internals/checks/tests/e2e/npm_addon_version_test.py +++ /dev/null @@ -1,51 +0,0 @@ -"""E2E test for `dirsql-checks npm-addon-version` through the real CLI. - -No mocking of any kind: spawns the packaged `dirsql-checks` console script as -a subprocess against a real dist directory. The full probe (load a built -addon, run its CLI) runs in CI's release-ci `npm-addon-version` job against -the precheck matrix's addon; here the cheap no-addon and multiple-addon -contracts are exercised end to end. -""" - -from __future__ import annotations - -import shutil -import subprocess - - -def _cli() -> str: - dirsql_checks = shutil.which("dirsql-checks") - assert dirsql_checks is not None, ( - "`dirsql-checks` console script not on PATH -- run " - "`uv run --project internals/checks pytest tests/e2e` " - "or `uv sync --project internals/checks`" - ) - return dirsql_checks - - -def describe_dirsql_checks_npm_addon_version(): - def it_skips_cleanly_when_the_dist_dir_has_no_addon(tmp_path): - (tmp_path / "package.json").write_text("{}") - - proc = subprocess.run( - [_cli(), "npm-addon-version", "--dist-dir", str(tmp_path)], - capture_output=True, - text=True, - ) - - assert proc.returncode == 0, proc.stderr - assert "version probe skipped" in proc.stdout - - def it_fails_with_fix_instructions_on_multiple_addons(tmp_path): - (tmp_path / "a.node").write_text("") - (tmp_path / "b.node").write_text("") - - proc = subprocess.run( - [_cli(), "npm-addon-version", "--dist-dir", str(tmp_path)], - capture_output=True, - text=True, - ) - - assert proc.returncode == 1 - assert "expected exactly one napi addon" in proc.stderr - assert "release-ci.yml" in proc.stderr diff --git a/internals/checks/tests/integration/release_globs_test.py b/internals/checks/tests/integration/release_globs_test.py index 61f5d9f85..ca4786ad2 100644 --- a/internals/checks/tests/integration/release_globs_test.py +++ b/internals/checks/tests/integration/release_globs_test.py @@ -13,6 +13,9 @@ from __future__ import annotations +import tomllib +from pathlib import Path + import pytest from checks.release_globs.carve_out import carve_out @@ -81,3 +84,14 @@ def it_rejects_a_leading_bang_negation_in_the_publish_globs(tmp_path, capsys, ne globs = [*carve_out("packages/rust"), negation] assert run(*write(tmp_path, globs)) == 1 assert "leading-`!` negation" in capsys.readouterr().out + + +def describe_the_real_release_config(): + def it_keeps_the_napi_manifest_at_the_npm_package_path(): + # The release stamps the planned version into a napi crate only when its + # Cargo.toml sits at the npm package's `path`; elsewhere the CLI reports + # the committed literal. + repo = Path(__file__).resolve().parents[4] + config = tomllib.loads((repo / "putitoutthere.toml").read_text()) + (npm,) = [package for package in config["package"] if package["kind"] == "npm"] + assert (repo / npm["path"] / "Cargo.toml").is_file() diff --git a/packages/ts/changelog.d/2026-10-05-npm-version.md b/packages/ts/changelog.d/2026-10-05-npm-version.md deleted file mode 100644 index 6e1e89449..000000000 --- a/packages/ts/changelog.d/2026-10-05-npm-version.md +++ /dev/null @@ -1,2 +0,0 @@ -**Fixed** `npx dirsql --version` reports the npm package's version. It used to -print `0.2.7`, the unreleased crate version, on every release. diff --git a/packages/ts/e2e-attestations/claude-1297-npm-version.json b/packages/ts/e2e-attestations/claude-1297-npm-version.json deleted file mode 100644 index 68f4227eb..000000000 --- a/packages/ts/e2e-attestations/claude-1297-npm-version.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "command": "pnpm test:e2e", - "ran_at": 1791219722, - "exit_code": 0, - "commit": "ae3e2d5a3cca3200954fa6646ad2be0599d0b9e1", - "branch": "claude/1297-npm-version" -} diff --git a/packages/ts/migrations.d/2026-10-05-npm-version.md b/packages/ts/migrations.d/2026-10-05-npm-version.md deleted file mode 100644 index 275f5c6d4..000000000 --- a/packages/ts/migrations.d/2026-10-05-npm-version.md +++ /dev/null @@ -1,27 +0,0 @@ -### npm: `dirsql --version` reports the published version - -#### Summary - -The npm CLI's `--version` printed `dirsql 0.2.7` whatever version was -installed. It now prints the version of the installed `dirsql` package, as -the PyPI build already did. Nothing breaks. - -#### Required changes - -_None._ - -#### Deprecations removed - -_None._ - -#### Behavior changes without code changes - -- `npx dirsql --version` prints `dirsql ` instead of - `dirsql 0.2.7`. - -#### Verification - -```sh -npx dirsql@latest --version -# expected: dirsql -``` diff --git a/packages/ts/Cargo.toml b/packages/ts/napi/Cargo.toml similarity index 86% rename from packages/ts/Cargo.toml rename to packages/ts/napi/Cargo.toml index b53bee232..7047e651d 100644 --- a/packages/ts/Cargo.toml +++ b/packages/ts/napi/Cargo.toml @@ -3,12 +3,8 @@ name = "dirsql-napi" version.workspace = true edition.workspace = true publish = false -# Colocated with package.json because the release only stamps the planned -# version into a napi crate whose manifest sits at the npm package path. -build = "napi/build.rs" [lib] -path = "napi/src/lib.rs" crate-type = ["cdylib"] [dependencies] diff --git a/packages/ts/package.json b/packages/ts/package.json index 5d23f9fa5..95788a0c1 100644 --- a/packages/ts/package.json +++ b/packages/ts/package.json @@ -44,7 +44,7 @@ }, "wireit": { "napi:build": { - "command": "napi build --release --manifest-path Cargo.toml --output-dir .", + "command": "napi build --release --manifest-path napi/Cargo.toml --output-dir .", "output": ["dirsql.node", "index.d.ts"] }, "tsc": { @@ -66,7 +66,7 @@ "src/platforms.json", "napi/src/**/*.rs", "napi/build.rs", - "Cargo.toml", + "napi/Cargo.toml", "../rust/src/**/*.rs", "../rust/Cargo.toml" ], @@ -76,11 +76,11 @@ "dependencies": ["napi:build", "tsc", "stage:platform"] }, "build:debug": { - "command": "napi build --manifest-path Cargo.toml --output-dir . && tsc -p tsconfig.build.json", + "command": "napi build --manifest-path napi/Cargo.toml --output-dir . && tsc -p tsconfig.build.json", "files": [ "napi/src/**/*.rs", "napi/build.rs", - "Cargo.toml", + "napi/Cargo.toml", "../rust/src/**/*.rs", "src/**/*.ts", "src/**/*.json", diff --git a/packages/ts/testing-conventions.toml b/packages/ts/testing-conventions.toml index 515e16764..f952e4388 100644 --- a/packages/ts/testing-conventions.toml +++ b/packages/ts/testing-conventions.toml @@ -4,12 +4,11 @@ # start staling this package. # # The napi crate is in scope because the npm launcher runs the CLI in-process -# through it (#721), so every e2e case crosses it -- the whole crate dir plus its -# manifest at the package root, since Cargo.toml and build.rs decide what lands -# in the addon. packages/python/src is +# through it (#721), so every e2e case crosses it -- the whole crate dir, since +# Cargo.toml and build.rs decide what lands in the addon. packages/python/src is # not: it cannot reach this suite. [e2e] -extra_scope = ["packages/rust/src", "packages/ts/napi", "packages/ts/Cargo.toml"] +extra_scope = ["packages/rust/src", "packages/ts/napi"] # The live TypeScript floors; the root file carries no `[typescript]` table. [typescript.coverage] diff --git a/packages/ts/tools/stage-platform.ts b/packages/ts/tools/stage-platform.ts index 2eec6fb31..aed9275b7 100644 --- a/packages/ts/tools/stage-platform.ts +++ b/packages/ts/tools/stage-platform.ts @@ -138,7 +138,7 @@ function stageOne(args: StageOneArgs): StageResult["staged"][number] { // instead of `dirsql.node`. Without it the cross-build's output // collides with the host's napi:build file at the package root. // `--manifest-path`/`--output-dir` point napi at the colocated - // `dirsql-napi` crate (packages/ts/Cargo.toml) while still dropping the + // `dirsql-napi` crate (packages/ts/napi) while still dropping the // artifact into this package (packages/ts) root. const cross = spawn( "npx", @@ -148,7 +148,7 @@ function stageOne(args: StageOneArgs): StageResult["staged"][number] { "--release", "--platform", "--manifest-path", - "Cargo.toml", + "napi/Cargo.toml", "--output-dir", ".", "--target", From de85e688f134577349363e83cbe3da57beb10788 Mon Sep 17 00:00:00 2001 From: thekevinbot <248217931+thekevinbot@users.noreply.github.com> Date: Mon, 5 Oct 2026 14:20:42 -0400 Subject: [PATCH 07/11] Colocate the napi crate manifest with package.json putitoutthere stamps the planned version into a napi crate only when its Cargo.toml sits at the npm package path. Ours lived in packages/ts/napi, so the stamp was skipped and the addon's CLI reported the committed core literal 0.2.7. --- .github/workflows/dirsql-rust-ci.yml | 7 ++++-- .github/workflows/dirsql-typescript-ci.yml | 2 +- Cargo.toml | 2 +- agents/reference/e2e-attestation.md | 4 ++-- packages/ts/{napi => }/Cargo.toml | 4 ++++ .../ts/changelog.d/2026-10-05-npm-version.md | 2 ++ .../ts/migrations.d/2026-10-05-npm-version.md | 22 +++++++++++++++++++ packages/ts/package.json | 8 +++---- packages/ts/testing-conventions.toml | 8 +++---- packages/ts/tools/stage-platform.ts | 4 ++-- 10 files changed, 47 insertions(+), 16 deletions(-) rename packages/ts/{napi => }/Cargo.toml (86%) create mode 100644 packages/ts/changelog.d/2026-10-05-npm-version.md create mode 100644 packages/ts/migrations.d/2026-10-05-npm-version.md diff --git a/.github/workflows/dirsql-rust-ci.yml b/.github/workflows/dirsql-rust-ci.yml index 9d389994e..34167919b 100644 --- a/.github/workflows/dirsql-rust-ci.yml +++ b/.github/workflows/dirsql-rust-ci.yml @@ -1,7 +1,8 @@ name: dirsql Rust CI -# The binding crates fan in (packages/ts/napi, packages/python/src): they are -# cargo workspace members, so the workspace-wide unit/lint jobs compile them. +# The binding crates fan in (packages/ts/napi with its manifest at +# packages/ts/Cargo.toml, and packages/python/src): they are cargo workspace +# members, so the workspace-wide unit/lint jobs compile them. # The root testing-conventions.toml carries this lane's config and the # [rust.coverage] floors the coverage job reads. on: @@ -10,6 +11,7 @@ on: paths: - 'packages/rust/**' - 'packages/ts/napi/**' + - 'packages/ts/Cargo.toml' - 'packages/python/src/**' - 'Cargo.toml' - 'Cargo.lock' @@ -22,6 +24,7 @@ on: - '!packages/rust/migrations.d/**' - '!packages/rust/README.md' - 'packages/ts/napi/**' + - 'packages/ts/Cargo.toml' - 'packages/python/src/**' - 'Cargo.toml' - 'Cargo.lock' diff --git a/.github/workflows/dirsql-typescript-ci.yml b/.github/workflows/dirsql-typescript-ci.yml index a91086a06..d30c9248c 100644 --- a/.github/workflows/dirsql-typescript-ci.yml +++ b/.github/workflows/dirsql-typescript-ci.yml @@ -113,7 +113,7 @@ jobs: build: # Builds the napi addon once and shares it with unit/binding/distcheck # below instead of each job/matrix-cell rebuilding it independently. - # The addon is N-API (napi6, see packages/ts/napi/Cargo.toml), which is + # The addon is N-API (napi6, see packages/ts/Cargo.toml), which is # ABI-stable across Node majors, so one build serves the whole Node # 20/22/24 matrix. name: Build native module diff --git a/Cargo.toml b/Cargo.toml index 928e1feef..85be71918 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["packages/python", "packages/rust", "packages/ts/napi"] +members = ["packages/python", "packages/rust", "packages/ts"] # Test-only loadable-extension fixture, built on demand by # packages/rust/tests/extensions.rs. Excluded so it isn't built/linted by # `cargo build --workspace` / `cargo clippy --workspace`. diff --git a/agents/reference/e2e-attestation.md b/agents/reference/e2e-attestation.md index cc7582007..9908fbf05 100644 --- a/agents/reference/e2e-attestation.md +++ b/agents/reference/e2e-attestation.md @@ -27,7 +27,7 @@ Receipts are append-only in practice -- a merged branch's receipt stays in the d **Everything compiled into the binding is in scope -- CI-enforced via `[e2e].extra_scope`.** The shared Rust core (`packages/rust/src`) is compiled into both bindings but lives in neither binding's subtree, and `e2e verify --scope` requires the scope to be a **descendant of the caller's `path`** -- so a binding caller cannot point its own `--scope` at the core. That case is solved by the `e2e verify --extra-scope` / `--exclude` flags (upstream #333): an `[e2e]` block declares the extra roots, `detect.py` reads it, and the reusable `e2e-verify` job passes it through. Pure config; the former bespoke `e2e_core_freshness.py` gate is not needed. Two roots per binding: - **The core, `packages/rust/src` -- puts BOTH bindings in scope** (#337). **After any shared-core change, attest each binding** (`just e2e-attest-python`, `just e2e-attest-ts`) -- CI enforces this, it is no longer a by-hand-only promise. **`cli`-only** core source (`packages/rust/src/cli/**`, `packages/rust/src/bin/**`) is feature-gated out of the binding *libraries*, but since #721 each launcher calls the core's `run_cli` **in-process through its binding** -- a CLI change alters what those suites exercise, so it demands a receipt from both bindings like any other core change (no `exclude` carve-out). -- **The binding crate itself -- puts ONLY its own package in scope** (#933): `packages/python/src` (the PyO3 glue) for python, `packages/ts/napi` (the napi-rs crate, manifest and `build.rs` included) for typescript. Same #721 argument one layer out: `run_cli` in each binding does argv framing, GIL detach / exit-code handling, and runs in every e2e case, so a binding-only diff changes what the suites exercise with no core change at all. A napi change has no bearing on the python suite and never demands a python receipt, and vice versa. +- **The binding crate itself -- puts ONLY its own package in scope** (#933): `packages/python/src` (the PyO3 glue) for python, `packages/ts/napi` plus `packages/ts/Cargo.toml` (the napi-rs crate, `build.rs` included; the manifest sits at the package root so the release stamps its version) for typescript. Same #721 argument one layer out: `run_cli` in each binding does argv framing, GIL detach / exit-code handling, and runs in every e2e case, so a binding-only diff changes what the suites exercise with no core change at all. A napi change has no bearing on the python suite and never demands a python receipt, and vice versa. **That per-package split is why `packages/python` and `packages/ts` each carry their own `testing-conventions.toml`** (passed explicitly as the lane's `config:`, the pattern `internals/checks` established in #550). `[e2e]` is a single global table applied to every caller reading a given config, so "core + *my* binding" is not expressible in one shared file: declaring both bindings at the repo root would make a napi change demand a python receipt (and force `packages/ts/napi/**` into the python lane's path triggers to enforce it at all). The root `[e2e]` block stays as the default for a caller with no config of its own. @@ -41,7 +41,7 @@ Agents must run the full e2e suite locally before any `git push` that includes a **"Substantial" means any change touching:** - `packages/rust/**` (Rust core) -- `packages/ts/napi/**` (napi-rs binding crate) +- `packages/ts/napi/**` and `packages/ts/Cargo.toml` (napi-rs binding crate) - `packages/python/src/**` (excluding files matching `*_test.py`) - `packages/ts/src/**` (excluding files matching `*.test.ts` / `*.spec.ts`) - Any shared SDK runtime code reachable from the above diff --git a/packages/ts/napi/Cargo.toml b/packages/ts/Cargo.toml similarity index 86% rename from packages/ts/napi/Cargo.toml rename to packages/ts/Cargo.toml index 7047e651d..b53bee232 100644 --- a/packages/ts/napi/Cargo.toml +++ b/packages/ts/Cargo.toml @@ -3,8 +3,12 @@ name = "dirsql-napi" version.workspace = true edition.workspace = true publish = false +# Colocated with package.json because the release only stamps the planned +# version into a napi crate whose manifest sits at the npm package path. +build = "napi/build.rs" [lib] +path = "napi/src/lib.rs" crate-type = ["cdylib"] [dependencies] diff --git a/packages/ts/changelog.d/2026-10-05-npm-version.md b/packages/ts/changelog.d/2026-10-05-npm-version.md new file mode 100644 index 000000000..6e1e89449 --- /dev/null +++ b/packages/ts/changelog.d/2026-10-05-npm-version.md @@ -0,0 +1,2 @@ +**Fixed** `npx dirsql --version` reports the npm package's version. It used to +print `0.2.7`, the unreleased crate version, on every release. diff --git a/packages/ts/migrations.d/2026-10-05-npm-version.md b/packages/ts/migrations.d/2026-10-05-npm-version.md new file mode 100644 index 000000000..ddb37a897 --- /dev/null +++ b/packages/ts/migrations.d/2026-10-05-npm-version.md @@ -0,0 +1,22 @@ +### npm: `dirsql --version` reports the published version + +#### Summary + +`npx dirsql --version` now prints the installed package's version instead of +`0.2.7`. Nothing breaks. + +#### Required changes + +_None._ + +#### Deprecations removed + +_None._ + +#### Behavior changes without code changes + +_None._ + +#### Verification + +_None._ diff --git a/packages/ts/package.json b/packages/ts/package.json index 95788a0c1..5d23f9fa5 100644 --- a/packages/ts/package.json +++ b/packages/ts/package.json @@ -44,7 +44,7 @@ }, "wireit": { "napi:build": { - "command": "napi build --release --manifest-path napi/Cargo.toml --output-dir .", + "command": "napi build --release --manifest-path Cargo.toml --output-dir .", "output": ["dirsql.node", "index.d.ts"] }, "tsc": { @@ -66,7 +66,7 @@ "src/platforms.json", "napi/src/**/*.rs", "napi/build.rs", - "napi/Cargo.toml", + "Cargo.toml", "../rust/src/**/*.rs", "../rust/Cargo.toml" ], @@ -76,11 +76,11 @@ "dependencies": ["napi:build", "tsc", "stage:platform"] }, "build:debug": { - "command": "napi build --manifest-path napi/Cargo.toml --output-dir . && tsc -p tsconfig.build.json", + "command": "napi build --manifest-path Cargo.toml --output-dir . && tsc -p tsconfig.build.json", "files": [ "napi/src/**/*.rs", "napi/build.rs", - "napi/Cargo.toml", + "Cargo.toml", "../rust/src/**/*.rs", "src/**/*.ts", "src/**/*.json", diff --git a/packages/ts/testing-conventions.toml b/packages/ts/testing-conventions.toml index f952e4388..3ed868a12 100644 --- a/packages/ts/testing-conventions.toml +++ b/packages/ts/testing-conventions.toml @@ -4,11 +4,11 @@ # start staling this package. # # The napi crate is in scope because the npm launcher runs the CLI in-process -# through it (#721), so every e2e case crosses it -- the whole crate dir, since -# Cargo.toml and build.rs decide what lands in the addon. packages/python/src is -# not: it cannot reach this suite. +# through it (#721), so every e2e case crosses it -- the whole crate dir plus its +# manifest at the package root, since Cargo.toml and build.rs decide what lands +# in the addon. packages/python/src is not: it cannot reach this suite. [e2e] -extra_scope = ["packages/rust/src", "packages/ts/napi"] +extra_scope = ["packages/rust/src", "packages/ts/napi", "packages/ts/Cargo.toml"] # The live TypeScript floors; the root file carries no `[typescript]` table. [typescript.coverage] diff --git a/packages/ts/tools/stage-platform.ts b/packages/ts/tools/stage-platform.ts index aed9275b7..2eec6fb31 100644 --- a/packages/ts/tools/stage-platform.ts +++ b/packages/ts/tools/stage-platform.ts @@ -138,7 +138,7 @@ function stageOne(args: StageOneArgs): StageResult["staged"][number] { // instead of `dirsql.node`. Without it the cross-build's output // collides with the host's napi:build file at the package root. // `--manifest-path`/`--output-dir` point napi at the colocated - // `dirsql-napi` crate (packages/ts/napi) while still dropping the + // `dirsql-napi` crate (packages/ts/Cargo.toml) while still dropping the // artifact into this package (packages/ts) root. const cross = spawn( "npx", @@ -148,7 +148,7 @@ function stageOne(args: StageOneArgs): StageResult["staged"][number] { "--release", "--platform", "--manifest-path", - "napi/Cargo.toml", + "Cargo.toml", "--output-dir", ".", "--target", From bb0d45e76eb97d574c232319a5d80e9bcee20db0 Mon Sep 17 00:00:00 2001 From: thekevinbot <248217931+thekevinbot@users.noreply.github.com> Date: Mon, 5 Oct 2026 14:20:48 -0400 Subject: [PATCH 08/11] e2e attestation for claude/1297-npm-version --- packages/ts/e2e-attestations/claude-1297-npm-version.json | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 packages/ts/e2e-attestations/claude-1297-npm-version.json diff --git a/packages/ts/e2e-attestations/claude-1297-npm-version.json b/packages/ts/e2e-attestations/claude-1297-npm-version.json new file mode 100644 index 000000000..038cfc98c --- /dev/null +++ b/packages/ts/e2e-attestations/claude-1297-npm-version.json @@ -0,0 +1,7 @@ +{ + "command": "pnpm test:e2e", + "ran_at": 1791224448, + "exit_code": 0, + "commit": "de85e688f134577349363e83cbe3da57beb10788", + "branch": "claude/1297-npm-version" +} From fdb9088765bf08be8c735a41b0a48d8954e2f3ce Mon Sep 17 00:00:00 2001 From: thekevinbot <248217931+thekevinbot@users.noreply.github.com> Date: Mon, 5 Oct 2026 14:32:29 -0400 Subject: [PATCH 09/11] e2e attestation for claude/1297-npm-version --- packages/ts/e2e-attestations/claude-1297-npm-version.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ts/e2e-attestations/claude-1297-npm-version.json b/packages/ts/e2e-attestations/claude-1297-npm-version.json index 038cfc98c..f1599bda2 100644 --- a/packages/ts/e2e-attestations/claude-1297-npm-version.json +++ b/packages/ts/e2e-attestations/claude-1297-npm-version.json @@ -1,7 +1,7 @@ { "command": "pnpm test:e2e", - "ran_at": 1791224448, + "ran_at": 1791225149, "exit_code": 0, - "commit": "de85e688f134577349363e83cbe3da57beb10788", + "commit": "909df2c633acf60f23015c3b5e83a173dbc0e9aa", "branch": "claude/1297-npm-version" } From 0a3b8dcc6a20ef9fedbf5245918cf8537c1c0f5e Mon Sep 17 00:00:00 2001 From: thekevinbot <248217931+thekevinbot@users.noreply.github.com> Date: Mon, 5 Oct 2026 14:53:22 -0400 Subject: [PATCH 10/11] e2e attestation for claude/1297-npm-version --- packages/ts/e2e-attestations/claude-1297-npm-version.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ts/e2e-attestations/claude-1297-npm-version.json b/packages/ts/e2e-attestations/claude-1297-npm-version.json index f1599bda2..64fa1ba6a 100644 --- a/packages/ts/e2e-attestations/claude-1297-npm-version.json +++ b/packages/ts/e2e-attestations/claude-1297-npm-version.json @@ -1,7 +1,7 @@ { "command": "pnpm test:e2e", - "ran_at": 1791225149, + "ran_at": 1791226402, "exit_code": 0, - "commit": "909df2c633acf60f23015c3b5e83a173dbc0e9aa", + "commit": "802e69c0b59b0407ac60ea3f565f45694331c61a", "branch": "claude/1297-npm-version" } From 9a5b5f6cfc3a17ca3e5a11b4ac20b78c5e08e1c1 Mon Sep 17 00:00:00 2001 From: thekevinbot <248217931+thekevinbot@users.noreply.github.com> Date: Mon, 5 Oct 2026 17:50:28 -0400 Subject: [PATCH 11/11] e2e attestation for claude/1297-npm-version --- packages/ts/e2e-attestations/claude-1297-npm-version.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ts/e2e-attestations/claude-1297-npm-version.json b/packages/ts/e2e-attestations/claude-1297-npm-version.json index 64fa1ba6a..bd815ff69 100644 --- a/packages/ts/e2e-attestations/claude-1297-npm-version.json +++ b/packages/ts/e2e-attestations/claude-1297-npm-version.json @@ -1,7 +1,7 @@ { "command": "pnpm test:e2e", - "ran_at": 1791226402, + "ran_at": 1791237028, "exit_code": 0, - "commit": "802e69c0b59b0407ac60ea3f565f45694331c61a", + "commit": "ce521cdd6a0d9088f75a689159ec3f37ff2c864a", "branch": "claude/1297-npm-version" }