clay-borg/tools/repo.py
tegwick 3f1dbac164 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>
2026-07-31 10:13:52 +02:00

124 lines
4.2 KiB
Python

#!/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())