CB-WP-0004 T01: fix environment friction at the root
CB-RES-0003 measured 84 turns and $15.33 — the largest mechanical category — spent prefixing commands with `cd` and `export PATH="$HOME/.cargo/bin:$PATH"`. Both causes are now fixed once instead of per-leaf. tools/repo.py resolves the repo root from __file__ and cargo from PATH then the standard rustup locations. Every tool imports ROOT from it, so the repo path is stated once rather than redefined in four files — single source of fact, the rule DFD earned in InnerLoop v1.2. rule-coverage and dep-weight now call enter_root(), which is why their relative paths did not need rewriting one by one. The Makefile derives REPO from MAKEFILE_LIST and resolves CARGO the same way, so `make -C <repo> <target>` works from any directory with no prefix. make env-test is the positive control, and is in `make all`: every tool runs from / with PATH=/usr/bin:/bin. Without it this fix could regress silently and invalidate T05's measurement — the whole point of the control loop. dep-weight's "cargo not on PATH" error is kept rather than deleted. It should now be unreachable, and --self-test asserts cargo_bin() resolves unaided; a control that never fires is cheaper than a regression. loop-lint failed on repo.py on its first run — a reporting tool with a positive control but no --self-test entry point. Second time the gate has caught work from its own pass within the hour. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
578dcbea78
commit
3f1dbac164
9 changed files with 254 additions and 30 deletions
BIN
tools/__pycache__/repo.cpython-312.pyc
Normal file
BIN
tools/__pycache__/repo.cpython-312.pyc
Normal file
Binary file not shown.
|
|
@ -40,7 +40,8 @@ except ModuleNotFoundError: # pragma: no cover - py<3.11
|
|||
print("ERROR: needs Python 3.11+ for tomllib", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
from repo import ROOT as REPO # noqa: E402 (single source of fact, T01)
|
||||
|
||||
PRICES = os.path.join(REPO, "benchmarks", "baselines", "model-prices.toml")
|
||||
COMPONENTS = ("input", "output", "cache_read", "write_5m", "write_1h")
|
||||
TASK_RE = re.compile(r"\bT\d\d\b")
|
||||
|
|
|
|||
|
|
@ -22,10 +22,11 @@ Usage: python3 tools/dep-weight.py [--json] [--self-test]
|
|||
import glob
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
from repo import cargo_bin, enter_root
|
||||
|
||||
PACKAGE = "games-ground"
|
||||
CONFIGS = {
|
||||
"shipped-runtime": ["--no-default-features"],
|
||||
|
|
@ -42,14 +43,19 @@ TARGETS = {
|
|||
|
||||
def crates(extra_args):
|
||||
"""Third-party crates in the normal (non-dev) dependency graph."""
|
||||
if not shutil.which("cargo"):
|
||||
# T01: locate cargo rather than demanding the caller export PATH. The
|
||||
# error below is kept as a positive control — it should now be
|
||||
# unreachable on a machine with rustup installed, and a control that
|
||||
# never fires is still cheaper than a regression.
|
||||
cargo = cargo_bin()
|
||||
if not cargo:
|
||||
print(
|
||||
"ERROR: cargo not on PATH. Try: export PATH=\"$HOME/.cargo/bin:$PATH\"",
|
||||
"ERROR: cargo not found on PATH or in ~/.cargo/bin — is rustup installed?",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
out = subprocess.run(
|
||||
["cargo", "tree", "-p", PACKAGE, "--edges", "normal", "--prefix", "none"]
|
||||
[cargo, "tree", "-p", PACKAGE, "--edges", "normal", "--prefix", "none"]
|
||||
+ extra_args,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
|
|
@ -114,6 +120,11 @@ def self_test():
|
|||
|
||||
# Targets must be present and numeric — a missing target would make
|
||||
# the breach check vacuous.
|
||||
# T01: this tool is useless without cargo, and used to demand the caller
|
||||
# put it on PATH. Assert it resolves unaided.
|
||||
check("cargo resolves without caller PATH setup", bool(cargo_bin()),
|
||||
cargo_bin() or "NOT FOUND")
|
||||
|
||||
check("targets defined for every configuration",
|
||||
set(TARGETS) == set(CONFIGS) and all(
|
||||
isinstance(v, int) and v > 0 for v in TARGETS.values()),
|
||||
|
|
@ -129,6 +140,8 @@ def self_test():
|
|||
|
||||
|
||||
def main():
|
||||
# T01: `cargo tree` and the own-source walk are both repo-relative.
|
||||
enter_root()
|
||||
if "--self-test" in sys.argv:
|
||||
return self_test()
|
||||
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ import os
|
|||
import re
|
||||
import sys
|
||||
|
||||
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
from repo import ROOT as REPO # noqa: E402 (single source of fact, T01)
|
||||
LOADABILITY_LIMIT = 400
|
||||
|
||||
# Artifact classes the loop produces. history/ is an append-only trail
|
||||
|
|
|
|||
124
tools/repo.py
Normal file
124
tools/repo.py
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Repo location and toolchain discovery — the root fix for CB-WP-0004 T01.
|
||||
|
||||
CB-RES-0003 measured 84 turns and $15.33 spent on `cd` and
|
||||
`export PATH="$HOME/.cargo/bin:$PATH"`. That friction had two causes:
|
||||
|
||||
1. tools resolved their inputs relative to the *caller's* working
|
||||
directory, so every invocation had to be preceded by a `cd`;
|
||||
2. `cargo` is not on the default PATH, so every Rust-touching command had
|
||||
to be preceded by an `export`.
|
||||
|
||||
Both are fixed here, once, rather than at each leaf. `tools/dep-weight.py`
|
||||
previously carried its own "cargo not on PATH" message — evidence the
|
||||
friction was noticed and patched in the wrong place.
|
||||
|
||||
Single source of fact (InnerLoop v1.2): ROOT is derived here and imported;
|
||||
it is not recomputed per tool.
|
||||
"""
|
||||
import os
|
||||
import shutil
|
||||
|
||||
# tools/repo.py -> tools -> repo root.
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
# Where a rustup install puts the toolchain when PATH has not been set up.
|
||||
CARGO_FALLBACKS = (
|
||||
os.path.expanduser("~/.cargo/bin"),
|
||||
"/usr/local/cargo/bin",
|
||||
)
|
||||
|
||||
|
||||
def enter_root():
|
||||
"""Run from the repo root regardless of where the caller invoked us.
|
||||
|
||||
Every tool calls this first. After it, plain relative paths
|
||||
(`specs/...`, `scenarios/...`) are correct, which is why the tools
|
||||
below did not need rewriting path-by-path.
|
||||
"""
|
||||
os.chdir(ROOT)
|
||||
return ROOT
|
||||
|
||||
|
||||
def cargo_bin():
|
||||
"""Absolute path to cargo, or None.
|
||||
|
||||
Checks PATH first so an explicitly-configured toolchain wins, then the
|
||||
standard rustup locations. Returning None rather than exiting keeps the
|
||||
policy (abort? skip?) with the caller.
|
||||
"""
|
||||
found = shutil.which("cargo")
|
||||
if found:
|
||||
return found
|
||||
for d in CARGO_FALLBACKS:
|
||||
cand = os.path.join(d, "cargo")
|
||||
if os.path.isfile(cand) and os.access(cand, os.X_OK):
|
||||
return cand
|
||||
return None
|
||||
|
||||
|
||||
def cargo_env():
|
||||
"""Environment with the cargo bin dir prepended to PATH.
|
||||
|
||||
For subprocesses that shell out to cargo indirectly. Returns a copy;
|
||||
never mutates os.environ.
|
||||
"""
|
||||
env = dict(os.environ)
|
||||
cargo = cargo_bin()
|
||||
if cargo:
|
||||
env["PATH"] = os.path.dirname(cargo) + os.pathsep + env.get("PATH", "")
|
||||
return env
|
||||
|
||||
|
||||
def self_test():
|
||||
"""Positive control: this module must actually locate things.
|
||||
|
||||
A resolver that silently returns a wrong-but-plausible answer is the
|
||||
harness-does-nothing class applied to paths — every downstream tool
|
||||
would then measure the wrong tree while reporting success.
|
||||
"""
|
||||
results = []
|
||||
|
||||
def check(name, ok, detail=""):
|
||||
results.append((name, ok, detail))
|
||||
|
||||
check("ROOT is a directory", os.path.isdir(ROOT), ROOT)
|
||||
# Identified by content, not by name: a ROOT that pointed at the parent
|
||||
# directory would still be "a directory".
|
||||
check("ROOT is *this* repo",
|
||||
os.path.isfile(os.path.join(ROOT, "Cargo.toml"))
|
||||
and os.path.isdir(os.path.join(ROOT, "specs"))
|
||||
and os.path.isfile(os.path.join(ROOT, "INTENT.md")))
|
||||
# ROOT must not depend on the caller's cwd — the defect this fixes.
|
||||
here = os.getcwd()
|
||||
try:
|
||||
os.chdir("/")
|
||||
again = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
check("ROOT is independent of the caller's cwd", again == ROOT)
|
||||
finally:
|
||||
os.chdir(here)
|
||||
|
||||
cargo = cargo_bin()
|
||||
check("cargo is locatable without PATH setup", bool(cargo), cargo or "NOT FOUND")
|
||||
check("cargo_env puts cargo on PATH",
|
||||
not cargo or os.path.dirname(cargo) in cargo_env()["PATH"].split(os.pathsep))
|
||||
|
||||
print("repo self-test (positive control)")
|
||||
ok = True
|
||||
for name, passed, detail in results:
|
||||
print(f" [{'ok ' if passed else 'FAIL'}] {name}"
|
||||
+ (f" — {detail}" if detail else ""))
|
||||
ok &= passed
|
||||
return 0 if ok else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
# This module has nothing to report but its own controls, so bare
|
||||
# invocation and --self-test are the same run. The flag exists because
|
||||
# `make self-tests` and loop-lint both look for it by name.
|
||||
if len(sys.argv) > 1 and sys.argv[1] not in ("--self-test",):
|
||||
print(f"usage: {sys.argv[0]} [--self-test]", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
sys.exit(self_test())
|
||||
|
|
@ -24,6 +24,8 @@ import glob
|
|||
import re
|
||||
import sys
|
||||
|
||||
from repo import enter_root
|
||||
|
||||
RULE_RE = r"\*\*(GR-[A-Z]+\d+)"
|
||||
COVERS_RE = r"covers: \[(.*?)\]"
|
||||
AGGREGATE = "games/ground/src/lib.rs"
|
||||
|
|
@ -107,6 +109,9 @@ def self_test():
|
|||
|
||||
|
||||
def main():
|
||||
# T01: inputs are repo-relative, so anchor to the repo rather than
|
||||
# requiring the caller to `cd` first.
|
||||
enter_root()
|
||||
if "--self-test" in sys.argv:
|
||||
return self_test()
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue