From f59a592e4931b7f1a94750ab4ac24d7e89d9965c Mon Sep 17 00:00:00 2001 From: Volt Date: Mon, 19 Oct 2026 18:56:07 +0000 Subject: [PATCH] tools (volt): verify.py --repo asks git for the work-tree root, so the gate runs inside `git worktree add` (rc 2 -> rc 0; selftest 42 -> 50 cells) The repo test was `os.path.isdir(/.git)`, which is wrong in both directions. A LINKED worktree's `.git` is a 47-byte FILE (`gitdir: ...`), so `git worktree add --detach ` produced a checkout the gate refused with rc 2 "not a git repository" -- measured at cf5101f: the committed blob rc 2, the fixed blob rc 0 with four legs PASS in the same tree, and 5 of 5 legs PASS with --fast off (selftest-sweep: 15 tools, 15 OK, 0 fatal). An empty directory named `.git` was the same test's false positive: it passed while git refused the path. `resolve_repo()` asks `git -C rev-parse --show-toplevel`: it succeeds in every work tree (main, linked, submodule) and fails in a bare repo or a bare `.git` directory. `--repo` stays the ROOT -- a subdirectory is rc 2 naming the root it would have graded, because widening it silently would grade a tree nobody named. Legs, rcs and --queue/--index semantics are untouched. Selftest: _fixture() now `git init`s a real work tree (it used to create an empty `.git/`, a repository only to the test being replaced) and takes commit=True where a cell needs a HEAD to branch from. New cells 23/23b/23c (a linked worktree is graded, and the row names the worktree -- RED on the old blob), 24/24b (subdirectory refused, root named), 25/25b (empty .git refused), 26 (bare repo refused). 42 -> 50 cells, 50/50, rc 0. No queue item opened or closed; no another-party file edited. --- checkins/2026-10-19-volt.md | 58 +++++++++++++++ tools/verify.py | 140 +++++++++++++++++++++++++++++++++--- 2 files changed, 188 insertions(+), 10 deletions(-) diff --git a/checkins/2026-10-19-volt.md b/checkins/2026-10-19-volt.md index 3590b93..a9ef9c5 100644 --- a/checkins/2026-10-19-volt.md +++ b/checkins/2026-10-19-volt.md @@ -412,3 +412,61 @@ wiring step, and the 07:34Z block already records it as an open question. ## Ask None. No queue item opened or closed. + +--- + +## Did (fifth tick, 18:50–19:0xZ) — the gate runs inside a worktree: ask git, not `isdir(.git)` + +Reply 36 ended with a real limitation found by hitting it: from +`git worktree add --detach `, `tools/verify.py` returned **rc 2 "cannot +read repo: … is not a git repository"**, because line 404 asked +`os.path.isdir(/.git)` and a linked worktree's `.git` is a **47-byte +file** holding `gitdir: /root/troupe-standup/.git/worktrees/ts`. The rc-2 +contract behaved correctly — could-not-grade, never a silent pass — but the +consequence was that a lane which sandboxed correctly could not run the operator +gate at all, and had to choose between grading unsandboxed in the shared clone +and reporting rc 2. + +**Measured before the change, on `cf5101f`, same tree, two blobs** +(`before/red-green.txt`): the committed blob (`git show HEAD:tools/verify.py`) +— rc **2**, one line of stderr. The fixed blob — rc **0**, four legs PASS, one +SKIP, `index current … 10171 lines / 1308413 bytes`. Full (non-`--fast`) gate in +that same worktree: **5 of 5 legs pass**, `selftest-sweep … 15 tools, 15 OK, +0 fatal`, 36.0 s. + +**The fix is one function and no special case.** `resolve_repo()` asks +`git -C rev-parse --show-toplevel`. It succeeds in every work tree — main, +linked, submodule — and fails in a bare repository or a directory that merely +holds a `.git` entry, which is the second half of the same defect: an **empty +`.git/` directory passed the old test** on a path git itself refuses. `--repo` +is still the ROOT: a subdirectory is rc 2 **naming the root it would have +graded**, because widening it silently would grade a tree nobody named. The leg +table, the rcs and the five legs are untouched; `--queue`/`--index` stay +repo-relative for `git show :`. + +**The selftest grew by eight cells, and the fixture got honest.** `_fixture()` +used to create an empty `.git/` — a repository only to the test being replaced — +and now runs `git init`, with `commit=True` when a cell needs a HEAD to branch a +worktree from. New cells: **23/23b/23c** a linked worktree is graded and the row +names the worktree (RED on the old blob, GREEN here), **24/24b** a subdirectory +is refused with the root named, **25/25b** an empty `.git` directory is refused, +**26** a bare repository is refused. `--selftest` **42 → 50 cells, 50/50, rc 0**. + +**Refusals measured, each rc 2** (`before/refusals.txt`): subdirectory +(`--repo takes the root`), empty `.git` (`fatal: not a git repository`), bare +(`fatal: this operation must be run in a work tree`), missing path +(`is not a directory`). + +## Blocked + +Nothing. + +## Next + +- Re-run the gate at the commit this lands in, so the fix is graded by the + revision that carries it rather than by the working tree (doing that now). +- No queue item opened, closed, or claimed; no another-party file edited. + +## Ask + +None. diff --git a/tools/verify.py b/tools/verify.py index 0d5e666..79540bf 100644 --- a/tools/verify.py +++ b/tools/verify.py @@ -116,7 +116,23 @@ TOOLS ARE RESOLVED FROM --repo, NOT FROM THIS FILE'S DIRECTORY docs/queue-index.md) because `queue_append_audit` reads them with `git show :`, which has no absolute-path form. -`--selftest` fixture corpus, 42 cells +--repo IS A WORK TREE, ASKED OF GIT + Until 2026-10-19 the repo test was `os.path.isdir(/.git)`, and it was + wrong in both directions. A LINKED worktree's `.git` is a 47-byte file holding + `gitdir: ...`, so from `git worktree add --detach ` the gate returned + **rc 2 "not a git repository"** against a checkout whose every leg passes -- + measured that day on `cf5101f`: the old test rc 2, the test that follows rc 0 + with five legs green, `selftest-sweep` included. That made the gate + unrunnable in the one sandbox a lane gets for free and forced the choice + between grading unsandboxed in the shared clone and not running it at all. + An empty directory named `.git` was the same test's false positive: it passed + while git itself refused the path. `resolve_repo` now asks + `git rev-parse --show-toplevel`, which succeeds in every work tree (main, + linked, submodule) and fails in a bare repository or a bare `.git` directory. + The path must still be the ROOT: a subdirectory is rc 2 naming the root, + because widening `--repo` silently would grade a tree nobody named. + +`--selftest` fixture corpus, 50 cells """ import argparse @@ -222,6 +238,48 @@ def pick_detail(text, limit=150, summary=False): return kept[-1][:limit] if kept else "" +def resolve_repo(path): + """`--repo` is a git WORK TREE ROOT. Return (repo, why-not), asking git. + + Until 2026-10-19 this test was `os.path.isdir(/.git)`, which is the + wrong question in both directions, and both were measured on that day: + + * a LINKED worktree's `.git` is a 47-byte FILE holding `gitdir: ...`, so + `git worktree add --detach` produced a checkout every leg would have + graded and the gate refused with rc 2 "not a git repository" -- the one + sandbox a lane gets for free was the one place the operator gate could + not run, and the alternatives were grading unsandboxed in the shared + clone or not running the gate at all; + * an empty directory named `.git` passed the same test on a path git + itself refuses, so the refusal was also not worth the name. + + `git rev-parse --show-toplevel` answers the real question: it succeeds in + every work tree (main, linked, submodule) and fails in a bare repository or + a directory that merely holds a `.git` entry. The path must still be the + ROOT -- a subdirectory is rc 2 with the root named, because widening + `--repo` silently would grade a different tree than the caller named. + """ + repo = os.path.abspath(path) + if not os.path.isdir(repo): + return None, "cannot read repo: %s is not a directory" % repo + try: + proc = subprocess.run(["git", "-C", repo, "rev-parse", "--show-toplevel"], + capture_output=True, text=True) + except OSError as exc: + return None, ("cannot read repo: %s (git is not runnable: %s)" + % (repo, exc)) + top = (proc.stdout or "").strip() + if proc.returncode != 0 or not top: + why = [l for l in (proc.stderr or "").strip().splitlines() if l.strip()] + return None, ("cannot read repo: %s is not a git work tree (%s)" + % (repo, why[-1] if why else + "git rev-parse rc %d" % proc.returncode)) + if os.path.realpath(top) != os.path.realpath(repo): + return None, ("cannot read repo: %s is inside the work tree rooted at " + "%s; --repo takes the root" % (repo, top)) + return repo, None + + def newest_checkin_day(repo): """The newest `` prefix in checkins/ -> (day, why-not). @@ -397,12 +455,9 @@ def main(argv=None): if a.selftest: return selftest() - repo = os.path.abspath(a.repo) - if not os.path.isdir(repo): - print("cannot read repo: %s is not a directory" % repo, file=sys.stderr) - return 2 - if not os.path.isdir(os.path.join(repo, ".git")): - print("cannot read repo: %s is not a git repository" % repo, file=sys.stderr) + repo, why = resolve_repo(a.repo) + if why: + print(why, file=sys.stderr) return 2 rows = [] @@ -444,17 +499,38 @@ def _stub(path, rc, out="", sleep=0.0, marker=None, argv_marker=None): os.chmod(path, 0o755) +def _git_init(repo, commit=False): + """Make `repo` a REAL work tree; with commit=True, one commit at HEAD. + + The fixture used to create an empty `.git/` directory, which is not a + repository -- it only looked like one to the `isdir(.git)` test this gate + replaced. The fixture is now as real as what it grades, so cell 25 can grade + the empty directory as the refusal it always was. + """ + subprocess.run(["git", "init", "-q", repo], check=True, + capture_output=True, text=True) + if commit: + subprocess.run(["git", "-C", repo, "add", "-A"], check=True, + capture_output=True, text=True) + subprocess.run(["git", "-C", repo, + "-c", "user.name=verify-selftest", + "-c", "user.email=selftest@invalid", + "-c", "commit.gpgsign=false", + "commit", "-q", "-m", "fixture"], check=True, + capture_output=True, text=True) + + def _fixture(base, rcs, outs=None, sleep=0.0, markers=None, argv_markers=None, - checkins=("2026-01-01-fixture.md",)): + checkins=("2026-01-01-fixture.md",), commit=False): """A throwaway repo with stubbed legs. rcs: {tool: rc}. `checkins` seeds `checkins/`: one dated file by default (so the check-in leg has a day to grade), a tuple of names for cells that need a specific set, - and None for a repo with no `checkins/` directory at all. + and None for a repo with no `checkins/` directory at all. `commit` makes one + commit at HEAD, which a cell needs before it can `git worktree add`. """ repo = os.path.join(base, "repo") os.makedirs(os.path.join(repo, "tools")) - os.makedirs(os.path.join(repo, ".git")) with open(os.path.join(repo, QUEUE_DEFAULT), "w") as fh: fh.write("## Q1 -- fixture\n") os.makedirs(os.path.join(repo, "docs")) @@ -473,6 +549,7 @@ def _fixture(base, rcs, outs=None, sleep=0.0, markers=None, argv_markers=None, out=(outs or {}).get(tool, ""), sleep=sleep, marker=(markers or {}).get(tool), argv_marker=(argv_markers or {}).get(tool)) + _git_init(repo, commit=commit) return repo @@ -733,6 +810,49 @@ def selftest(): "tool: 1 new fault(s)\n"), "cannot grade: baseline x: No such file") + # 23. THE SANDBOX THE GATE REFUSED. A linked worktree's `.git` is a FILE + # (`gitdir: ...`), not a directory, so `os.path.isdir(repo/.git)` -- the + # test this gate used until 2026-10-19 -- answered "not a git repository" + # (rc 2) on a checkout every leg would have graded. Measured RED against + # the old test on this exact fixture, GREEN here. + repo = _fixture(os.path.join(td, "c23"), GREEN5, commit=True) + wt = os.path.join(td, "c23-wt") + subprocess.run(["git", "-C", repo, "worktree", "add", "--detach", wt, + "HEAD"], check=True, capture_output=True, text=True) + with open(os.path.join(wt, ".git")) as fh: + gitfile = fh.read() + check("23 the fixture worktree really carries a .git file, not a dir", + gitfile.startswith("gitdir: ") and not os.path.isdir(wt + "/.git"), + True) + rc, out = _verify("--repo", wt, "--fast") + check("23b a linked worktree is graded, not refused -> rc 0", rc, 0) + check("23c the row names the worktree, not the tree it links to", + "verify -- repo: %s" % os.path.abspath(wt) in out, True) + + # 24. --repo stays the ROOT: a subdirectory is refused with the root + # named, because widening it silently would grade a tree nobody asked for + rc, out = _verify("--repo", os.path.join(repo, "tools"), "--fast") + check("24 a subdirectory is refused, not widened -> rc 2", rc, 2) + check("24b the refusal names the root it would have graded", + "--repo takes the root" in out and os.path.realpath(repo) in out, + True) + + # 25. the OLD test's false positive: an empty `.git` directory is not a + # repository, and git says so where `isdir` said otherwise. + fake = os.path.join(td, "c25") + os.makedirs(os.path.join(fake, ".git")) + rc, out = _verify("--repo", fake, "--fast") + check("25 an empty '.git' directory is not a repo -> rc 2", rc, 2) + check("25b and the refusal says git refused it", + "is not a git work tree" in out and "cannot read repo:" in out, True) + + # 26. a bare repository has no work tree to grade + bare = os.path.join(td, "c26.git") + subprocess.run(["git", "init", "-q", "--bare", bare], check=True, + capture_output=True, text=True) + rc, out = _verify("--repo", bare, "--fast") + check("26 a bare repository -> rc 2", rc, 2) + print("selftest: %d cells, %d passed, %d failed" % (cells, passed, len(failures))) for f in failures: print(" FAIL %s" % f)