diff --git a/docs/host-setup.md b/docs/host-setup.md index d80d4468..f21f2d5c 100644 --- a/docs/host-setup.md +++ b/docs/host-setup.md @@ -21,7 +21,7 @@ This section is the **contract**: which tools a host needs and which repo proced | Python 3 | `scripts/` and `spec/` (standard library only, no packages to install) | `python3 --version`, or `py -3 --version` on native Windows | **3.13**, target | | `jq` | the ruleset normalizer in `repo-config/configure.sh`, the ruleset diff in [`AUDIT.md`][audit] section 6, and payload regeneration | `jq --version` | **1.7**, target | | `docker` | the four linters, which run as pinned images rather than local installs | `docker --version` | none | -| `uv` / `uvx` | coverage runs, and the Python toolchain (`ruff`, `pyright` or `mypy`) in a Python repo | `uv --version` | none | +| `uv` / `uvx` | coverage runs, and the Python toolchain (`ruff`, `pyright` or `mypy`) in a Python repo | `uv --version` | **0.12.2**, target | The **Floor** column exists because presence and sufficiency are different questions and the answer to the first was being read as the answer to the second. A tool below its floor still answers `--version`, so every other column reports it as fine while `scripts/host_gate.py` fails it. The kind is named beside the number, since a **measured** floor sits above a version known to break a documented procedure and gives a failing host a defect to point at, where a **target** floor names the version the repo's toolchain is configured for and does not. The next section carries the reasoning behind each one. diff --git a/scripts/test_host_gate.py b/scripts/test_host_gate.py index f22f1cec..8dbb2105 100755 --- a/scripts/test_host_gate.py +++ b/scripts/test_host_gate.py @@ -656,7 +656,7 @@ def test_the_declared_floors_are_the_ones_with_a_stated_reason(self): one in the data, which is what caught the python3 floor being added without this line. """ floors = {t['name'] for t in self.data['tools'] if t['minimum'] is not None} - self.assertEqual(floors, {'gh', 'git-restore-mtime', 'jq', 'python3'}) + self.assertEqual(floors, {'gh', 'git-restore-mtime', 'jq', 'python3', 'uv'}) def test_the_contract_table_carries_every_declared_floor(self): """docs/host-setup.md restates the floors, so the doc goes stale the moment the data moves. @@ -691,7 +691,9 @@ def norm(text): floor_col = [c.lower() for c in cells].index('floor') continue if cells: - rows[norm(cells[0])] = cells + # A compound cell like `uv` / `uvx` names two tools, so each names the row. + for token in cells[0].split('/'): + rows[norm(token)] = cells self.assertIsNotNone(floor_col, 'docs/host-setup.md has no contract table with a Floor column') for t in self.data['tools']: # An optional tool is deliberately outside the table, which lists what a host must provide. diff --git a/spec/host-tools.json b/spec/host-tools.json index d57ccd26..c8ed655e 100644 --- a/spec/host-tools.json +++ b/spec/host-tools.json @@ -94,8 +94,18 @@ "required": true, "probes": [["uv", "--version"]], "pattern": "uv (\\d+(?:\\.\\d+)*)", - "minimum": null, - "why": "Used for coverage runs and the Python toolchain in a Python repository, where the pinned tool versions live in that repository rather than in uv itself. No floor has been measured here." + "minimum": "0.12.2", + "why": "Used for coverage runs and the Python toolchain in a Python repository, where the pinned tool versions live in that repository rather than in uv itself, so no uv feature is what a floor here would guard. The floor is a target rather than a measured breakage, and it says so rather than implying a defect nobody found. It is anchored to what two independently set-up hosts already carry rather than to a version anyone has verified good, the same reasoning jq's floor uses. A winget install on Windows carried 0.12.2 while winget already offered 0.12.4, so the winget package can lag upstream by a release, and host-setup/linux/install-tools.sh, which fetches upstream's latest release directly rather than a distribution package, carried 0.12.4 on a separate Linux host. The floor sits at the lower of the two, so a host on either managed install path already clears it, and a host below it is unverified rather than known broken.", + "source": { + "linux": "The upstream release archive at https://github.com/astral-sh/uv, which host-setup/linux/install-tools.sh fetches directly, and no distribution packages it.", + "macos": "Homebrew, which tracks upstream releases.", + "windows": "winget (astral-sh.uv), which can lag upstream by a release rather than tracking it release for release." + }, + "remedy": { + "linux": "host-setup/linux/install-tools.sh --upgrade uv", + "macos": "brew upgrade uv", + "windows": "host-setup/windows/install-tools.ps1 -Upgrade uv" + } } ] }