From 3f1dbac1644e6e24ac335c5a9c8bfd62b51ed966 Mon Sep 17 00:00:00 2001 From: tegwick Date: Fri, 31 Jul 2026 10:13:52 +0200 Subject: [PATCH] CB-WP-0004 T01: fix environment friction at the root MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 ` 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 --- Makefile | 83 +++++++++++----- README.md | 25 +++++ tools/__pycache__/repo.cpython-312.pyc | Bin 0 -> 6463 bytes tools/cb-cost.py | 3 +- tools/dep-weight.py | 21 +++- tools/loop-lint.py | 2 +- tools/repo.py | 124 ++++++++++++++++++++++++ tools/rule-coverage.py | 5 + workplans/CB-WP-0004-mechanical-work.md | 21 +++- 9 files changed, 254 insertions(+), 30 deletions(-) create mode 100644 tools/__pycache__/repo.cpython-312.pyc create mode 100644 tools/repo.py diff --git a/Makefile b/Makefile index 7674ec5..98cf81e 100644 --- a/Makefile +++ b/Makefile @@ -1,73 +1,112 @@ # One command surface (InnerLoop §agentic-efficiency #3). Deterministic, # greppable output; precursor of the `cb` CLI. +# +# CB-WP-0004 T01: every target here runs from a clean shell, from any +# directory, with no prefix. Invoke as `make -C ` from +# elsewhere. No target requires `cd` or `export PATH` — CB-RES-0003 +# measured 84 turns and $15.33 spent on exactly those two prefixes. -CARGO := cargo +# Absolute path to this Makefile's directory, so recipes never depend on +# the caller's working directory. +REPO := $(patsubst %/,%,$(dir $(abspath $(lastword $(MAKEFILE_LIST))))) -.PHONY: check test sim bench bench-test coverage dep-weight cost cost-test cost-pin cost-budget cost-mix loop-lint self-tests loc all +# Locate cargo instead of requiring it on the inherited PATH. Mirrors +# tools/repo.py:cargo_bin() — kept in sync by `make env-test`. +CARGO := $(firstword $(shell command -v cargo 2>/dev/null) \ + $(wildcard $(HOME)/.cargo/bin/cargo) \ + $(wildcard /usr/local/cargo/bin/cargo) \ + cargo) +export PATH := $(dir $(CARGO)):$(PATH) + +PY := python3 +TOOLS := $(REPO)/tools + +# Every cargo recipe runs at the repo root; the shell does not persist cd. +IN_REPO := cd $(REPO) && + +.PHONY: check test sim bench bench-test coverage dep-weight cost cost-test cost-pin cost-budget cost-mix loop-lint self-tests env-test loc all ## fmt + clippy (deny warnings) + HashMap deny-lint check: - $(CARGO) fmt --all --check - $(CARGO) clippy --workspace --all-targets -- -D warnings + $(IN_REPO) $(CARGO) fmt --all --check + $(IN_REPO) $(CARGO) clippy --workspace --all-targets -- -D warnings ## unit + scenario-format tests test: - $(CARGO) test --workspace + $(IN_REPO) $(CARGO) test --workspace ## run all GROUND scenarios through cb-sim dep-weight: - python3 tools/dep-weight.py + $(PY) $(TOOLS)/dep-weight.py coverage: - python3 tools/rule-coverage.py + $(PY) $(TOOLS)/rule-coverage.py # M-D2-CST (specs/CostAccounting.md). cost-test is the positive control and # runs first: a cost number from an unverified collector is void. cost: cost-test - python3 tools/cb-cost.py --composition --by-task + $(PY) $(TOOLS)/cb-cost.py --composition --by-task cost-test: - python3 tools/cb-cost.py --self-test + $(PY) $(TOOLS)/cb-cost.py --self-test # InnerLoop rules that are mechanically checkable (CB-WP-0003 T01). loop-lint: - python3 tools/loop-lint.py + $(PY) $(TOOLS)/loop-lint.py # Positive control for every reporting tool, per InnerLoop v1.1 Step 5. self-tests: - python3 tools/cb-cost.py --self-test - python3 tools/loop-lint.py --self-test - python3 tools/rule-coverage.py --self-test - python3 tools/dep-weight.py --self-test + $(PY) $(TOOLS)/cb-cost.py --self-test + $(PY) $(TOOLS)/loop-lint.py --self-test + $(PY) $(TOOLS)/rule-coverage.py --self-test + $(PY) $(TOOLS)/dep-weight.py --self-test + $(PY) $(TOOLS)/repo.py --self-test + +# T01 positive control: prove the environment fix, do not assume it. Runs +# every tool from a foreign working directory with a PATH that has no +# cargo on it. Before T01 this failed; if it fails again, the friction is +# back and CB-EV-0003's measurement is invalid. +env-test: + @cd / && env PATH=/usr/bin:/bin $(PY) $(TOOLS)/repo.py --self-test + @cd / && env PATH=/usr/bin:/bin $(PY) $(TOOLS)/rule-coverage.py --self-test >/dev/null \ + && echo " [ok ] rule-coverage runs from / with no cargo on PATH" + @cd / && env PATH=/usr/bin:/bin $(PY) $(TOOLS)/dep-weight.py --self-test >/dev/null \ + && echo " [ok ] dep-weight runs from / with no cargo on PATH" + @cd / && env PATH=/usr/bin:/bin $(PY) $(TOOLS)/cb-cost.py --self-test >/dev/null \ + && echo " [ok ] cb-cost runs from / with no cargo on PATH" + @cd / && env PATH=/usr/bin:/bin $(PY) $(TOOLS)/loop-lint.py --self-test >/dev/null \ + && echo " [ok ] loop-lint runs from / with no cargo on PATH" + @$(MAKE) -C $(REPO) coverage >/dev/null \ + && echo " [ok ] make -C works from any directory" # CB-01/CB-02: live spend since the last commit. cost-budget: cost-test - python3 tools/cb-cost.py --budget + $(PY) $(TOOLS)/cb-cost.py --budget # CB-RES-0003 baseline: mechanical vs judgment turns. cost-mix: cost-test - python3 tools/cb-cost.py --composition + $(PY) $(TOOLS)/cb-cost.py --composition cost-pin: cost-test - python3 tools/cb-cost.py --pin fc76445 --composition --by-task + $(PY) $(TOOLS)/cb-cost.py --pin fc76445 --composition --by-task sim: - $(CARGO) run -q -p cb-sim -- scenarios/ground/*.yaml + $(IN_REPO) $(CARGO) run -q -p cb-sim -- $(REPO)/scenarios/ground/*.yaml ## Criterion benches (AM-6/AM-7) bench: - $(CARGO) bench -p games-ground + $(IN_REPO) $(CARGO) bench -p games-ground ## InnerLoop positive control: run every bench once, no measurement. ## Fails if a workload stalls or produces the wrong event count. bench-test: - $(CARGO) bench -p games-ground --bench synthetic -- --test + $(IN_REPO) $(CARGO) bench -p games-ground --bench synthetic -- --test ## AM-2/AM-3 input: source LOC per crate (excludes tests would need tokei) loc: - @for d in crates/cb-kernel crates/cb-events crates/cb-game-runtime games/ground tools/cb-sim; do \ + @$(IN_REPO) for d in crates/cb-kernel crates/cb-events crates/cb-game-runtime games/ground tools/cb-sim; do \ printf '%-28s %s\n' $$d "$$(find $$d/src -name '*.rs' | xargs cat | grep -vcE '^\s*(//|$$)')"; \ done -all: check test sim coverage dep-weight self-tests loop-lint bench-test +all: check test sim coverage dep-weight self-tests env-test loop-lint bench-test diff --git a/README.md b/README.md index 5f57961..b86f1d0 100644 --- a/README.md +++ b/README.md @@ -4,4 +4,29 @@ A rebuild from scratch simulation and games engine framework set up to assimilat Licensed under the Target Revenue Source License (TRSL V1C1) — see [LICENSE](LICENSE); canonical text lives in the org's `target-revenue` repository. +## Running the gates + +```sh +make all # every gate, from a clean shell +make -C /path/to/clay-borg all # …or from any other directory +``` + +**There is no environment setup step.** No `cd`, no `export PATH`, no +activation script. `make` locates the repo from its own path and `cargo` +from the standard rustup locations; the Python tools do the same via +`tools/repo.py`. The only prerequisites are a rustup toolchain and Python +3.11+. + +This is deliberate and enforced: `make env-test` runs every tool from `/` +with a PATH containing no cargo, and `make all` includes it. CB-RES-0003 +measured 84 agent turns and $15.33 spent prefixing commands with `cd` and +`export PATH` before that friction was fixed at the root (CB-WP-0004 T01). + +Other useful targets: `make cost` (spend per task), `make cost-budget` +(spend since the last commit), `make cost-mix` (mechanical vs judgment +turns), `make loop-lint` (executable InnerLoop rules), `make self-tests` +(every tool's positive control). + +## GROUND + The first product vertical is a virtual tabletop implementation of **GROUND — A Game of Bonds and Rivalry: DARVO Edition**. The boardgame itself (rules, editions, content) is at home in the sister repository **`ground-game`** — that repo is authoritative for what GROUND *is*; clay-borg implements the engine that runs it. \ No newline at end of file diff --git a/tools/__pycache__/repo.cpython-312.pyc b/tools/__pycache__/repo.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0ef261cea9e749079c86b2ace27d72680fcda723 GIT binary patch literal 6463 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 J#GPCT{|R-oy1oDa literal 0 HcmV?d00001 diff --git a/tools/cb-cost.py b/tools/cb-cost.py index a895f0f..dd328c4 100644 --- a/tools/cb-cost.py +++ b/tools/cb-cost.py @@ -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") diff --git a/tools/dep-weight.py b/tools/dep-weight.py index f5df6b3..3a62388 100755 --- a/tools/dep-weight.py +++ b/tools/dep-weight.py @@ -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() diff --git a/tools/loop-lint.py b/tools/loop-lint.py index d57b9a0..82e4163 100644 --- a/tools/loop-lint.py +++ b/tools/loop-lint.py @@ -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 diff --git a/tools/repo.py b/tools/repo.py new file mode 100644 index 0000000..5bcffcc --- /dev/null +++ b/tools/repo.py @@ -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()) diff --git a/tools/rule-coverage.py b/tools/rule-coverage.py index 109f9ed..9809dbf 100755 --- a/tools/rule-coverage.py +++ b/tools/rule-coverage.py @@ -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() diff --git a/workplans/CB-WP-0004-mechanical-work.md b/workplans/CB-WP-0004-mechanical-work.md index ea3fe1b..c8f28f2 100644 --- a/workplans/CB-WP-0004-mechanical-work.md +++ b/workplans/CB-WP-0004-mechanical-work.md @@ -1,7 +1,7 @@ --- id: CB-WP-0004 title: "Move mechanical turns off the token budget, and prove it worked" -status: proposed +status: in_progress state_hub_workstream_id: "6880ac78-d817-41b9-b267-f12ff9deea28" --- @@ -41,7 +41,7 @@ unless the instrument disproved it (§Step 4, correction vs retarget). ```task id: CB-WP-0004-T01 -status: todo +status: done priority: high state_hub_task_id: "3ddfd3e2-8596-4969-a069-09577933fcbc" ``` @@ -64,6 +64,23 @@ still cheaper than a regression. **Predicted:** environment-setup turns → **< 10** (from 84), **$12–15** recovered. Highest confidence in the review. +**Delivered.** `tools/repo.py` resolves the repo root from `__file__` and +`cargo` from PATH-then-rustup-locations; every tool imports it, so `REPO` +is now stated once rather than four times. The Makefile derives `REPO` +from `MAKEFILE_LIST` and resolves `CARGO` the same way. `make env-test` +is the positive control — it runs every tool from `/` with +`PATH=/usr/bin:/bin`, and is wired into `make all`, so this cannot +silently regress and invalidate T05's measurement. + +The leaf workaround in `dep-weight.py` was kept, not deleted: it is now +unreachable on a rustup machine, and `--self-test` asserts `cargo_bin()` +resolves unaided. Per the task text, a control that never fires is +cheaper than a regression. + +`loop-lint` failed on `repo.py` immediately — a new tool with a positive +control but no `--self-test` entry point. Second time the gate has caught +its own pass's work within the hour. + ## Task: `make task-done` — one command for a task close ```task