From d0d542e4788a8aec3c51dcb4d18e8be65efb1ccb Mon Sep 17 00:00:00 2001 From: Dennis Alexis Valin Dittrich Date: Sat, 5 Sep 2026 09:32:17 +0200 Subject: [PATCH] fix(#4183): resolve root phase Fallow base (#4215) * test(#4183): reproduce root phase Fallow base failure * fix(#4183): resolve root phase Fallow base Fall back to the phase root commit when its parent is unresolvable. * chore(#4183): add release note * chore(#4183): bind changeset to fork PR * docs(#4183): describe root fallback precisely * chore(#4183): bind changeset to upstream PR --------- Co-authored-by: CI Rebase Check Co-authored-by: Tom Boucher --- .changeset/bold-otters-run.md | 5 ++ .../code-review/steps/structural-pre-pass.md | 6 ++- .../code-review-pipeline-regression.test.cjs | 53 +++++++++++++++++++ 3 files changed, 63 insertions(+), 1 deletion(-) create mode 100644 .changeset/bold-otters-run.md diff --git a/.changeset/bold-otters-run.md b/.changeset/bold-otters-run.md new file mode 100644 index 000000000..61ae65251 --- /dev/null +++ b/.changeset/bold-otters-run.md @@ -0,0 +1,5 @@ +--- +type: Fixed +pr: 4215 +--- +**Structural pre-pass no longer aborts for phases introduced in the repository's root commit** — Fallow uses the root commit itself when no parent exists instead of receiving an invalid parent revision. (#4183) diff --git a/gsd-core/workflows/code-review/steps/structural-pre-pass.md b/gsd-core/workflows/code-review/steps/structural-pre-pass.md index 10c033629..f6621413b 100644 --- a/gsd-core/workflows/code-review/steps/structural-pre-pass.md +++ b/gsd-core/workflows/code-review/steps/structural-pre-pass.md @@ -39,7 +39,11 @@ if [ \"$FALLOW_SCOPE\" = \"phase\" ]; then # same-numbered phases and tail -1 selected the oldest. FALLOW_PHASE_START=$(git log --format=\"%H\" --diff-filter=A -- \"${PHASE_DIR}\" 2>/dev/null | tail -1) if [ -n \"$FALLOW_PHASE_START\" ]; then - FALLOW_BASE=\"${FALLOW_PHASE_START}^\" + if git rev-parse \"${FALLOW_PHASE_START}^\" >/dev/null 2>&1; then + FALLOW_BASE=\"${FALLOW_PHASE_START}^\" + else + FALLOW_BASE=\"${FALLOW_PHASE_START}\" + fi FALLOW_SCOPE_ARGS=(--changed-since \"$FALLOW_BASE\") fi fi diff --git a/tests/code-review-pipeline-regression.test.cjs b/tests/code-review-pipeline-regression.test.cjs index 8805c3f9f..8feacc38a 100644 --- a/tests/code-review-pipeline-regression.test.cjs +++ b/tests/code-review-pipeline-regression.test.cjs @@ -990,6 +990,59 @@ describe('Bug 5 (#3191) — same anchored, portable phase-scope grep at all thre } ); + test( + 'root commit: fallow phase scope uses a resolvable root SHA', + SKIP_WIN32, + () => { + const repo = createTempDir('gsd-4183-fallow-root-'); + try { + gitOrThrow(['init', '-b', 'main'], { cwd: repo, timeoutMs: GIT_TIMEOUT_MS }); + gitOrThrow(['config', 'user.email', 'test@test.com'], { cwd: repo, timeoutMs: GIT_TIMEOUT_MS }); + gitOrThrow(['config', 'user.name', 'Test'], { cwd: repo, timeoutMs: GIT_TIMEOUT_MS }); + gitOrThrow(['config', 'commit.gpgsign', 'false'], { cwd: repo, timeoutMs: GIT_TIMEOUT_MS }); + + const phaseFile = path.join(repo, '.planning', 'phases', '06-ctx', 'PLAN.md'); + fs.mkdirSync(path.dirname(phaseFile), { recursive: true }); + fs.writeFileSync(phaseFile, '# phase context\n'); + gitOrThrow(['add', '.planning'], { cwd: repo, timeoutMs: GIT_TIMEOUT_MS }); + gitOrThrow(['commit', '-m', 'docs(06): initial phase context'], { cwd: repo, timeoutMs: GIT_TIMEOUT_MS }); + const rootSha = gitOrThrow(['rev-parse', 'HEAD'], { cwd: repo, timeoutMs: GIT_TIMEOUT_MS }).trim(); + + fs.writeFileSync(path.join(repo, 'index.js'), 'module.exports = 1;\n'); + gitOrThrow(['add', 'index.js'], { cwd: repo, timeoutMs: GIT_TIMEOUT_MS }); + gitOrThrow(['commit', '-m', 'feat: add source'], { cwd: repo, timeoutMs: GIT_TIMEOUT_MS }); + + const result = runDerivation(repo, extractFallowDerivation(), '06'); + assert.equal(result.status, 0, `snippet exited ${result.status}; stderr=${result.stderr}`); + const fallowBase = parseSentinel(result.stdout, 'FALLOW_BASE'); + assert.deepStrictEqual( + fallowBase, + [rootSha], + `root-parent FALLOW_BASE regression: expected ${rootSha}, got ${JSON.stringify(fallowBase)}`, + ); + assert.equal( + gitOrThrow(['rev-parse', '--verify', `${fallowBase[0]}^{commit}`], { cwd: repo, timeoutMs: GIT_TIMEOUT_MS }).trim(), + rootSha, + 'FALLOW_BASE must resolve to the root commit', + ); + + if (process.env.CI) { + const { requireFallowBinary } = require('../gsd-core/bin/lib/fallow-runner.cjs'); + const { execTool } = require('../gsd-core/bin/lib/shell-command-projection.cjs'); + const audit = execTool( + requireFallowBinary({ cwd: ROOT, envPath: '' }), + ['audit', '--changed-since', fallowBase[0], '--format', 'json'], + { cwd: repo, timeout: 120000 }, + ); + assert.ok([0, 1].includes(audit.exitCode), `fallow root audit exit=${audit.exitCode}; stderr=${audit.stderr}`); + console.log(`fallow-root-audit normal-exit=${audit.exitCode}`); + } + } finally { + cleanup(repo); + } + }, + ); + test( 'T5: with no genuine phase scope commit, every derivation yields NO base (fail-closed preserved)', SKIP_WIN32,