Files
fusion/packages/engine/src/merger-glob.ts
gsxdsm 8c3758584b refactor(engine): peel merger glob, test-command, and file-scope satellites (#2143)
## 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 -->
2026-07-15 14:44:35 -07:00

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);
}