fix(#4351): run-scope preserved review evidence (#4713)

* fix(#4351): run-scope preserved review evidence

The preserve block copied lane output into a flat .review-diagnostics/
using each file's source basename. A lane slug is stable across runs, so
the destination was stable across runs too -- and cp over an existing
file is a success, so a second review of the same phase destroyed the
first run's evidence with no error and no warning, in the one directory
that exists to outlive the rm -rf beside it.

Copy into one subdirectory per run instead. $RUN_DIR is mktemp -d, so
its basename is already unique per run by construction; the UTC stamp in
front is only a sort key and is omitted if date fails. Destination-only:
nothing inside $RUN_DIR is renamed, because both
prepare_trimmed_prompt_for_reviewer and the lane invocation resolver
depend on those exact basenames.

Verified by extracting the real fence and running it twice against one
phase dir: before, one report survived and it was run 2's; after, both.

Emitted-Drift-Ack-Growth: review.md — per-run diagnostics subdirectory plus the comment explaining why uniqueness cannot come from the clock
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(#4351): cover repeated runs, resolve through the run subdir

Adds the two-run rows the issue asks for: both runs' reports recoverable,
and a lane failing identically twice leaving two stubs. Both assert on
CONTENT, not a file count -- a clobber producing the same number of
files would pass a count-only check, and the defect is that run 1's
bytes were replaced.

runWriteReviewsFlow gains an optional phaseDir so a caller can run the
flow twice against one phase directory, which is the only arrangement
that can observe the overwrite. Omitted, it mints a fresh one as before.

The existing rows asserted a flat readdir of the diagnostics root, which
the fix makes stale. They now resolve through preservedPath/preservedNames
so they keep asserting WHICH files were preserved rather than silently
becoming assertions about the layout; the layout is pinned once,
explicitly, by oneRunSubdirectoryPerRun_4351.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* chore(#4351): add changeset fragment

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* chore(#4351): name the run, not the clock, in the changeset

Adversarial review: the fragment said each run gets its own "timestamped
subdirectory", which reads as though the timestamp provides the
separation. It does not -- collision-safety is mktemp's random basename,
and the stamp is a sort key that is dropped entirely when date fails.
The code comment already said so; the user-facing text now agrees.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* chore(#4351): backfill the changeset PR number

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: sim <sim@local>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Tom Boucher
2026-09-13 23:58:38 -04:00
committed by GitHub
parent 1110c3b4ee
commit ec22377a9d
3 changed files with 182 additions and 15 deletions

View File

@@ -0,0 +1,5 @@
---
type: Fixed
pr: 4713
---
**Repeated reviews no longer overwrite each other's preserved evidence** — a second review of the same phase silently replaced the first run's lane reports and error stubs in `.review-diagnostics/`, because the copy used each file's lane-named basename and a lane slug is stable across runs. Each run's evidence now lands in its own subdirectory, named after that run, so repeat reviews accumulate diagnostics instead of clobbering them. (#4351)

View File

@@ -892,11 +892,28 @@ done
_PRESERVE_OK=true
if [ ${#_DIAG_MD[@]} -gt 0 ] || [ ${#_DIAG_ERR[@]} -gt 0 ]; then
if mkdir -p "$DIAG_DIR"; then
if [ ${#_DIAG_MD[@]} -gt 0 ] && ! cp "${_DIAG_MD[@]}" "$DIAG_DIR/"; then
# #4351: ONE SUBDIRECTORY PER RUN. A flat copy used each file's SOURCE basename,
# and a lane slug is stable across runs — so a second review of the same phase
# silently overwrote the first run's evidence for any lane that ran both times.
# `cp` over an existing file is a success, so this lost data with no warning, in
# the one directory whose entire purpose is to outlive the `rm -rf` below.
#
# $RUN_DIR is `mktemp -d .../gsd-review-XXXXXX`, so its basename is already
# unique per run BY CONSTRUCTION — uniqueness never depends on the clock. The
# UTC stamp is only a sort key in front of it, and is omitted entirely if `date`
# fails. Lane basenames are unchanged INSIDE the subdirectory, so evidence still
# correlates back to its lane, and nothing inside $RUN_DIR is renamed (both
# `prepare_trimmed_prompt_for_reviewer` and the lane invocation resolver depend
# on those exact basenames).
_DIAG_STAMP="$(date -u +%Y%m%dT%H%M%SZ 2>/dev/null || true)"
_DIAG_RUN_DIR="$DIAG_DIR/${_DIAG_STAMP:+${_DIAG_STAMP}-}$(basename "$RUN_DIR")"
# `mkdir -p` still creates $DIAG_DIR itself, so a plain FILE sitting at
# $DIAG_DIR still fails here and still skips the `rm -rf` (#3885).
if mkdir -p "$_DIAG_RUN_DIR"; then
if [ ${#_DIAG_MD[@]} -gt 0 ] && ! cp "${_DIAG_MD[@]}" "$_DIAG_RUN_DIR/"; then
_PRESERVE_OK=false
fi
if [ ${#_DIAG_ERR[@]} -gt 0 ] && ! cp "${_DIAG_ERR[@]}" "$DIAG_DIR/"; then
if [ ${#_DIAG_ERR[@]} -gt 0 ] && ! cp "${_DIAG_ERR[@]}" "$_DIAG_RUN_DIR/"; then
_PRESERVE_OK=false
fi
else
@@ -907,7 +924,7 @@ fi
if [ "$_PRESERVE_OK" = "true" ]; then
rm -rf "$RUN_DIR"
else
echo "WARNING: evidence preservation to $DIAG_DIR failed — leaving the un-preserved run directory intact at: $RUN_DIR" >&2
echo "WARNING: evidence preservation to ${_DIAG_RUN_DIR:-$DIAG_DIR} failed — leaving the un-preserved run directory intact at: $RUN_DIR" >&2
fi
```
</step>

View File

@@ -694,14 +694,57 @@ function extractPresentResultsPreserveAndCleanupBash() {
* "not a directory" for every caller, root included, because it is not a
* permissions check at all.
*/
// #4351: preserved evidence now lands in a PER-RUN SUBDIRECTORY under
// `.review-diagnostics/`. A flat copy used each file's SOURCE basename, and a
// lane slug is stable across runs, so a second review of the same phase silently
// overwrote the first run's evidence.
//
// These helpers resolve THROUGH that subdirectory layer on purpose. Rows that
// assert WHICH files were preserved should keep asserting exactly that; without
// them each such row would quietly become an assertion about the directory
// LAYOUT instead, and would have to be rewritten again the next time the layout
// moves. The layout itself is pinned once, explicitly, by
// `oneRunSubdirectoryPerRun_4351`.
function preservedRunDirs(result) {
if (!fs.existsSync(result.diagDir)) return [];
return fs.readdirSync(result.diagDir)
.map((name) => path.join(result.diagDir, name))
.filter((p) => fs.statSync(p).isDirectory())
.sort();
}
function preservedNames(result) {
return preservedRunDirs(result)
.flatMap((dir) => fs.readdirSync(dir))
.sort();
}
// Resolves a preserved basename to its real path. When the file was NOT
// preserved this deliberately returns a path that does not exist, so a negative
// assertion (`existsSync(...) === false`) still reads naturally at the call site.
function preservedPath(result, name) {
for (const dir of preservedRunDirs(result)) {
const candidate = path.join(dir, name);
if (fs.existsSync(candidate)) return candidate;
}
return path.join(result.diagDir, name);
}
function runWriteReviewsFlow(t, opts) {
const scriptDir = createTempDir('gsd-3352-script-');
const runDir = createTempDir('gsd-3352-rundir-');
const phaseDir = createTempDir('gsd-3352-phasedir-');
// #4351: an explicit phaseDir lets a caller run this flow TWICE against the
// SAME phase directory, which is the only way to observe a second run
// clobbering the first run's preserved evidence. Omitted (every pre-existing
// caller) it mints a fresh one exactly as before.
const phaseDir = opts.phaseDir || createTempDir('gsd-3352-phasedir-');
t.after(() => {
cleanup(scriptDir);
cleanup(runDir);
cleanup(phaseDir);
// A caller-supplied phaseDir is owned by the caller — cleaning it here would
// destroy run 1's evidence before run 2 could be asserted against it.
if (!opts.phaseDir) cleanup(phaseDir);
});
if (opts.blockDiagDirWithFile) {
@@ -934,15 +977,15 @@ describe('#3352 per-lane evidence survives cleanup (R3)', () => {
assert.equal(result.runDirExists, false, 'the run dir must still be destroyed');
assert.equal(result.diagDirExists, true, 'diagnostics must have been preserved somewhere under phase_dir');
const codexMd = path.join(result.diagDir, 'gsd-review-codex.md');
const codexErr = path.join(result.diagDir, 'gsd-review-codex.err');
const claudeErr = path.join(result.diagDir, 'gsd-review-claude.err');
const codexMd = preservedPath(result, 'gsd-review-codex.md');
const codexErr = preservedPath(result, 'gsd-review-codex.err');
const claudeErr = preservedPath(result, 'gsd-review-claude.err');
assert.ok(fs.existsSync(codexMd), 'codex .md evidence must be preserved');
assert.equal(fs.readFileSync(codexMd, 'utf-8'), LANE_FAILURE_MD('codex'));
assert.ok(fs.existsSync(codexErr), 'codex non-empty .err evidence must be preserved');
assert.ok(fs.existsSync(claudeErr), 'claude non-empty .err evidence must be preserved');
const geminiErr = path.join(result.diagDir, 'gsd-review-gemini.err');
const geminiErr = preservedPath(result, 'gsd-review-gemini.err');
assert.equal(fs.existsSync(geminiErr), false, 'an empty .err must not be copied as if it were real evidence');
});
});
@@ -1045,9 +1088,7 @@ describe('#4097 the run\'s own input copies are not preserved as diagnostics', (
assert.equal(result.runDirExists, false, 'successful preservation must still clean up the run dir');
assert.equal(result.diagDirExists, true, 'real lane evidence must still be preserved');
const preserved = fs.existsSync(result.diagDir)
? fs.readdirSync(result.diagDir).sort()
: [];
const preserved = preservedNames(result);
// Only the lane's own output belongs in the diagnostics folder.
assert.deepEqual(
preserved,
@@ -1055,13 +1096,13 @@ describe('#4097 the run\'s own input copies are not preserved as diagnostics', (
`diagnostics must hold exactly the lane report and stderr sidecar, not the run's input copies; got: ${preserved.join(', ')}`,
);
assert.equal(
fs.readFileSync(path.join(result.diagDir, 'gsd-review-claude.md'), 'utf-8'),
fs.readFileSync(preservedPath(result, 'gsd-review-claude.md'), 'utf-8'),
'# Claude review\nok\n',
'the lane report must be preserved byte-identically',
);
for (const inputName of Object.keys(INPUT_COPIES_4097)) {
assert.equal(
fs.existsSync(path.join(result.diagDir, inputName)),
fs.existsSync(preservedPath(result, inputName)),
false,
`input copy ${inputName} must NOT be swept into .review-diagnostics/ (#4097)`,
);
@@ -1117,3 +1158,107 @@ describe('#3352 preserved evidence is never swept into the commit (N6)', () => {
assert.equal(afterFiles.split(/\s+/).length, 1, `--files must carry exactly one path; got: ${afterFiles}`);
});
});
// ─── #4351: repeated runs must accumulate evidence, never clobber it ────────
//
// The preserve block copies lane output into `.review-diagnostics/` using each
// file's SOURCE basename (`gsd-review-<slug>.md` / `.err`). A lane slug is
// stable across runs, so before this fix the destination path was stable across
// runs too — and `cp` over an existing file is a SUCCESS, so a second review of
// the same phase destroyed the first run's evidence with no error and no
// warning, in the one directory that exists to outlive `rm -rf "$RUN_DIR"`.
//
// These rows run the REAL extracted fence twice against ONE phase directory,
// which is the only arrangement that can observe the overwrite.
describe('#4351 repeated review runs accumulate evidence instead of clobbering it', () => {
test('twoRunsOnOnePhaseBothPreserved_4351', (t) => {
const sharedPhaseDir = createTempDir('gsd-4351-phasedir-');
t.after(() => cleanup(sharedPhaseDir));
const first = runWriteReviewsFlow(t, {
phaseDir: sharedPhaseDir,
selected: 'codex',
jsonlLines: [{ slug: 'codex' }],
lanes: { codex: { md: '# run one report\n', err: 'run one stderr\n' } },
});
assert.equal(first.outcome, 'exited');
assert.equal(first.diagDirExists, true, 'run 1 must preserve its evidence');
const second = runWriteReviewsFlow(t, {
phaseDir: sharedPhaseDir,
selected: 'codex',
jsonlLines: [{ slug: 'codex' }],
lanes: { codex: { md: '# run two report\n', err: 'run two stderr\n' } },
});
assert.equal(second.outcome, 'exited');
// Assert on CONTENT, not on a file count: a clobber that happened to produce
// the same number of files would pass a count-only check, and the whole
// defect is that run 1's bytes were replaced by run 2's.
const runDirs = preservedRunDirs(second);
assert.equal(
runDirs.length, 2,
`each run must own a distinct subdirectory; got ${runDirs.length}: ${runDirs.map((d) => path.basename(d)).join(', ')}`,
);
const reports = runDirs
.map((dir) => path.join(dir, 'gsd-review-codex.md'))
.filter((p) => fs.existsSync(p))
.map((p) => readFileNormalized(p));
assert.deepEqual(
reports.sort(),
['# run one report\n', '# run two report\n'].sort(),
'BOTH runs\' reports must be recoverable — before #4351 only the second survived',
);
});
test('identicalLaneFailureTwiceLeavesTwoStubs_4351', (t) => {
// The 2nd acceptance criterion, and the worst case for a flat layout: the
// SAME lane failing the SAME way twice produces byte-identical basenames.
const sharedPhaseDir = createTempDir('gsd-4351-phasedir-err-');
t.after(() => cleanup(sharedPhaseDir));
const opts = {
phaseDir: sharedPhaseDir,
selected: 'claude',
jsonlLines: [],
lanes: { claude: { md: '# lane failed\n', err: 'identical stack trace\n' } },
};
runWriteReviewsFlow(t, opts);
const second = runWriteReviewsFlow(t, opts);
const stubs = preservedRunDirs(second)
.map((dir) => path.join(dir, 'gsd-review-claude.err'))
.filter((p) => fs.existsSync(p));
assert.equal(
stubs.length, 2,
'two identical failures must leave two independently recoverable stubs',
);
for (const stub of stubs) {
assert.equal(readFileNormalized(stub), 'identical stack trace\n');
}
});
test('oneRunSubdirectoryPerRun_4351', (t) => {
// Pins the LAYOUT itself, once, so the helpers above stay honest: a single
// run creates exactly one subdirectory, the lane basenames are unchanged
// INSIDE it (nothing in $RUN_DIR was renamed — two consumers depend on those
// names), and no evidence is left sitting flat in the diagnostics root.
const result = runWriteReviewsFlow(t, {
selected: 'codex',
jsonlLines: [{ slug: 'codex' }],
lanes: { codex: { md: '# only run\n', err: 'only stderr\n' } },
});
const runDirs = preservedRunDirs(result);
assert.equal(runDirs.length, 1, 'one run must create exactly one subdirectory');
assert.deepEqual(
fs.readdirSync(runDirs[0]).sort(),
['gsd-review-codex.err', 'gsd-review-codex.md'],
'lane basenames must be unchanged inside the run subdirectory',
);
const flat = fs.readdirSync(result.diagDir)
.filter((name) => fs.statSync(path.join(result.diagDir, name)).isFile());
assert.deepEqual(flat, [], 'no evidence may be left flat in the diagnostics root');
});
});