probe: discover environments from manifests, not a hardcoded list

The environment list was written out four times — probe.py's shim tuple and its
results dict, tests/test_probe.py's ENVIRONMENTS, and ci.yml's wheel loop — so
adding an environment meant four edits and five workstreams collided on three
files. Discovery is now a tomllib scan of environments/*/pyproject.toml plus a
@probes(name) registry, with an optional [tool.arena] tasksets key for one
package that ships several (tera_spatial declares four).

An environment discovered without a registered probe WARNS rather than exits 1,
because interactive environments land their engine before their probe. That is
temporary; it becomes a hard failure with the last registration.

The warning is not a licence to stop being gated. test_probe.py pins the
ungated set to an explicit PENDING_PROBES allowlist that only ever shrinks —
without it, removing a @probes decorator moved the name from the table into a
warning line that already carries five, and every remaining assertion stayed
vacuously true while the environment silently stopped being gated.

Also untracks three environments/*/uv.lock files, which had been failing the
lock-policy step on main.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-21 15:54:56 -07:00
co-authored by Claude Opus 5
parent 16f4ee21ae
commit 8fe348ca82
6 changed files with 179 additions and 10502 deletions
+13 -2
View File
@@ -23,7 +23,16 @@ jobs:
python-version: ${{ matrix.python }} python-version: ${{ matrix.python }}
- name: Enforce library lock policy - name: Enforce library lock policy
run: test -z "$(find environments -name uv.lock -print -quit)" run: test -z "$(find environments -name uv.lock -print -quit)"
- name: Run four-environment floor and ceiling probe # An environment is `environments/*/pyproject.toml` and nothing else — the same
# denominator probe.py's discover() uses. A half-created scaffold directory with no
# manifest is invisible to both, so the two can never disagree about what exists.
- name: Refuse an empty environment scan
run: |
set -euo pipefail
count=$(find environments -mindepth 2 -maxdepth 2 -name pyproject.toml | wc -l)
echo "discovered $count environment manifests"
test "$count" -gt 0
- name: Run the floor and ceiling probe for every discovered environment
run: uv run --with regex python probe.py run: uv run --with regex python probe.py
- name: Run root probe integration test - name: Run root probe integration test
run: uv run --with regex python -m unittest discover -s tests -v run: uv run --with regex python -m unittest discover -s tests -v
@@ -35,7 +44,9 @@ jobs:
python -m unittest discover -s environments/redaction_pressure/tests -v python -m unittest discover -s environments/redaction_pressure/tests -v
- name: Build all environment distributions - name: Build all environment distributions
run: | run: |
for environment in redaction_pressure canary_trap fault_localisation schema_migration bot_detection grand_exchange drop_table_inference; do set -euo pipefail
for manifest in environments/*/pyproject.toml; do
environment=$(basename "$(dirname "$manifest")")
uv build "environments/$environment" --out-dir "dist/$environment" uv build "environments/$environment" --out-dir "dist/$environment"
done done
- name: Verify documented Redaction Pressure entrypoint - name: Verify documented Redaction Pressure entrypoint
-3488
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
-3488
View File
File diff suppressed because it is too large Load Diff
+93 -15
View File
@@ -62,7 +62,10 @@ the first time and this is what caught it:
the first block was the shortcut's luckiest. the first block was the shortcut's luckiest.
Exit code is the API: 1 if any environment's floor is above zero or its ceiling is Exit code is the API: 1 if any environment's floor is above zero or its ceiling is
unreachable. unreachable. Nothing here is listed by hand — the environments are `environments/*/pyproject.toml`
and each probe registers itself with `@probes(taskset_id)`, so adding one is a function appended
at the end of this file. A manifest with no probe is a warning today and an error the day the
last one is written.
uv run --with regex python probe.py uv run --with regex python probe.py
""" """
@@ -76,25 +79,80 @@ import random
import re import re
import statistics import statistics
import sys import sys
import tomllib
import types import types
from collections.abc import Callable
from pathlib import Path from pathlib import Path
ENVS = Path(__file__).parent / "environments" ENVS = Path(__file__).parent / "environments"
def discover() -> dict[str, list[Path]]:
"""Taskset id -> the package directories that ship it, read off the manifests.
The seven names this file used to carry as a literal tuple are `environments/*/pyproject.toml`
now: an environment is gated by existing, not by being remembered here. One directory may
declare more than one taskset — the Tera bridge ships four spatial tasksets out of a single
package — through an optional `[tool.arena] tasksets = [...]`. Absent that key the taskset id
is the project name, which is what all seven of the originals do.
Other sessions create directories under `environments/` while this runs, so a manifest that
disappears between the glob and the read is skipped rather than fatal. A manifest that is
present and unparseable is still fatal — that is a broken commit, not a race.
"""
found: dict[str, list[Path]] = {}
for manifest in sorted(ENVS.glob("*/pyproject.toml")):
try:
data = tomllib.loads(manifest.read_text(encoding="utf-8"))
except FileNotFoundError:
continue
wheel = (data.get("tool", {}).get("hatch", {}).get("build", {})
.get("targets", {}).get("wheel", {}))
packages = wheel.get("packages") or [manifest.parent.name]
names = data.get("tool", {}).get("arena", {}).get("tasksets")
if not names:
name = data.get("project", {}).get("name")
if not name:
print(f"warning: {manifest} declares no project.name — skipped", file=sys.stderr)
continue
names = [name]
for name in names:
found[name] = [manifest.parent / package for package in packages]
return found
# Each package's __init__ imports its taskset, which imports verifiers — a heavy dependency # Each package's __init__ imports its taskset, which imports verifiers — a heavy dependency
# this file does not need and should not require. A self-check that only runs once the # this file does not need and should not require. A self-check that only runs once the
# training stack is installed is a self-check nobody runs. So the package name is registered # training stack is installed is a self-check nobody runs. So the package name is registered
# as a namespace pointing at the source directory, and the leaf modules are imported under # as a namespace pointing at the source directory, and the leaf modules are imported under
# it directly: intra-package imports still resolve, __init__ never executes. # it directly: intra-package imports still resolve, __init__ never executes.
for _pkg in ("redaction_pressure", "canary_trap", "fault_localisation", "schema_migration", for _paths in discover().values():
"bot_detection", "grand_exchange", "drop_table_inference"): for _path in _paths:
_shim = types.ModuleType(_pkg) _shim = types.ModuleType(_path.name)
_shim.__path__ = [str(ENVS / _pkg / _pkg)] _shim.__path__ = [str(_path)]
sys.modules[_pkg] = _shim sys.modules[_path.name] = _shim
# Every floor-and-ceiling function registers itself here under the taskset id `discover()`
# reads out of the manifest, so adding an environment is one appended function at the end of
# this file and no edit anywhere else in the repository.
_PROBES: dict[str, Callable[[], dict[str, float]]] = {}
def probes(name: str):
def register(fn: Callable[[], dict[str, float]]) -> Callable[[], dict[str, float]]:
if name in _PROBES:
raise RuntimeError(f"two probes are registered for {name}")
_PROBES[name] = fn
return fn
return register
TASKS = 24 TASKS = 24
@probes("redaction-pressure")
def redaction() -> dict[str, float]: def redaction() -> dict[str, float]:
from redaction_pressure.corpus import build_slices from redaction_pressure.corpus import build_slices
from redaction_pressure.scan import measure, parse_rules from redaction_pressure.scan import measure, parse_rules
@@ -126,6 +184,7 @@ def redaction() -> dict[str, float]:
} }
@probes("canary-trap")
def canary() -> dict[str, float]: def canary() -> dict[str, float]:
from canary_trap.corpus import SUBJECTS, build from canary_trap.corpus import SUBJECTS, build
from canary_trap.scan import measure, parse_probes from canary_trap.scan import measure, parse_probes
@@ -149,6 +208,7 @@ def canary() -> dict[str, float]:
} }
@probes("fault-localisation")
def fault() -> dict[str, float]: def fault() -> dict[str, float]:
from fault_localisation.incident import build, loudest from fault_localisation.incident import build, loudest
@@ -174,6 +234,7 @@ def fault() -> dict[str, float]:
} }
@probes("schema-migration")
def migration() -> dict[str, float]: def migration() -> dict[str, float]:
from schema_migration.run import measure from schema_migration.run import measure
@@ -206,6 +267,7 @@ def migration() -> dict[str, float]:
} }
@probes("bot-detection")
def bots() -> dict[str, float]: def bots() -> dict[str, float]:
from bot_detection.accounts import build_slices from bot_detection.accounts import build_slices
from bot_detection.scan import ( from bot_detection.scan import (
@@ -327,6 +389,7 @@ def bots() -> dict[str, float]:
} }
@probes("grand-exchange")
def exchange() -> dict[str, float]: def exchange() -> dict[str, float]:
from grand_exchange.book import ( from grand_exchange.book import (
BUY_BAND, FILL_SHARE, MAX_ITEM_SHARE, MAX_ORDERS, MIN_CROSSINGS, SELL_BAND, TAX, BUY_BAND, FILL_SHARE, MAX_ITEM_SHARE, MAX_ORDERS, MIN_CROSSINGS, SELL_BAND, TAX,
@@ -605,6 +668,7 @@ def exchange() -> dict[str, float]:
} }
@probes("drop-table-inference")
def drops() -> dict[str, float]: def drops() -> dict[str, float]:
from drop_table_inference.estimate import ( from drop_table_inference.estimate import (
_tail_loss, measure, parse_estimate, reference_estimate, _tail_loss, measure, parse_estimate, reference_estimate,
@@ -749,15 +813,29 @@ def drops() -> dict[str, float]:
def main() -> int: def main() -> int:
results = { discovered = discover()
"redaction-pressure": redaction(), if not discovered:
"canary-trap": canary(), print("no environments under environments/*/pyproject.toml — the scan is empty, which is"
"fault-localisation": fault(), " a broken checkout and not a clean run", file=sys.stderr)
"schema-migration": migration(), return 1
"bot-detection": bots(),
"grand-exchange": exchange(), orphans = sorted(set(_PROBES) - set(discovered))
"drop-table-inference": drops(), if orphans:
} print(f"probe registered for {', '.join(orphans)}, which no manifest declares — a"
" renamed or deleted environment", file=sys.stderr)
return 1
# A WARNING and not exit 1, deliberately. A new environment's `pyproject.toml` lands before
# its probe does, several sessions share this repository, and a hard gate here turns CI red
# for all of them the moment someone scaffolds a directory. This becomes `return 1` in the
# same commit that lands `exchange_live()` — the last environment that has a manifest and no
# probe. Until then an ungated environment is loud but not fatal.
missing = sorted(set(discovered) - set(_PROBES))
if missing:
print(f"warning: no probe is registered for {', '.join(missing)} — NOT GATED",
file=sys.stderr)
results = {name: _PROBES[name]() for name in sorted(discovered) if name in _PROBES}
print(f"{'environment':22}{'inaction':>10}{'crude':>9}{'plausible':>11}{'oracle':>9} verdict") print(f"{'environment':22}{'inaction':>10}{'crude':>9}{'plausible':>11}{'oracle':>9} verdict")
failed = False failed = False
for name, r in results.items(): for name, r in results.items():
+73 -21
View File
@@ -1,24 +1,47 @@
from __future__ import annotations from __future__ import annotations
import re
import subprocess import subprocess
import sys import sys
import tomllib
import unittest import unittest
from pathlib import Path from pathlib import Path
ROOT = Path(__file__).resolve().parents[1] ROOT = Path(__file__).resolve().parents[1]
# Every environment probe.py gates. A new environment that is not added here still has to ROW = re.compile(r"^(?P<name>\S(?:.*\S)?)\s{2,}(?:-?\d+\.\d+\s*){4}\s+(?P<verdict>.+?)$")
# print "ok", because the count assertion below counts them — so this list going stale fails
# the build rather than quietly narrowing what the test covers. # The environments that are allowed to carry no probe yet, because their taskset layer is not
ENVIRONMENTS = ( # written. Everything discovered and NOT in here MUST be gated.
"redaction-pressure", #
"canary-trap", # This list is the floor, and it is why it exists rather than a bare count: without it, both
"fault-localisation", # loops below are vacuous when nothing is gated, and CI goes green while gating zero
"schema-migration", # environments. Commenting out a single `@probes(...)` decorator used to pass — the degated
"bot-detection", # name simply moved into the warning line, which already legitimately carries five names.
"grand-exchange", #
"drop-table-inference", # It only ever shrinks. Delete a name here in the same commit that lands its probe.
) PENDING_PROBES = frozenset({
"grand-exchange-live",
"tera-crow-nav",
"tera-drive-101",
"tera-office-nav",
"tera-california-flight",
})
def discovered_tasksets() -> set[str]:
"""The same denominator `probe.py:discover()` uses, derived independently of it.
Deriving it a second time here rather than importing `probe` is the point: if discovery
and the manifests ever disagree, this test is what says so. A directory with no
`pyproject.toml` is not an environment to either of us.
"""
names: set[str] = set()
for manifest in sorted((ROOT / "environments").glob("*/pyproject.toml")):
data = tomllib.loads(manifest.read_text(encoding="utf-8"))
declared = data.get("tool", {}).get("arena", {}).get("tasksets")
names.update(declared or [data["project"]["name"]])
return names
class RootProbeIntegrationTests(unittest.TestCase): class RootProbeIntegrationTests(unittest.TestCase):
@@ -28,18 +51,47 @@ class RootProbeIntegrationTests(unittest.TestCase):
cwd=ROOT, cwd=ROOT,
text=True, text=True,
capture_output=True, capture_output=True,
timeout=30, # The probe sweeps constants and runs multi-block ladders, and the interactive
# environments will make it dearer still. A tight timeout here only ever fails a
# slow machine, never a wrong reward.
timeout=180,
check=False, check=False,
) )
self.assertEqual( detail = f"\nstdout:\n{completed.stdout}\nstderr:\n{completed.stderr}"
completed.returncode, self.assertEqual(completed.returncode, 0, msg=f"root probe failed:{detail}")
0,
msg=f"root probe failed:\nstdout:\n{completed.stdout}\nstderr:\n{completed.stderr}", discovered = discovered_tasksets()
self.assertTrue(discovered, msg="no environment manifests found — the scan is empty")
rows = {}
for line in completed.stdout.splitlines()[1:]:
match = ROW.match(line.rstrip())
if match:
rows[match["name"]] = match["verdict"].strip()
for name, verdict in rows.items():
with self.subTest(environment=name):
self.assertIn(name, discovered, msg=f"probe gated {name}, no manifest declares it")
self.assertEqual(verdict, "ok", msg=f"{name} is not ok:{detail}")
# Anything discovered and not gated has to be named in the warning. That warning is
# temporary — it becomes a hard exit 1 in probe.py once the last unregistered
# environment lands its probe — and this assertion holds either way.
ungated = discovered - set(rows)
for name in sorted(ungated):
with self.subTest(environment=name):
self.assertIn(name, completed.stderr,
msg=f"{name} is neither gated nor reported as ungated:{detail}")
# The floor. Being named in the warning is not a licence to stop being gated: an
# environment may only be ungated if it is on the pending list. Without this, removing
# a `@probes(...)` decorator moves the name from the table into the warning and every
# assertion above stays vacuously true.
self.assertLessEqual(
ungated, PENDING_PROBES,
msg=f"these were gated and no longer are: {sorted(ungated - PENDING_PROBES)}{detail}",
) )
for environment in ENVIRONMENTS: self.assertTrue(rows, msg=f"the probe gated nothing at all:{detail}")
with self.subTest(environment=environment):
self.assertIn(environment, completed.stdout)
self.assertEqual(completed.stdout.count(" ok"), len(ENVIRONMENTS))
if __name__ == "__main__": if __name__ == "__main__":