From 22e55738cb00337a54d69f399098f0e390d682c8 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Sun, 9 Aug 2026 12:12:18 -0700 Subject: [PATCH 1/4] Put the Floors in the Contract Table, and Tie the Table to the Data The table answers presence, and a tool below its floor answers --version like any other, so every column reported a host as fine while host_gate.py failed it. A host on 3.12 read the Python 3 row and stopped. That was equally true of gh at 2.46 before this change, so the fix is a Floor column covering both rather than a note on the one row the finding named. The kind is named beside the number, since a measured floor gives a failing host a defect to point at and a target floor does not. git-restore-mtime is deliberately absent from the table, carrying a floor while being optional, and the prose below already says why. Restating a number in prose creates the drift it was meant to prevent, so a test now reads the shipped docs/host-setup.md and asserts every required tool's declared floor appears in a table row. It was written first against the old table to confirm it fails, and the failure it produced was a real one: git-restore-mtime has a floor and no row, which is what scoped the assertion to required tools. Rewriting the table to 3.12 fails it with "python3 declares 3.13 and no table row states it". Co-Authored-By: Claude Opus 5 (1M context) --- docs/host-setup.md | 16 +++++++++------- scripts/test_host_gate.py | 22 ++++++++++++++++++++++ 2 files changed, 31 insertions(+), 7 deletions(-) diff --git a/docs/host-setup.md b/docs/host-setup.md index add21460..96109f08 100644 --- a/docs/host-setup.md +++ b/docs/host-setup.md @@ -14,13 +14,15 @@ Supported hosts: This section is the **contract**: which tools a host needs and which repo procedure stops working without each one. It deliberately names no installer, because `winget`, `brew` and `apt` differ per platform while the requirement does not. Per-platform install commands are tracked separately, so this table stays true on every host. -| Tool | Needed by | Present when | -| --- | --- | --- | -| `git` | everything, and the identity and signing contract in [`STANDUP.md`][standup] step 0 | `git --version` | -| `gh` | the PR and review loop, `gh api` queries, `repo-config/configure.sh` | `gh --version` | -| Python 3 | `scripts/` and `spec/` (standard library only, no packages to install) | `python3 --version`, or `py -3 --version` on native Windows | -| `docker` | the four linters, which run as pinned images rather than local installs | `docker --version` | -| `uv` / `uvx` | coverage runs, and the Python toolchain (`ruff`, `pyright` or `mypy`) in a Python repo | `uv --version` | +| Tool | Needed by | Present when | Floor | +| --- | --- | --- | --- | +| `git` | everything, and the identity and signing contract in [`STANDUP.md`][standup] step 0 | `git --version` | none | +| `gh` | the PR and review loop, `gh api` queries, `repo-config/configure.sh` | `gh --version` | **2.47.0**, measured | +| Python 3 | `scripts/` and `spec/` (standard library only, no packages to install) | `python3 --version`, or `py -3 --version` on native Windows | **3.13**, 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 | + +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. Two consequences worth reading off the table rather than discovering later. **Python 3 needs no packages**, because every script here is standard library only, so a bare interpreter is enough. And **the linters need only `docker`**, not `node`, `dotnet` or a local `markdownlint`, since each runs as a pinned image, which is what keeps a local run and CI the same check. diff --git a/scripts/test_host_gate.py b/scripts/test_host_gate.py index b82d10c1..c9ba52f3 100755 --- a/scripts/test_host_gate.py +++ b/scripts/test_host_gate.py @@ -415,6 +415,28 @@ def test_the_declared_floors_are_the_ones_with_a_stated_reason(self): floors = {t['name'] for t in self.data['tools'] if t['minimum'] is not None} self.assertEqual(floors, {'gh', 'git-restore-mtime', 'python3'}) + 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. + + The table's other columns answer presence, and a tool below its floor answers `--version` + like any other, so the Floor column is the only thing there that distinguishes present from + sufficient. A number that drifts out of step with the data is worse than an absent one, + since a host reads the table and stops. + """ + doc = (host_gate.SPEC.parent.parent / 'docs' / 'host-setup.md').read_text(encoding='utf-8') + table = [ln for ln in doc.splitlines() if ln.startswith('| ') and '|' in ln[2:]] + self.assertTrue(table, 'no contract table found in docs/host-setup.md') + for t in self.data['tools']: + # An optional tool is deliberately outside the table, which lists what a host must provide. + # `git-restore-mtime` carries a floor and no row, and that is correct. + if t['minimum'] is None or not t.get('required'): + continue + rows = [ln for ln in table if t['name'] in ln or t['name'].rstrip('3') in ln] + self.assertTrue(rows, f'{t["name"]} declares a floor and has no row in the contract table') + self.assertTrue( + any(t['minimum'] in ln for ln in rows), + f'{t["name"]} declares {t["minimum"]} and no table row states it') + def test_a_target_floor_says_so_rather_than_implying_a_defect(self): """The python3 floor is a target, so its `why` has to distinguish itself from a measured one. From c5765ca16bb3d6312660de636124e8cadcac9c6e Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Sun, 9 Aug 2026 12:16:58 -0700 Subject: [PATCH 2/4] Key the Table Row on Its Tool Cell, Not on the Whole Line The row lookup searched the entire line, so `python3` matched the probe command in the Present when column rather than the Tool label. The test passed for a reason it did not intend, and would have broken had that column ever been reworded. The rstrip('3') fallback was dead either way, since it produces `python`, which never matches `Python 3`. The row is now keyed on its Tool cell alone, with both sides normalized by lowercasing and dropping anything not alphanumeric, so the prose label `Python 3` keys as the identifier `python3` and the match is the one the test means. Checked against three rewrites of the table rather than one. A drifted number and a removed floor both fail it, and rewording Present when to "the interpreter answers its version flag" leaves it passing, which is the brittleness the finding named. Co-Authored-By: Claude Opus 5 (1M context) --- scripts/test_host_gate.py | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/scripts/test_host_gate.py b/scripts/test_host_gate.py index c9ba52f3..797c7202 100755 --- a/scripts/test_host_gate.py +++ b/scripts/test_host_gate.py @@ -9,6 +9,7 @@ from __future__ import annotations import json +import re import sys import unittest from pathlib import Path @@ -424,17 +425,27 @@ def test_the_contract_table_carries_every_declared_floor(self): since a host reads the table and stops. """ doc = (host_gate.SPEC.parent.parent / 'docs' / 'host-setup.md').read_text(encoding='utf-8') - table = [ln for ln in doc.splitlines() if ln.startswith('| ') and '|' in ln[2:]] - self.assertTrue(table, 'no contract table found in docs/host-setup.md') + rows = {} + for ln in doc.splitlines(): + if not ln.startswith('| '): + continue + cells = [c.strip() for c in ln.strip('|').split('|')] + # The row is keyed on its Tool cell alone, never on the whole line. + # Matching the line matches the probe command in `Present when` instead. + # The key would then be whatever that column said, and the test would pass for the wrong reason. + # The label is prose and the name is an identifier, so `Python 3` keys as `python3`. + if cells: + rows[re.sub(r'[^a-z0-9.]', '', cells[0].lower())] = cells + self.assertIn('python3', rows, 'no contract table found in docs/host-setup.md') for t in self.data['tools']: # An optional tool is deliberately outside the table, which lists what a host must provide. # `git-restore-mtime` carries a floor and no row, and that is correct. if t['minimum'] is None or not t.get('required'): continue - rows = [ln for ln in table if t['name'] in ln or t['name'].rstrip('3') in ln] - self.assertTrue(rows, f'{t["name"]} declares a floor and has no row in the contract table') + key = re.sub(r'[^a-z0-9.]', '', t['name'].lower()) + self.assertIn(key, rows, f'{t["name"]} declares a floor and has no row in the contract table') self.assertTrue( - any(t['minimum'] in ln for ln in rows), + any(t['minimum'] in c for c in rows[key]), f'{t["name"]} declares {t["minimum"]} and no table row states it') def test_a_target_floor_says_so_rather_than_implying_a_defect(self): From f04ef1fdef1a30d56e9aad2248dee6c67f2e0a4f Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Sun, 9 Aug 2026 12:21:04 -0700 Subject: [PATCH 3/4] Assert the Floor Cell by Its Header, and Say What Each Failure Means Two findings, and the second is the first one over again in the next column. The version assertion searched every cell in the row, so a table stating a version in Present when would satisfy it with the Floor column drifted or deleted, which is the failure this test exists to catch. My own three-case check missed it because Present when does not happen to contain 3.13 today, which is the same accidental pass in a different place. The Floor column is now located by its header rather than by index or by search, and the assertion reads that one cell. Locating it by header rather than as the last column also survives a column being appended. The other finding is a message that named the wrong condition: assertIn on the python3 row reported "no contract table found", which is untrue when the table is present and the row is not. Finding the table and finding a row are now separate assertions with separate messages, and the version failure prints the Floor cell it actually read. Checked against five table rewrites rather than three, including the case the finding named: a version moved into Present when with Floor blanked now fails, as does removing the Floor column outright, while rewording Present when still passes. Co-Authored-By: Claude Opus 5 (1M context) --- scripts/test_host_gate.py | 32 +++++++++++++++++++++----------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/scripts/test_host_gate.py b/scripts/test_host_gate.py index 797c7202..6678e355 100755 --- a/scripts/test_host_gate.py +++ b/scripts/test_host_gate.py @@ -424,29 +424,39 @@ def test_the_contract_table_carries_every_declared_floor(self): sufficient. A number that drifts out of step with the data is worse than an absent one, since a host reads the table and stops. """ + def norm(text): + """A prose label reduced to the identifier it names, so `Python 3` keys as `python3`.""" + return re.sub(r'[^a-z0-9.]', '', text.lower()) + doc = (host_gate.SPEC.parent.parent / 'docs' / 'host-setup.md').read_text(encoding='utf-8') - rows = {} + rows, floor_col = {}, None for ln in doc.splitlines(): if not ln.startswith('| '): continue cells = [c.strip() for c in ln.strip('|').split('|')] - # The row is keyed on its Tool cell alone, never on the whole line. - # Matching the line matches the probe command in `Present when` instead. - # The key would then be whatever that column said, and the test would pass for the wrong reason. - # The label is prose and the name is an identifier, so `Python 3` keys as `python3`. + # Every lookup here is on one named cell, never on the row and never on a fixed index. + # Searching the row matches the probe command in `Present when` as well. + # A table stating a version there would then satisfy this with the Floor column deleted. + # That is the failure this test exists to catch rather than to reproduce. + if floor_col is None and 'floor' in [c.lower() for c in cells]: + floor_col = [c.lower() for c in cells].index('floor') + continue if cells: - rows[re.sub(r'[^a-z0-9.]', '', cells[0].lower())] = cells - self.assertIn('python3', rows, 'no contract table found in docs/host-setup.md') + rows[norm(cells[0])] = 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. # `git-restore-mtime` carries a floor and no row, and that is correct. if t['minimum'] is None or not t.get('required'): continue - key = re.sub(r'[^a-z0-9.]', '', t['name'].lower()) + key = norm(t['name']) self.assertIn(key, rows, f'{t["name"]} declares a floor and has no row in the contract table') - self.assertTrue( - any(t['minimum'] in c for c in rows[key]), - f'{t["name"]} declares {t["minimum"]} and no table row states it') + cells = rows[key] + self.assertGreater(len(cells), floor_col, + f'the {t["name"]} row has no Floor cell') + self.assertIn(t['minimum'], cells[floor_col], + f'{t["name"]} declares {t["minimum"]} and its Floor cell says ' + f'{cells[floor_col]!r}') def test_a_target_floor_says_so_rather_than_implying_a_defect(self): """The python3 floor is a target, so its `why` has to distinguish itself from a measured one. From 9f839e49803fad9abf642f975ceb81357269224d Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Sun, 9 Aug 2026 12:26:17 -0700 Subject: [PATCH 4/4] Stop Reading at the End of the Contract Table The parser scanned every table in the document, so a later table's first column was a key like any other and would overwrite a tool row. The document carries a second table today, "Now possible", whose keys collide with nothing, and a test that depends on that is one the next table silently breaks. Reading now stops at the first non-table line after the contract table. The previous version, run against a document with a later table carrying a Python 3 row, fails with "2 not greater than 3 : the python3 row has no Floor cell", which is a real break rather than a hypothetical one. This version passes it, while the drift cases it must catch still fail. That is the third finding in a row of one shape on this test, each a lookup wider than the thing it meant to name: the row keyed on the whole line, the version searched across every cell, and now the table read to the end of the file. Co-Authored-By: Claude Opus 5 (1M context) --- scripts/test_host_gate.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/scripts/test_host_gate.py b/scripts/test_host_gate.py index 6678e355..bcc1ddc3 100755 --- a/scripts/test_host_gate.py +++ b/scripts/test_host_gate.py @@ -431,15 +431,22 @@ def norm(text): doc = (host_gate.SPEC.parent.parent / 'docs' / 'host-setup.md').read_text(encoding='utf-8') rows, floor_col = {}, None for ln in doc.splitlines(): - if not ln.startswith('| '): + cells = [c.strip() for c in ln.strip('|').split('|')] if ln.startswith('| ') else None + # Reading stops at the end of the contract table rather than at the end of the file. + # A later table's first column is a key like any other and would overwrite a tool row. + # The document carries a second table today whose keys collide with nothing. + # A test that depends on that is a test the next table silently breaks. + if floor_col is not None and cells is None: + break + if cells is None: continue - cells = [c.strip() for c in ln.strip('|').split('|')] # Every lookup here is on one named cell, never on the row and never on a fixed index. # Searching the row matches the probe command in `Present when` as well. # A table stating a version there would then satisfy this with the Floor column deleted. # That is the failure this test exists to catch rather than to reproduce. - if floor_col is None and 'floor' in [c.lower() for c in cells]: - floor_col = [c.lower() for c in cells].index('floor') + if floor_col is None: + if 'floor' in [c.lower() for c in cells]: + floor_col = [c.lower() for c in cells].index('floor') continue if cells: rows[norm(cells[0])] = cells