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:
@@ -62,7 +62,10 @@ the first time and this is what caught it:
|
||||
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
|
||||
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
|
||||
"""
|
||||
@@ -76,25 +79,80 @@ import random
|
||||
import re
|
||||
import statistics
|
||||
import sys
|
||||
import tomllib
|
||||
import types
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
|
||||
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
|
||||
# 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
|
||||
# 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.
|
||||
for _pkg in ("redaction_pressure", "canary_trap", "fault_localisation", "schema_migration",
|
||||
"bot_detection", "grand_exchange", "drop_table_inference"):
|
||||
_shim = types.ModuleType(_pkg)
|
||||
_shim.__path__ = [str(ENVS / _pkg / _pkg)]
|
||||
sys.modules[_pkg] = _shim
|
||||
for _paths in discover().values():
|
||||
for _path in _paths:
|
||||
_shim = types.ModuleType(_path.name)
|
||||
_shim.__path__ = [str(_path)]
|
||||
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
|
||||
|
||||
|
||||
@probes("redaction-pressure")
|
||||
def redaction() -> dict[str, float]:
|
||||
from redaction_pressure.corpus import build_slices
|
||||
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]:
|
||||
from canary_trap.corpus import SUBJECTS, build
|
||||
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]:
|
||||
from fault_localisation.incident import build, loudest
|
||||
|
||||
@@ -174,6 +234,7 @@ def fault() -> dict[str, float]:
|
||||
}
|
||||
|
||||
|
||||
@probes("schema-migration")
|
||||
def migration() -> dict[str, float]:
|
||||
from schema_migration.run import measure
|
||||
|
||||
@@ -206,6 +267,7 @@ def migration() -> dict[str, float]:
|
||||
}
|
||||
|
||||
|
||||
@probes("bot-detection")
|
||||
def bots() -> dict[str, float]:
|
||||
from bot_detection.accounts import build_slices
|
||||
from bot_detection.scan import (
|
||||
@@ -327,6 +389,7 @@ def bots() -> dict[str, float]:
|
||||
}
|
||||
|
||||
|
||||
@probes("grand-exchange")
|
||||
def exchange() -> dict[str, float]:
|
||||
from grand_exchange.book import (
|
||||
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]:
|
||||
from drop_table_inference.estimate import (
|
||||
_tail_loss, measure, parse_estimate, reference_estimate,
|
||||
@@ -749,15 +813,29 @@ def drops() -> dict[str, float]:
|
||||
|
||||
|
||||
def main() -> int:
|
||||
results = {
|
||||
"redaction-pressure": redaction(),
|
||||
"canary-trap": canary(),
|
||||
"fault-localisation": fault(),
|
||||
"schema-migration": migration(),
|
||||
"bot-detection": bots(),
|
||||
"grand-exchange": exchange(),
|
||||
"drop-table-inference": drops(),
|
||||
}
|
||||
discovered = discover()
|
||||
if not discovered:
|
||||
print("no environments under environments/*/pyproject.toml — the scan is empty, which is"
|
||||
" a broken checkout and not a clean run", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
orphans = sorted(set(_PROBES) - set(discovered))
|
||||
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")
|
||||
failed = False
|
||||
for name, r in results.items():
|
||||
|
||||
Reference in New Issue
Block a user