fix: auto-resolve build artifact conflicts in milestone merge (#2777)

* fix: auto-resolve build artifact conflicts in milestone merge

The binary conflict classification in mergeMilestoneToMain only
auto-resolved .gsd/ prefixed files. Machine-generated build artifacts
like .tsbuildinfo, .pyc, __pycache__/, .DS_Store, and .map files were
treated as real code conflicts, blocking auto-merge unnecessarily.

Extract an isSafeToAutoResolve helper that checks both the .gsd/ prefix
and a SAFE_AUTO_RESOLVE_PATTERNS regex list. Matched files are resolved
with --theirs, same as .gsd/ state files.

Closes #2761

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* test: add tests for build artifact auto-resolve patterns

Extract isSafeToAutoResolve and SAFE_AUTO_RESOLVE_PATTERNS to module-level
exports for testability. Add unit tests covering .gsd/ state files, build
artifacts (.tsbuildinfo, .pyc, __pycache__, .DS_Store, .map), and rejection
of real source files (.ts, .js, .py, .json, .md).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
TÂCHES 2026-03-26 20:03:37 -06:00 committed by GitHub
parent 46016adf9a
commit f96a26b3c9
2 changed files with 112 additions and 13 deletions

View file

@ -157,6 +157,25 @@ function clearProjectRootStateFiles(basePath: string, milestoneId: string): void
}
}
// ─── Build Artifact Auto-Resolve ─────────────────────────────────────────────
/** Patterns for machine-generated build artifacts that can be safely
* auto-resolved by accepting --theirs during merge. These files are
* regenerable and never contain meaningful manual edits. */
export const SAFE_AUTO_RESOLVE_PATTERNS: RegExp[] = [
/\.tsbuildinfo$/,
/\.pyc$/,
/\/__pycache__\//,
/\.DS_Store$/,
/\.map$/,
];
/** Returns true if the file path is safe to auto-resolve during merge.
* Covers `.gsd/` state files and common build artifacts. */
export const isSafeToAutoResolve = (filePath: string): boolean =>
filePath.startsWith(".gsd/") ||
SAFE_AUTO_RESOLVE_PATTERNS.some((re) => re.test(filePath));
// ─── Dispatch-Level Sync (project root ↔ worktree) ──────────────────────────
/**
@ -1408,30 +1427,30 @@ export function mergeMilestoneToMain(
: nativeConflictFiles(originalBasePath_);
if (conflictedFiles.length > 0) {
// Separate .gsd/ state file conflicts from real code conflicts.
// GSD state files (STATE.md, auto.lock, etc.)
// diverge between branches during normal operation — always prefer the
// milestone branch version since it has the latest execution state.
const gsdConflicts = conflictedFiles.filter((f) => f.startsWith(".gsd/"));
// Separate auto-resolvable conflicts (GSD state files + build artifacts)
// from real code conflicts. GSD state files diverge between branches
// during normal operation. Build artifacts are machine-generated and
// regenerable. Both are safe to accept from the milestone branch.
const autoResolvable = conflictedFiles.filter(isSafeToAutoResolve);
const codeConflicts = conflictedFiles.filter(
(f) => !f.startsWith(".gsd/"),
(f) => !isSafeToAutoResolve(f),
);
// Auto-resolve .gsd/ conflicts by accepting the milestone branch version
if (gsdConflicts.length > 0) {
for (const gsdFile of gsdConflicts) {
// Auto-resolve safe conflicts by accepting the milestone branch version
if (autoResolvable.length > 0) {
for (const safeFile of autoResolvable) {
try {
nativeCheckoutTheirs(originalBasePath_, [gsdFile]);
nativeAddPaths(originalBasePath_, [gsdFile]);
nativeCheckoutTheirs(originalBasePath_, [safeFile]);
nativeAddPaths(originalBasePath_, [safeFile]);
} catch {
// If checkout --theirs fails, try removing the file from the merge
// (it's a runtime file that shouldn't be committed anyway)
nativeRmForce(originalBasePath_, [gsdFile]);
nativeRmForce(originalBasePath_, [safeFile]);
}
}
}
// If there are still non-.gsd conflicts, escalate
// If there are still real code conflicts, escalate
if (codeConflicts.length > 0) {
// Pop stash before throwing so local work is not lost (#2151).
if (stashed) {

View file

@ -0,0 +1,80 @@
/**
* auto-worktree-auto-resolve.test.ts Unit tests for isSafeToAutoResolve.
*
* Covers: .gsd/ state files, build artifacts (.tsbuildinfo, .pyc, __pycache__,
* .DS_Store, .map), and rejection of real source files.
*/
import { describe, test } from "node:test";
import assert from "node:assert/strict";
import {
isSafeToAutoResolve,
SAFE_AUTO_RESOLVE_PATTERNS,
} from "../auto-worktree.ts";
describe("isSafeToAutoResolve", () => {
// ─── .gsd/ state files ───────────────────────────────────────────────────
test("returns true for .gsd/ prefixed paths", () => {
assert.ok(isSafeToAutoResolve(".gsd/STATE.md"));
assert.ok(isSafeToAutoResolve(".gsd/milestones/M001/CONTEXT.md"));
assert.ok(isSafeToAutoResolve(".gsd/gsd.db"));
});
// ─── Build artifact patterns ─────────────────────────────────────────────
test("returns true for .tsbuildinfo files", () => {
assert.ok(isSafeToAutoResolve("tsconfig.tsbuildinfo"));
assert.ok(isSafeToAutoResolve("dist/tsconfig.tsbuildinfo"));
});
test("returns true for .pyc files", () => {
assert.ok(isSafeToAutoResolve("module.pyc"));
assert.ok(isSafeToAutoResolve("src/utils/helpers.pyc"));
});
test("returns true for __pycache__/ paths", () => {
assert.ok(isSafeToAutoResolve("src/__pycache__/module.cpython-311.pyc"));
assert.ok(isSafeToAutoResolve("lib/__pycache__/foo.py"));
});
test("returns true for .DS_Store files", () => {
assert.ok(isSafeToAutoResolve(".DS_Store"));
assert.ok(isSafeToAutoResolve("src/.DS_Store"));
});
test("returns true for .map source map files", () => {
assert.ok(isSafeToAutoResolve("dist/index.js.map"));
assert.ok(isSafeToAutoResolve("out/bundle.css.map"));
});
// ─── Real source files (should NOT be auto-resolved) ─────────────────────
test("returns false for .ts source files", () => {
assert.ok(!isSafeToAutoResolve("src/index.ts"));
assert.ok(!isSafeToAutoResolve("lib/utils.ts"));
});
test("returns false for .js source files", () => {
assert.ok(!isSafeToAutoResolve("src/index.js"));
assert.ok(!isSafeToAutoResolve("lib/helpers.js"));
});
test("returns false for .py source files", () => {
assert.ok(!isSafeToAutoResolve("src/main.py"));
assert.ok(!isSafeToAutoResolve("scripts/deploy.py"));
});
test("returns false for config and data files", () => {
assert.ok(!isSafeToAutoResolve("package.json"));
assert.ok(!isSafeToAutoResolve("tsconfig.json"));
assert.ok(!isSafeToAutoResolve("README.md"));
});
// ─── SAFE_AUTO_RESOLVE_PATTERNS export ────────────────────────────────────
test("SAFE_AUTO_RESOLVE_PATTERNS is a non-empty array of RegExp", () => {
assert.ok(Array.isArray(SAFE_AUTO_RESOLVE_PATTERNS));
assert.ok(SAFE_AUTO_RESOLVE_PATTERNS.length > 0);
for (const pattern of SAFE_AUTO_RESOLVE_PATTERNS) {
assert.ok(pattern instanceof RegExp);
}
});
});