## Summary Wave 2 of package code organization (follows merged #2139): peel pure/satellite clusters out of `packages/engine/src/merger.ts` while keeping `merger.js` import paths stable for tests and production. - **`merger-glob.ts`** — `ConflictType`, `LOCKFILE_PATTERNS`, `GENERATED_PATTERNS`, `matchGlob` - **`merger-workspace-test-commands.ts`** — pnpm workspace package mapping + `inferDefaultTestCommand` / scoped + file-scoped test command derivation - **`merger-file-scope.ts`** — `parseDiffStat`, `extractFileScope` / `matchesScope`, squash file-scope invariant (`FileScopeViolationError`, assert/enforce with main’s `status` audit semantics and merge-trait modes) - Re-exports from `merger.ts`; baseline ratchet for `merger.ts` (~12.5k → ~11.8k) ## Test plan - [x] `pnpm --filter @fusion/engine exec tsc --noEmit` - [x] eslint on merger + satellites - [x] `merger-file-scope-invariant.test.ts` (18 tests) - [x] focused conflict-resolution pattern tests - [ ] CI merge gate ## Plan Continues `docs/plans/2026-07-14-001-refactor-package-code-organization-plan.md` (U6 merger satellites). <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added file-scope validation for merge operations, with strict and warning-only enforcement options. * Added automatic classification of lockfile and generated-file conflicts. * Added workspace-aware test selection based on changed packages and files. * Added support for inferring test commands across pnpm, Yarn, Bun, and npm projects. * **Refactor** * Organized merge, conflict-resolution, file-scope, and test-inference functionality into dedicated modules without changing existing public access paths. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
87 lines
2.8 KiB
TypeScript
87 lines
2.8 KiB
TypeScript
/**
|
|
* FNXC:CodeOrganization 2026-07-15-12:00:
|
|
* Conflict path patterns and simple glob matching peeled from merger.ts.
|
|
* Public path remains merger.ts via re-export for test import stability.
|
|
*/
|
|
/** Conflict type classification for merge conflict resolution */
|
|
export type ConflictType =
|
|
| "lockfile-ours"
|
|
| "generated-theirs"
|
|
| "trivial-whitespace"
|
|
| "complex";
|
|
|
|
/** Lock file patterns that should auto-resolve using "ours" (keep current branch's version) */
|
|
export const LOCKFILE_PATTERNS = [
|
|
"package-lock.json",
|
|
"pnpm-lock.yaml",
|
|
"yarn.lock",
|
|
"Gemfile.lock",
|
|
"composer.lock",
|
|
"poetry.lock",
|
|
"bun.lockb",
|
|
"go.sum",
|
|
];
|
|
|
|
/** Generated file patterns that should auto-resolve using "theirs" (keep branch's fresh generation) */
|
|
export const GENERATED_PATTERNS = [
|
|
"*.gen.ts",
|
|
"*.gen.js",
|
|
"*.min.js",
|
|
"*.min.css",
|
|
"dist/*",
|
|
"build/*",
|
|
"coverage/*",
|
|
".next/*",
|
|
".nuxt/*",
|
|
".output/*",
|
|
".cache/*",
|
|
"out/*",
|
|
"__generated__/*",
|
|
"generated/*",
|
|
];
|
|
|
|
/** Check if a path matches a glob pattern (simple glob support: * and **) */
|
|
export function matchGlob(path: string, pattern: string): boolean {
|
|
// Handle ** which matches across directory boundaries (must do before single *)
|
|
if (pattern.includes("**")) {
|
|
// Convert ** to match any characters including /
|
|
const regexPattern = pattern
|
|
.replace(/\./g, "\\.")
|
|
.replace(/\*\*/g, "<<<DOUBLESTAR>>>")
|
|
.replace(/\*/g, "[^/]*")
|
|
.replace(/<<<DOUBLESTAR>>>/g, ".*");
|
|
const regex = new RegExp(`^${regexPattern}$`);
|
|
return regex.test(path);
|
|
}
|
|
|
|
// Handle patterns with single directory wildcards (e.g., "src/*.ts")
|
|
const lastSlash = pattern.lastIndexOf("/");
|
|
if (lastSlash !== -1) {
|
|
const patternDir = pattern.slice(0, lastSlash);
|
|
const patternFile = pattern.slice(lastSlash + 1);
|
|
const pathDir = path.lastIndexOf("/") !== -1 ? path.slice(0, path.lastIndexOf("/")) : "";
|
|
const pathFile = path.lastIndexOf("/") !== -1 ? path.slice(path.lastIndexOf("/")) : path;
|
|
|
|
// Check if directories match
|
|
if (patternDir.includes("*")) {
|
|
const dirRegex = new RegExp(`^${patternDir.replace(/\./g, "\\.").replace(/\*/g, "[^/]*")}$`);
|
|
if (!dirRegex.test(pathDir)) return false;
|
|
} else if (!pathDir.endsWith(patternDir) && patternDir !== pathDir) {
|
|
return false;
|
|
}
|
|
|
|
// Match filename pattern
|
|
return matchGlob(pathFile, patternFile);
|
|
}
|
|
|
|
// Simple pattern without directory - match against filename only or full path
|
|
const fileName = path.lastIndexOf("/") !== -1 ? path.slice(path.lastIndexOf("/") + 1) : path;
|
|
|
|
// Convert glob to regex
|
|
const regexPattern = pattern
|
|
.replace(/\./g, "\\.")
|
|
.replace(/\*/g, "[^/]*");
|
|
const regex = new RegExp(`^${regexPattern}$`);
|
|
return regex.test(fileName) || regex.test(path);
|
|
}
|