chore(config): T-1258 — tooling/ becomes an importable package
The skeleton the reach CLI hangs off. Nothing moves yet: this adds the package, the bounded core/, and explicit setuptools discovery. core/console.py is the single output path, and the split it enforces is the whole design — stdout carries the command's actual output so `reach ... | jq` keeps working, stderr carries the event stream as JSONL. Rendering happens at the sink: a terminal gets human text, anything else gets raw JSONL, so a live view and a job log are one artefact in two presentations. Emitting is optional — the gates emit nothing — and verdict() prints once, last, carrying its remedy as a structured field. core/config.py resolves the repo root from __file__ against a project.yaml sentinel, with an SR_REPO_ROOT override. No subprocess and no git call: this is on the gate path, and cwd is not a reliable signal anyway since a hook runs from the root and an agent call may not. Both paths are validated, because a silent fallback is how you end up editing one checkout and checking another. Discovery is configured explicitly rather than left to flat-layout auto-discovery, which would have had to choose between erroring on the ambiguity and quietly shipping client/ or docs/. Verified: top_level.txt contains exactly "tooling". Verified beyond the happy path — the sentinel rejects SR_REPO_ROOT=/tmp and names both remedies; debug events are suppressed at the default threshold while the verdict is not; stdout stays clean with stderr redirected away; and the three unconditional push-gate checks still pass now that tooling/ is a package, which was the real regression risk. Two findings recorded on the tickets. make setup-venv is stale — it calls .venv/bin/pip, but the venv was created by uv and has no pip, so the recorded procedure and the actual state have already diverged (T-1261 owns the fix). And settled-reach-tooling had never actually been installed: site-packages held the dependencies but no dist-info, which follows from there being no __init__.py to expose. This is the first commit where `import tooling` means anything. .venv/ was only ignored via .git/info/exclude, which is machine-local, so a fresh clone or a new worktree did not ignore it at all. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
"""Repo-root and path resolution. No domain logic lives here (D-263).
|
||||
|
||||
This module is on the push-gate path, so it does no subprocess work: no
|
||||
`git rev-parse`, no shelling out, nothing beyond the stdlib. Resolution is
|
||||
`__file__`-relative and validated against a sentinel file. That is not only
|
||||
faster than asking git — it is *more correct*, because the current working
|
||||
directory is not a reliable signal. A git hook runs from the repo root, an agent
|
||||
`Bash` call may not, and `reach` is meant to work from anywhere.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
# The file whose presence proves a directory is the repo root. project.yaml is
|
||||
# the version source of truth (CLAUDE.md), so it is the honest sentinel: if it
|
||||
# is absent, everything downstream was going to fail anyway — better to say so
|
||||
# here, by name, than to hand out a plausible wrong path.
|
||||
SENTINEL = "project.yaml"
|
||||
|
||||
ENV_OVERRIDE = "SR_REPO_ROOT"
|
||||
|
||||
|
||||
def repo_root() -> Path:
|
||||
"""Absolute path to the repo root.
|
||||
|
||||
`SR_REPO_ROOT` wins if set; otherwise the package's own location is used.
|
||||
Both are validated, deliberately: an override pointing somewhere useless
|
||||
should fail loudly rather than fall back to a default that happens to work,
|
||||
because a silent fallback is how you end up editing one checkout and
|
||||
checking another.
|
||||
|
||||
Not cached. The two syscalls are microseconds, and a cache would make the
|
||||
override untestable for the sake of nothing measurable.
|
||||
"""
|
||||
override = os.environ.get(ENV_OVERRIDE)
|
||||
if override:
|
||||
return _validated(Path(override).expanduser().resolve(), f"{ENV_OVERRIDE}={override}")
|
||||
# tooling/core/config.py -> tooling/core -> tooling -> repo root
|
||||
return _validated(Path(__file__).resolve().parents[2], "the installed package location")
|
||||
|
||||
|
||||
def path(*parts: str) -> Path:
|
||||
"""Join `parts` onto the repo root, so callers do not each re-resolve it."""
|
||||
return repo_root().joinpath(*parts)
|
||||
|
||||
|
||||
def _validated(root: Path, source: str) -> Path:
|
||||
if (root / SENTINEL).is_file():
|
||||
return root
|
||||
# Raised as RuntimeError only because core/errors.py does not exist yet;
|
||||
# T-1249 converts this to ReachError(message, fix=...). The message already
|
||||
# follows the contract — it names the command that fixes it.
|
||||
raise RuntimeError(
|
||||
f"cannot locate the repo root: {root} contains no {SENTINEL} "
|
||||
f"(resolved from {source}).\n"
|
||||
f"If reach was installed from a different checkout than the one you are "
|
||||
f"working in, re-point it:\n"
|
||||
f" uv tool install --editable <path-to-repo>\n"
|
||||
f"To override for a single command:\n"
|
||||
f" {ENV_OVERRIDE}=<path-to-repo> reach ..."
|
||||
)
|
||||
Reference in New Issue
Block a user