fix(execute): the step loop survives an adversarial review
An independent review of abc3083 found 15 defects, the two worst verified
in scratch repos. Each fix started with a test that failed.
- Proof of red could be "proven" by the environment: a start point
unpacked with git archive had no node_modules and no .git, so any test
failed there. The start point is now a git clone --shared checked out at
start_sha, gitignored top-level entries are linked in, and a control run
(start point + the change) must pass, or the result is not-run.
- The Test first file was never committed (the template lists it apart
from Files:) and leaked into the next step's diff. The step's scope is
now Files: + test files, staged at the checkpoint.
- New files outside Files: were invisible to every check. Every untracked
file is registered with --intent-to-add; a revert unregisters them.
- The floor guard paired renames (foo.test.js -> .bak read as clean) and
missed skipIf and node's t.skip/t.todo; prose files no longer count as
stubs.
- Phase 7.5 audited from the last step's start_sha; it now uses the run's
session_start_sha.
- The done line is written after the checkpoint and checked with --commit.
- fix_rounds never passes 5; attempts are never reset; re-control keeps
the spec/quality verdicts and earlier floor-guard rulings; the closed
stop list maps every existing stop; failed vs stopped made consistent;
the controller's effort is said, not assumed.
Tests: 1470 (1468 pass, 0 fail, 2 skipped).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
382d5ccba2
commit
44dcc779a9
11 changed files with 466 additions and 148 deletions
|
|
@ -45,6 +45,7 @@ export function isTestPath(p) {
|
|||
}
|
||||
|
||||
const isCheckPath = (p) => isTestPath(p) || CHECK_CONFIG_RE.test(p);
|
||||
const PROSE_RE = /\.(md|markdown|mdx|txt|rst|adoc)$/i;
|
||||
|
||||
// --- the patterns -------------------------------------------------------------
|
||||
|
||||
|
|
@ -55,7 +56,7 @@ const TEST_DECL_RE =
|
|||
/^\s*(test|it|describe|suite)(\.\w+)?\s*\(|^\s*(async\s+)?def\s+test_\w*|^\s*func\s+Test\w*|^\s*#\[test\]|@Test\b/;
|
||||
|
||||
const SKIP_RE =
|
||||
/\b(test|it|describe|suite)\.(skip|todo|only)\s*\(|\bx(it|describe|test)\s*\(|\bf(it|describe)\s*\(|\{\s*(skip|todo)\s*:\s*(true|['"`])|@pytest\.mark\.(skip|skipif|xfail)\b|\bpytest\.skip\s*\(|@unittest\.skip|\bt\.Skip(Now|f)?\s*\(|#\[ignore\]|@(Disabled|Ignore)\b/;
|
||||
/\b(test|it|describe|suite)\.(skip|skipIf|todo|only)\s*\(|\bt\.(skip|todo)\s*\(|\bx(it|describe|test)\s*\(|\bf(it|describe)\s*\(|\{\s*(skip|todo)\s*:\s*(true|['"`])|@pytest\.mark\.(skip|skipif|xfail)\b|\bpytest\.skip\s*\(|@unittest\.skip|\bt\.Skip(Now|f)?\s*\(|#\[ignore\]|@(Disabled|Ignore)\b/;
|
||||
|
||||
// Inline suppressions silence a check wherever they appear.
|
||||
const SUPPRESS_RE =
|
||||
|
|
@ -183,6 +184,9 @@ export function analyzeDiff(text) {
|
|||
const removed = all.filter((l) => l.type === '-');
|
||||
const added = all.filter((l) => l.type === '+');
|
||||
const test = isTestPath(f.path);
|
||||
// Prose (docs, notes) describes code; a "not implemented" or an
|
||||
// eslint-disable written in it is text, not a stub or a muted check.
|
||||
const prose = PROSE_RE.test(f.path);
|
||||
|
||||
if (test) {
|
||||
const lostAsserts = count(removed, ASSERT_RE) - count(added, ASSERT_RE);
|
||||
|
|
@ -196,7 +200,7 @@ export function analyzeDiff(text) {
|
|||
for (const l of addedNotRemoved(removed, added, SKIP_RE)) findings.push(finding('skipped-test', f.path, l));
|
||||
}
|
||||
|
||||
for (const l of addedNotRemoved(removed, added, SUPPRESS_RE)) findings.push(finding('muted-check', f.path, l));
|
||||
if (!prose) for (const l of addedNotRemoved(removed, added, SUPPRESS_RE)) findings.push(finding('muted-check', f.path, l));
|
||||
if (isCheckPath(f.path)) {
|
||||
for (const l of addedNotRemoved(removed, added, MUTE_RE)) findings.push(finding('muted-check', f.path, l));
|
||||
for (const h of f.hunks) {
|
||||
|
|
@ -211,7 +215,7 @@ export function analyzeDiff(text) {
|
|||
}
|
||||
}
|
||||
|
||||
if (!test) for (const l of addedNotRemoved(removed, added, STUB_RE)) findings.push(finding('stub', f.path, l));
|
||||
if (!test && !prose) for (const l of addedNotRemoved(removed, added, STUB_RE)) findings.push(finding('stub', f.path, l));
|
||||
}
|
||||
return { status: findings.length ? 'findings' : 'clean', findings };
|
||||
}
|
||||
|
|
@ -227,7 +231,10 @@ function diffFromGit(cwd, base, head) {
|
|||
const okHead = spawnSync('git', ['rev-parse', '--verify', '--quiet', `${head}^{commit}`], { cwd, encoding: 'utf8' });
|
||||
if (okHead.status !== 0) return { error: `cannot resolve --head ${head} in ${cwd}` };
|
||||
}
|
||||
const args = ['diff', '--no-color', '--no-ext-diff', '--find-renames', base, ...(head ? [head] : [])];
|
||||
// --no-renames: a rename is read as a deletion plus an addition. Paired as a
|
||||
// rename, `foo.test.js -> foo.test.js.bak` showed no removed line at all and
|
||||
// switched a test off silently (review, 2026-10-06).
|
||||
const args = ['diff', '--no-color', '--no-ext-diff', '--no-renames', base, ...(head ? [head] : [])];
|
||||
const r = spawnSync('git', args, { cwd, encoding: 'utf8', maxBuffer: 256 * 1024 * 1024 });
|
||||
if (r.status !== 0) return { error: `git diff failed: ${String(r.stderr).trim()}` };
|
||||
return { text: r.stdout };
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue