fix(gate): review round 3 --- canonical ancestry, guard witnesses, and a withdrawn claim
**THE ANCESTOR WALK WAS WRONG TWICE OVER.** `for _anc in $(...)` word-splits on IFS, so a gate root containing a SPACE was torn into fragments and the real ancestor never tested --- the check passed on exactly the path it should reject. And `dirname` walks LEXICAL ancestry while `detect_project` canonicalizes, so a symlinked root hid a marker the editor plainly sees. The walk resolves with `pwd -P` first and iterates a quoted `while`; both shapes are verified by hand (space-containing root refused, symlinked root refused at its real path). **THE 103-BYTE GUARD HAD NO WITNESS AT ALL** --- every other row runs with a short root, so the guard is silent and a broken one looked identical. Three rows now aim at it deliberately: boundary rejection and acceptance, a MULTIBYTE root (each `é` is one character and two bytes, so it is rejected only if the guard measures bytes), and **rejection must reap both created areas**, which is the leak the early trap exists to prevent. **The `Cargo.toml`-DIRECTORY case was claimed and not covered**, and the consequence is exactly as review predicted: reverting only the language-marker arm to `[ -e ]` stayed green. The marker-type row now drives all three shapes, and `M-G-5` --- that precise revert --- fails it. **Prose brought level with the implementation.** The framing, the handoff and the ledger all said 108; the supported floor is **103 usable bytes**, Darwin's 104-byte array minus its NUL. The ledger also still said `<pid>`, the superseded 21/30 reserve, and `M-G-1`. **And the ruling said nested gates "do not pay" the reserve, which is false and would have licensed exempting them.** They pay it in full; the short layout merely gives them the headroom to satisfy an unchanged production guard. Reworded, because the wrong version is the one a future reader would act on. **THE btrfs CAUSAL CLAIM IS WITHDRAWN.** The draft argued that a one-second deadline plus a slower filesystem was a plausible new mechanism for the fourth `managed_retry` occurrence. It does not survive inspection: the deadline bounds the connection RETRY loop, not the socketpair handshake that returned `BrokenPipe`, and the filesystem work happens before it is armed --- the tempdir is created and never bound. The environmental change is still recorded, as a CHANGE rather than a mechanism, so a later occurrence can compare like with like. Recording a mechanism the code does not support is worse than recording none: the next occurrence gets measured against a story instead of the evidence. TMPDIR stays disk-backed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
This commit is contained in:
parent
465acae547
commit
0a04d55a35
|
|
@ -288,7 +288,8 @@ from #171 and #215.
|
|||
responsibility to `TMPDIR` rather than adding a feature, which is why
|
||||
it amends this document instead of opening a new one.
|
||||
- **What it does:** every gate invocation gets a fresh, disk-backed
|
||||
`TMPDIR` at `<gate-root>/tmp/<pid>`, exported once so every stage and
|
||||
`TMPDIR` at `<gate-root>/tmp/<mktemp>`, exported once so every stage
|
||||
and
|
||||
every process they spawn inherits it, and reaped by the same exit trap
|
||||
as the ambient root. **A gate run no longer needs a `TMPDIR=`
|
||||
override.**
|
||||
|
|
@ -296,8 +297,11 @@ from #171 and #215.
|
|||
ANCESTOR marker — project detection walks upward — so a fresh
|
||||
subdirectory of `/tmp` inherits `/tmp`'s ancestors and the same stray
|
||||
`.git`. The directory had to move somewhere the gate already owns.
|
||||
- **`SUN_LEN` is the constraint that shaped the layout, and the fix's
|
||||
own gate run found it.** A Unix socket path cannot exceed 108 bytes
|
||||
- **The socket-path limit shaped the layout, and the fix's own gate run
|
||||
found it.** The budget is the **supported-platform floor of 103
|
||||
usable bytes** — Darwin's 104-byte array minus its terminating NUL,
|
||||
not Linux's 108, because a Linux-derived limit passes where it is
|
||||
written and bind-fails on the macOS leg. A path cannot exceed that
|
||||
and the suites bind sockets inside `TMPDIR`. The first placement,
|
||||
`$TARGET/gate-tmp/$STAMP-$$`, produced a 114-byte socket path and
|
||||
failed **six** daemon and attach tests with *"path must be shorter
|
||||
|
|
@ -308,16 +312,25 @@ from #171 and #215.
|
|||
socket failures deep in a suite name a limit, not a cause. The guard
|
||||
fails immediately with the path, its length and what to shorten.
|
||||
**Its reserve is measured, not round**: the longest suffix a fixture
|
||||
appends is 21 bytes, so 30 leaves ~40% headroom. An earlier
|
||||
"generous" 45 **fired on the gate's own behaviour tests**, which run
|
||||
the gate inside the gate and so reach 71 bytes — a guard that rejects
|
||||
a legitimate configuration fails on every run rather than a rare one.
|
||||
- **Witnesses (2), each mutation-checked:** `M-G-1` removes the export
|
||||
→ the propagation row alone; `M-G-2` stops the reaping → the cleanup
|
||||
row alone. Propagation is observed in a **spawned child** (the
|
||||
self-test's first step reports its own `$TMPDIR` into its log),
|
||||
because the gate exporting a variable would only prove the gate can
|
||||
export a variable.
|
||||
appends is **33** bytes (`/.tmpXXXXXX/directory-target.sock`), so
|
||||
**48** leaves ~45% headroom. Two earlier values were wrong in
|
||||
OPPOSITE directions — a "generous" 45 that fired on the gate's own
|
||||
behaviour tests, then a 30 that sat **below the real maximum** and
|
||||
would have passed 76–78-byte paths. The suite now roots its nested
|
||||
gates at a short base so it can SATISFY the unchanged production
|
||||
guard rather than be exempted from it.
|
||||
- **Witnesses, each mutation-checked.** `M-G-1b` keeps the assignment
|
||||
and removes only `export` → the propagation row; its predecessor
|
||||
`M-G-1` deleted both and so never proved inheritance. `M-G-2` stops
|
||||
the reaping → the cleanup row. `M-G-3` removes the ancestor check →
|
||||
the refusal row. `M-G-4` reverts to existence-only, and `M-G-5`
|
||||
reverts **only** the language-marker arm → the marker-type row, which
|
||||
is why that row covers a `Cargo.toml` **directory** as well as both
|
||||
`.git` shapes. `M-G-6` counts characters → the multibyte row.
|
||||
`M-G-7` moves the trap back after the guards → the
|
||||
rejection-cleanup row. Propagation is observed in a **spawned
|
||||
child**, because the gate exporting a variable would only prove the
|
||||
gate can export a variable.
|
||||
- **Proved against the live hazard:** `/tmp/.git` is still present on
|
||||
this machine, and the tests it reddened now pass with **no override**.
|
||||
- **Gates:** `./scripts/gate --acceptance gate_script_acceptance`, run
|
||||
|
|
|
|||
|
|
@ -283,8 +283,12 @@ commands, read `docs/active-work.md` immediately after this file.
|
|||
**FIXED 2026-08-13: `scripts/gate` now isolates `TMPDIR`.** Each
|
||||
invocation gets a directory created fresh by `mktemp -d` under
|
||||
**`<gate-root>/tmp/`** — the shared gate root, not the per-worktree
|
||||
target, because a Unix socket path cannot exceed **`SUN_LEN`** (108
|
||||
bytes) and the suites bind sockets inside `TMPDIR`. It is exported
|
||||
target, because a Unix socket path cannot exceed `sun_path` and the
|
||||
suites bind sockets inside `TMPDIR`. **The budget is the
|
||||
supported-platform floor: 103 usable bytes** — Darwin's 104-byte
|
||||
array minus its terminating NUL, not Linux's 108, because a
|
||||
Linux-derived limit passes where it is written and bind-fails on
|
||||
the macOS leg. It is exported
|
||||
once so every stage and every process they spawn inherits it, and
|
||||
reaped by the same exit trap as the ambient root. **A gate run no
|
||||
longer needs a `TMPDIR=` override.**
|
||||
|
|
|
|||
|
|
@ -552,27 +552,32 @@ is empty) and adds **no test to that binary**. Occurrence 3 excluded
|
|||
reproduces the signature with *nothing added to the binary*, which is
|
||||
independent corroboration rather than a repeat of the same argument.
|
||||
|
||||
**AND ONE THING IT INTRODUCES, NAMED RATHER THAN DISMISSED — the
|
||||
observing lane is not environmentally neutral even though it is
|
||||
code-neutral.** That lane moves the gate's `TMPDIR` off `/tmp`, which
|
||||
on this machine changes the filesystem underneath every
|
||||
`tempfile::tempdir()` in the run **from tmpfs to btrfs**. This test
|
||||
creates a tempdir and runs a handshake against a **one-second
|
||||
deadline** (`pmacs-gpu/src/attach.rs`). A slower filesystem under a
|
||||
timing-bounded test, under full-sweep load, is a plausible mechanism
|
||||
and **it did not exist in occurrences 1–3**.
|
||||
**The observing lane is environmentally non-neutral, and that is
|
||||
recorded as a CHANGE rather than as a mechanism.** It moves the gate's
|
||||
`TMPDIR` off `/tmp`, which on this machine puts every
|
||||
`tempfile::tempdir()` in the run on btrfs instead of tmpfs. Noted so a
|
||||
later occurrence can compare like with like.
|
||||
|
||||
It is not established either: the failing I/O is on a `UnixStream::pair`,
|
||||
not on the tempdir path, and the tempdir is created but never bound.
|
||||
**What this row must not do is book the occurrence as "the usual flake"
|
||||
when the observing lane changed the very conditions the flake is
|
||||
sensitive to.**
|
||||
**A causal claim built on that was advanced here and is WITHDRAWN.**
|
||||
The draft argued the test's one-second deadline plus a slower
|
||||
filesystem was a plausible new mechanism. It does not hold on
|
||||
inspection: **the deadline bounds the connection RETRY loop, not the
|
||||
socketpair handshake that returned `BrokenPipe`**, and the filesystem
|
||||
work happens before that deadline is armed. The tempdir is created and
|
||||
never bound — the failing I/O is on a `UnixStream::pair`. Recording a
|
||||
mechanism that the code does not support is worse than recording none,
|
||||
because the next occurrence gets measured against a story instead of
|
||||
against the evidence.
|
||||
|
||||
**The discriminating comparison for a fifth occurrence** is therefore
|
||||
the same lane's gate run with `TMPDIR` pointed back at a tmpfs — the
|
||||
one variable this lane moves — rather than another merge-base control.
|
||||
Three isolated re-runs on the current tree were green, which by this
|
||||
file's own rule establishes intermittence only.
|
||||
**So the causal status is unchanged by this occurrence: UNRESOLVED,
|
||||
with no new mechanism.** What it adds is the corroboration above. Three
|
||||
isolated re-runs on the current tree were green, which by this file's
|
||||
own rule establishes intermittence only.
|
||||
|
||||
**The discriminating comparison for a fifth occurrence** remains the
|
||||
one the third occurrence prescribed. One occurrence, with no supported
|
||||
mechanism, is not grounds to reverse a fix that closes two observed
|
||||
hazards.
|
||||
|
||||
**Third occurrence — worker identity Stage 1 review round 2,
|
||||
2026-08-09, local (Linux). Same selector, same `gpu`-step flavor, all
|
||||
|
|
|
|||
|
|
@ -452,7 +452,9 @@ Four decisions inside that, each of which had a cheaper wrong answer:
|
|||
therefore the marker. The directory has to sit somewhere with no
|
||||
marker above it.
|
||||
2. **Off the GATE ROOT, not the per-worktree target.** A Unix socket
|
||||
path cannot exceed `SUN_LEN` (108 bytes) and the suites bind sockets
|
||||
path cannot exceed `sun_path` — **103 usable bytes at the supported
|
||||
floor** (Darwin's 104-byte array minus its terminating NUL; Linux's
|
||||
is 108/107) and the suites bind sockets
|
||||
*inside* `TMPDIR`. The per-worktree target is 60 bytes and the gate
|
||||
root 36; the first implementation used the former and produced
|
||||
114-byte socket paths, failing six daemon and attach tests. **The
|
||||
|
|
@ -492,8 +494,10 @@ other, which is the worst place to find out. **The usable PATH length is
|
|||
one less than the array**, because the stored value is NUL-terminated:
|
||||
103 on Darwin, 107 on Linux. The script takes **103**.
|
||||
|
||||
**RULING — a synthetic nested gate does not pay a reserve it never
|
||||
uses.** The reserve exists for fixtures that bind sockets under
|
||||
**RULING — a synthetic nested gate is given room to SATISFY the
|
||||
reserve; it is not exempted from it.** The guard is unchanged for every
|
||||
run, nested or not. What changed is the layout the behaviour suite
|
||||
hands it. The reserve exists for fixtures that bind sockets under
|
||||
`TMPDIR`. This script's own behaviour suite runs *nested* gates whose
|
||||
plans are synthetic (`true`, `false`, one `echo`) and which bind no
|
||||
socket at all, so applying the fixture reserve to them would reject a
|
||||
|
|
@ -516,10 +520,13 @@ rather than the guard weakened**:
|
|||
plans create no markerless fixture — the same reason it may set the
|
||||
ancestor escape.
|
||||
|
||||
**The guard therefore keeps the true maximum for real runs**, and the
|
||||
tests stop paying for a hazard they cannot encounter. If a future
|
||||
behaviour row *does* bind a socket, it must move off the short base and
|
||||
take the reserve with it.
|
||||
**The guard therefore keeps the true maximum for every run**, and the
|
||||
suite simply stops handing it a root that cannot clear it. An earlier
|
||||
draft of this section said nested gates "do not pay" the reserve, which
|
||||
is wrong and would have licensed exempting them: they pay it in full
|
||||
and now have the headroom to afford it. If a future behaviour row binds
|
||||
a socket, it must move off the short base — the reserve already covers
|
||||
it.
|
||||
|
||||
**Escape hatch, documented test-only.**
|
||||
`PMACS_GATE_ALLOW_ANCESTOR_MARKER` exists for this script's own
|
||||
|
|
|
|||
59
scripts/gate
59
scripts/gate
|
|
@ -639,42 +639,45 @@ fi
|
|||
# markerless fixture exists for a marker to re-root. The check itself
|
||||
# is witnessed by a test that deliberately does NOT set this and
|
||||
# asserts the refusal.
|
||||
gate_marker_refusal() {
|
||||
echo "gate: a project marker sits above the gate TMPDIR:" >&2
|
||||
echo "gate: $1" >&2
|
||||
echo "gate: TMPDIR is $GATE_TMPDIR" >&2
|
||||
echo "gate: every markerless test fixture beneath it would be" >&2
|
||||
echo "gate: re-rooted at that directory. Move the gate root" >&2
|
||||
echo "gate: (PMACS_GATE_TARGET_ROOT) somewhere without one." >&2
|
||||
exit 2
|
||||
}
|
||||
|
||||
if [ -z "${PMACS_GATE_ALLOW_ANCESTOR_MARKER:-}" ]; then
|
||||
for _anc in $(
|
||||
_p="$GATE_TMPDIR"
|
||||
while [ "$_p" != "/" ] && [ -n "$_p" ]; do
|
||||
printf '%s\n' "$_p"
|
||||
_p=$(dirname "$_p")
|
||||
done
|
||||
printf '/\n'
|
||||
); do
|
||||
# TYPE MATTERS, and an existence-only test is wrong in both
|
||||
# directions. `match_marker` in `src/project.rs` requires `.git` to
|
||||
# be a DIRECTORY and the seven language markers to be FILES, so
|
||||
# `[ -e ]` would reject ancestors detection itself ignores --- most
|
||||
# importantly a `.git` FILE, which is exactly what a git WORKTREE
|
||||
# has. Every worktree in this repo would have tripped it.
|
||||
# CANONICAL, AND QUOTED. Two defects an obvious loop has:
|
||||
#
|
||||
# * `for _anc in $(...)` WORD-SPLITS on IFS, so a gate root containing
|
||||
# a space is torn into fragments and the real ancestor is never
|
||||
# tested --- the check would pass on exactly the path it should
|
||||
# reject.
|
||||
# * `dirname` walks LEXICAL ancestry. `detect_project` canonicalizes,
|
||||
# so a symlinked root can hide a marker the editor plainly sees.
|
||||
# Resolving first makes the two agree.
|
||||
_anc=$(cd "$GATE_TMPDIR" 2>/dev/null && pwd -P) || _anc="$GATE_TMPDIR"
|
||||
while :; do
|
||||
for _m in Cargo.toml .luarc.json pyproject.toml go.mod deno.json \
|
||||
deno.jsonc package.json; do
|
||||
# TYPE MATTERS, and an existence-only test is wrong in both
|
||||
# directions. `match_marker` in `src/project.rs` requires `.git`
|
||||
# to be a DIRECTORY and the seven language markers to be FILES,
|
||||
# so `[ -e ]` would reject ancestors detection itself ignores.
|
||||
if [ -f "$_anc/$_m" ]; then
|
||||
echo "gate: a project marker sits above the gate TMPDIR:" >&2
|
||||
echo "gate: $_anc/$_m" >&2
|
||||
echo "gate: TMPDIR is $GATE_TMPDIR" >&2
|
||||
echo "gate: every markerless test fixture beneath it would be" >&2
|
||||
echo "gate: re-rooted at that directory. Move the gate root" >&2
|
||||
echo "gate: (PMACS_GATE_TARGET_ROOT) somewhere without one." >&2
|
||||
exit 2
|
||||
gate_marker_refusal "$_anc/$_m"
|
||||
fi
|
||||
done
|
||||
# The one directory-valued marker. A git WORKTREE has a `.git`
|
||||
# FILE, which detection ignores and this must too.
|
||||
if [ -d "$_anc/.git" ]; then
|
||||
echo "gate: a project marker sits above the gate TMPDIR:" >&2
|
||||
echo "gate: $_anc/.git" >&2
|
||||
echo "gate: TMPDIR is $GATE_TMPDIR" >&2
|
||||
echo "gate: every markerless test fixture beneath it would be" >&2
|
||||
echo "gate: re-rooted at that directory. Move the gate root" >&2
|
||||
echo "gate: (PMACS_GATE_TARGET_ROOT) somewhere without one." >&2
|
||||
exit 2
|
||||
gate_marker_refusal "$_anc/.git"
|
||||
fi
|
||||
[ "$_anc" = "/" ] && break
|
||||
_anc=$(dirname "$_anc")
|
||||
done
|
||||
fi
|
||||
|
||||
|
|
|
|||
|
|
@ -694,7 +694,21 @@ fn the_ancestor_check_honours_marker_types() {
|
|||
);
|
||||
|
||||
// The same name as a DIRECTORY is a real marker.
|
||||
// A LANGUAGE marker as a DIRECTORY: detection wants a file, so the
|
||||
// guard must ignore it. Without this row, reverting only the
|
||||
// language-marker arm to `[ -e ]` stays green — the `.git` halves
|
||||
// below constrain the directory-valued marker alone.
|
||||
std::fs::remove_file(&mine).expect("rm git file");
|
||||
let cargo_dir = base.path().join("Cargo.toml");
|
||||
std::fs::create_dir(&cargo_dir).expect("cargo dir");
|
||||
let err = String::from_utf8_lossy(&run_it().stderr).into_owned();
|
||||
assert!(
|
||||
!err.contains(&format!("{}", cargo_dir.display())),
|
||||
"a `Cargo.toml` DIRECTORY is not a marker to detection and must \
|
||||
not be one here; stderr:\n{err}"
|
||||
);
|
||||
std::fs::remove_dir(&cargo_dir).expect("rm cargo dir");
|
||||
|
||||
std::fs::create_dir(&mine).expect("git dir");
|
||||
let out = run_it();
|
||||
let err = String::from_utf8_lossy(&out.stderr).into_owned();
|
||||
|
|
@ -708,6 +722,119 @@ fn the_ancestor_check_honours_marker_types() {
|
|||
);
|
||||
}
|
||||
|
||||
/// The socket-path guard: rejection, acceptance, and cleanup on
|
||||
/// rejection.
|
||||
///
|
||||
/// **The ordinary gate only ever exercises the passing side.** Every
|
||||
/// other row here runs with a short root, so the guard is silent and a
|
||||
/// broken guard would look identical. These construct the boundary
|
||||
/// deliberately.
|
||||
///
|
||||
/// The budget is **103 usable bytes** — Darwin's `sun_path[104]` minus
|
||||
/// its terminating NUL, the supported-platform floor — less a 48-byte
|
||||
/// fixture reserve, so a root whose derived TMPDIR exceeds 55 bytes is
|
||||
/// refused.
|
||||
#[test]
|
||||
fn the_socket_path_guard_rejects_accepts_and_cleans_up() {
|
||||
// The gate appends `/tmp/XXXXXX` (11 bytes) to the root, so a root
|
||||
// of N bytes yields a TMPDIR of N + 11.
|
||||
let over_root = long_root(60);
|
||||
let out = run_guarded(over_root.path());
|
||||
let err = String::from_utf8_lossy(&out.stderr).into_owned();
|
||||
assert!(
|
||||
!out.status.success(),
|
||||
"a root past the budget must be refused; stderr:\n{err}"
|
||||
);
|
||||
assert!(
|
||||
err.contains("too long for a unix socket path"),
|
||||
"and must say why; stderr:\n{err}"
|
||||
);
|
||||
|
||||
// REJECTION MUST NOT LEAK. The guard creates both temporary areas
|
||||
// before it can measure anything, so a rejection that exits before
|
||||
// the trap is armed leaves them behind — on every rejection, which
|
||||
// is worse than the failure it prevents.
|
||||
let leaked: Vec<_> = walkdir_shallow(over_root.path());
|
||||
assert!(
|
||||
leaked.is_empty(),
|
||||
"a rejected run must reap what it created; found {leaked:?}"
|
||||
);
|
||||
|
||||
// Just inside the budget: accepted.
|
||||
let ok_root = long_root(40);
|
||||
let out = run_guarded(ok_root.path());
|
||||
let err = String::from_utf8_lossy(&out.stderr).into_owned();
|
||||
assert!(
|
||||
!err.contains("too long for a unix socket path"),
|
||||
"a root inside the budget must not be refused; stderr:\n{err}"
|
||||
);
|
||||
}
|
||||
|
||||
/// The guard counts BYTES, not characters.
|
||||
///
|
||||
/// `${#var}` counts characters under a UTF-8 locale while `sun_path` is
|
||||
/// byte-limited, so a multibyte path measures short and passes a check
|
||||
/// it should fail. Each `é` here is one character and **two bytes**.
|
||||
#[test]
|
||||
fn the_socket_path_guard_counts_bytes_not_characters() {
|
||||
// Character-length ~34 but byte-length ~68: rejected only if the
|
||||
// guard measures bytes.
|
||||
let root = tempfile::Builder::new()
|
||||
.prefix(&"é".repeat(24))
|
||||
.tempdir_in(short_root_base())
|
||||
.expect("multibyte root");
|
||||
let chars = root.path().to_string_lossy().chars().count();
|
||||
let bytes = root.path().to_string_lossy().len();
|
||||
assert!(
|
||||
bytes > chars,
|
||||
"fixture must actually be multibyte: {chars} chars, {bytes} bytes"
|
||||
);
|
||||
|
||||
let out = run_guarded(root.path());
|
||||
let err = String::from_utf8_lossy(&out.stderr).into_owned();
|
||||
assert!(
|
||||
!out.status.success() && err.contains("too long for a unix socket path"),
|
||||
"a multibyte root over the BYTE budget must be refused — it is \
|
||||
{chars} characters but {bytes} bytes; stderr:\n{err}"
|
||||
);
|
||||
}
|
||||
|
||||
/// A root of exactly `total` bytes, so the boundary can be aimed at.
|
||||
fn long_root(total: usize) -> tempfile::TempDir {
|
||||
let base = short_root_base();
|
||||
// `<base>/<prefix>XXXXXX`
|
||||
let fixed = base.display().to_string().len() + 1 + 6;
|
||||
let pad = total.saturating_sub(fixed);
|
||||
tempfile::Builder::new()
|
||||
.prefix(&"a".repeat(pad))
|
||||
.tempdir_in(&base)
|
||||
.expect("sized root")
|
||||
}
|
||||
|
||||
/// Run the gate with the ancestor escape set (so only the LENGTH guard
|
||||
/// can speak) and no ambient `TMPDIR`.
|
||||
fn run_guarded(root: &Path) -> std::process::Output {
|
||||
std::process::Command::new(gate())
|
||||
.arg("--self-test")
|
||||
.current_dir(repo_root())
|
||||
.env("PMACS_GATE_TARGET_ROOT", root)
|
||||
.env("PMACS_GATE_ALLOW_ANCESTOR_MARKER", "1")
|
||||
.env_remove("TMPDIR")
|
||||
.output()
|
||||
.expect("run gate")
|
||||
}
|
||||
|
||||
/// Entries left under `root/tmp` and any `gate-ambient` leaf.
|
||||
fn walkdir_shallow(root: &Path) -> Vec<PathBuf> {
|
||||
let mut out = Vec::new();
|
||||
for sub in ["tmp"] {
|
||||
if let Ok(rd) = std::fs::read_dir(root.join(sub)) {
|
||||
out.extend(rd.filter_map(Result::ok).map(|e| e.path()));
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// The seam handoff §3 keeps authority over: a script cannot infer
|
||||
/// which acceptance suites a change touched, so it runs what it is
|
||||
/// handed — each one, in order.
|
||||
|
|
|
|||
Loading…
Reference in New Issue