@fern-api/fern-api-dev 5.27.5 → 5.27.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/cli.cjs +1734 -1567
  2. package/package.json +1 -1
package/cli.cjs CHANGED
@@ -360342,9 +360342,9 @@ var require_diff3 = __commonJS({
360342
360342
  }
360343
360343
  });
360344
360344
 
360345
- // ../../../node_modules/.pnpm/@fern-api+replay@0.15.0/node_modules/@fern-api/replay/dist/index.cjs
360345
+ // ../../../node_modules/.pnpm/@fern-api+replay@0.15.1/node_modules/@fern-api/replay/dist/index.cjs
360346
360346
  var require_dist14 = __commonJS({
360347
- "../../../node_modules/.pnpm/@fern-api+replay@0.15.0/node_modules/@fern-api/replay/dist/index.cjs"(exports2, module4) {
360347
+ "../../../node_modules/.pnpm/@fern-api+replay@0.15.1/node_modules/@fern-api/replay/dist/index.cjs"(exports2, module4) {
360348
360348
  "use strict";
360349
360349
  var __defProp4 = Object.defineProperty;
360350
360350
  var __getOwnPropDesc4 = Object.getOwnPropertyDescriptor;
@@ -360757,24 +360757,24 @@ var require_dist14 = __commonJS({
360757
360757
  async function applyPatchToContent(base4, patchContent, filePath) {
360758
360758
  const fileDiff = extractFileDiff(patchContent, filePath);
360759
360759
  if (!fileDiff) return null;
360760
- const tempDir2 = await (0, import_promises310.mkdtemp)((0, import_node_path42.join)((0, import_node_os32.tmpdir)(), "replay-migrate-"));
360760
+ const tempDir2 = await (0, import_promises510.mkdtemp)((0, import_node_path72.join)((0, import_node_os32.tmpdir)(), "replay-migrate-"));
360761
360761
  const tempGit = new GitClient(tempDir2);
360762
360762
  try {
360763
360763
  await tempGit.exec(["init"]);
360764
360764
  await tempGit.exec(["config", "user.email", "migrate@fern.com"]);
360765
360765
  await tempGit.exec(["config", "user.name", "Fern Replay Migration"]);
360766
360766
  await tempGit.exec(["config", "commit.gpgSign", "false"]);
360767
- const tempFilePath = (0, import_node_path42.join)(tempDir2, filePath);
360768
- await (0, import_promises310.mkdir)((0, import_node_path42.dirname)(tempFilePath), { recursive: true });
360769
- await (0, import_promises310.writeFile)(tempFilePath, base4);
360767
+ const tempFilePath = (0, import_node_path72.join)(tempDir2, filePath);
360768
+ await (0, import_promises510.mkdir)((0, import_node_path72.dirname)(tempFilePath), { recursive: true });
360769
+ await (0, import_promises510.writeFile)(tempFilePath, base4);
360770
360770
  await tempGit.exec(["add", filePath]);
360771
360771
  await tempGit.exec(["commit", "-m", `base for ${filePath}`, "--allow-empty"]);
360772
360772
  await tempGit.execWithInput(["apply", "--allow-empty"], fileDiff);
360773
- return await (0, import_promises310.readFile)(tempFilePath, "utf-8");
360773
+ return await (0, import_promises510.readFile)(tempFilePath, "utf-8");
360774
360774
  } catch {
360775
360775
  return null;
360776
360776
  } finally {
360777
- await (0, import_promises310.rm)(tempDir2, { recursive: true, force: true }).catch(() => {
360777
+ await (0, import_promises510.rm)(tempDir2, { recursive: true, force: true }).catch(() => {
360778
360778
  });
360779
360779
  }
360780
360780
  }
@@ -360796,15 +360796,15 @@ var require_dist14 = __commonJS({
360796
360796
  }
360797
360797
  return out.length > 0 ? out.join("\n") + "\n" : null;
360798
360798
  }
360799
- var import_promises310;
360799
+ var import_promises510;
360800
360800
  var import_node_os32;
360801
- var import_node_path42;
360801
+ var import_node_path72;
360802
360802
  var init_PatchApplyTheirs = __esm3({
360803
360803
  "src/PatchApplyTheirs.ts"() {
360804
360804
  "use strict";
360805
- import_promises310 = require("fs/promises");
360805
+ import_promises510 = require("fs/promises");
360806
360806
  import_node_os32 = require("os");
360807
- import_node_path42 = require("path");
360807
+ import_node_path72 = require("path");
360808
360808
  init_GitClient();
360809
360809
  }
360810
360810
  });
@@ -360898,7 +360898,7 @@ var require_dist14 = __commonJS({
360898
360898
  return match3?.[1];
360899
360899
  }
360900
360900
  var import_node_fs32 = require("fs");
360901
- var import_node_path43 = require("path");
360901
+ var import_node_path42 = require("path");
360902
360902
  var import_yaml18 = require_dist11();
360903
360903
  var LOCKFILE_HEADER = "# DO NOT EDIT MANUALLY - Managed by Fern Replay\n";
360904
360904
  var LockfileNotFoundError = class extends Error {
@@ -360915,10 +360915,10 @@ var require_dist14 = __commonJS({
360915
360915
  this.outputDir = outputDir;
360916
360916
  }
360917
360917
  get lockfilePath() {
360918
- return (0, import_node_path43.join)(this.outputDir, ".fern", "replay.lock");
360918
+ return (0, import_node_path42.join)(this.outputDir, ".fern", "replay.lock");
360919
360919
  }
360920
360920
  get customizationsPath() {
360921
- return (0, import_node_path43.join)(this.outputDir, ".fern", "replay.yml");
360921
+ return (0, import_node_path42.join)(this.outputDir, ".fern", "replay.yml");
360922
360922
  }
360923
360923
  exists() {
360924
360924
  return (0, import_node_fs32.existsSync)(this.lockfilePath);
@@ -360990,7 +360990,7 @@ var require_dist14 = __commonJS({
360990
360990
  }
360991
360991
  }
360992
360992
  this.lock.patches = cleanPatches;
360993
- const dir = (0, import_node_path43.dirname)(this.lockfilePath);
360993
+ const dir = (0, import_node_path42.dirname)(this.lockfilePath);
360994
360994
  if (!(0, import_node_fs32.existsSync)(dir)) {
360995
360995
  (0, import_node_fs32.mkdirSync)(dir, { recursive: true });
360996
360996
  }
@@ -361091,218 +361091,7 @@ var require_dist14 = __commonJS({
361091
361091
  }
361092
361092
  };
361093
361093
  var import_node_crypto7 = require("crypto");
361094
- var import_promises195 = require("fs/promises");
361095
- var import_node_os15 = require("os");
361096
361094
  var import_node_path210 = require("path");
361097
- var import_minimatch8 = require_commonjs3();
361098
- var CONFLICT_OPENER = "<<<<<<< Generated";
361099
- var CONFLICT_SEPARATOR = "=======";
361100
- var CONFLICT_CLOSER = ">>>>>>> Your customization";
361101
- function trimCR(line) {
361102
- return line.endsWith("\r") ? line.slice(0, -1) : line;
361103
- }
361104
- function findConflictRanges(lines) {
361105
- const ranges = [];
361106
- let i9 = 0;
361107
- while (i9 < lines.length) {
361108
- if (trimCR(lines[i9]) === CONFLICT_OPENER) {
361109
- let separatorIdx = -1;
361110
- let j7 = i9 + 1;
361111
- let found = false;
361112
- while (j7 < lines.length) {
361113
- const trimmed2 = trimCR(lines[j7]);
361114
- if (trimmed2 === CONFLICT_OPENER) {
361115
- break;
361116
- }
361117
- if (separatorIdx === -1 && trimmed2 === CONFLICT_SEPARATOR) {
361118
- separatorIdx = j7;
361119
- } else if (separatorIdx !== -1 && trimmed2 === CONFLICT_CLOSER) {
361120
- ranges.push({ start: i9, separator: separatorIdx, end: j7 });
361121
- i9 = j7;
361122
- found = true;
361123
- break;
361124
- }
361125
- j7++;
361126
- }
361127
- if (!found) {
361128
- i9++;
361129
- continue;
361130
- }
361131
- }
361132
- i9++;
361133
- }
361134
- return ranges;
361135
- }
361136
- function stripConflictMarkers(content5) {
361137
- const lines = content5.split(/\r?\n/);
361138
- const ranges = findConflictRanges(lines);
361139
- if (ranges.length === 0) {
361140
- return content5;
361141
- }
361142
- const result = [];
361143
- let rangeIdx = 0;
361144
- for (let i9 = 0; i9 < lines.length; i9++) {
361145
- if (rangeIdx < ranges.length) {
361146
- const range2 = ranges[rangeIdx];
361147
- if (i9 === range2.start) {
361148
- continue;
361149
- }
361150
- if (i9 > range2.start && i9 < range2.separator) {
361151
- result.push(lines[i9]);
361152
- continue;
361153
- }
361154
- if (i9 === range2.separator) {
361155
- continue;
361156
- }
361157
- if (i9 > range2.separator && i9 < range2.end) {
361158
- continue;
361159
- }
361160
- if (i9 === range2.end) {
361161
- rangeIdx++;
361162
- continue;
361163
- }
361164
- }
361165
- result.push(lines[i9]);
361166
- }
361167
- return result.join("\n");
361168
- }
361169
- var import_node_diff3 = require_diff3();
361170
- function threeWayMerge(base4, ours, theirs) {
361171
- const baseLines = base4.split("\n");
361172
- const oursLines = ours.split("\n");
361173
- const theirsLines = theirs.split("\n");
361174
- const regions = (0, import_node_diff3.diff3Merge)(oursLines, baseLines, theirsLines);
361175
- const outputLines = [];
361176
- const conflicts2 = [];
361177
- let currentLine = 1;
361178
- for (const region of regions) {
361179
- if (region.ok) {
361180
- outputLines.push(...region.ok);
361181
- currentLine += region.ok.length;
361182
- } else if (region.conflict) {
361183
- const resolved = tryResolveConflict(
361184
- region.conflict.a,
361185
- // ours (generator)
361186
- region.conflict.o,
361187
- // base
361188
- region.conflict.b
361189
- // theirs (user)
361190
- );
361191
- if (resolved !== null) {
361192
- outputLines.push(...resolved);
361193
- currentLine += resolved.length;
361194
- } else {
361195
- const startLine = currentLine;
361196
- outputLines.push("<<<<<<< Generated");
361197
- outputLines.push(...region.conflict.a);
361198
- outputLines.push("=======");
361199
- outputLines.push(...region.conflict.b);
361200
- outputLines.push(">>>>>>> Your customization");
361201
- const conflictLines = region.conflict.a.length + region.conflict.b.length + 3;
361202
- conflicts2.push({
361203
- startLine,
361204
- endLine: startLine + conflictLines - 1,
361205
- ours: region.conflict.a,
361206
- theirs: region.conflict.b
361207
- });
361208
- currentLine += conflictLines;
361209
- }
361210
- }
361211
- }
361212
- const result = {
361213
- content: outputLines.join("\n"),
361214
- hasConflicts: conflicts2.length > 0,
361215
- conflicts: conflicts2
361216
- };
361217
- if (conflicts2.length === 0) {
361218
- const dropped = computeDroppedContextLines(baseLines, theirsLines, outputLines);
361219
- if (dropped.length > 0) {
361220
- result.droppedContextLines = dropped;
361221
- }
361222
- }
361223
- return result;
361224
- }
361225
- function computeDroppedContextLines(baseLines, theirsLines, mergedLines) {
361226
- const baseCounts = countLines2(baseLines);
361227
- const theirsCounts = countLines2(theirsLines);
361228
- const mergedCounts = countLines2(mergedLines);
361229
- const dropped = [];
361230
- const seen = /* @__PURE__ */ new Set();
361231
- for (const line of baseLines) {
361232
- if (seen.has(line)) continue;
361233
- const inBase = baseCounts.get(line) ?? 0;
361234
- const inTheirs = theirsCounts.get(line) ?? 0;
361235
- const inMerged = mergedCounts.get(line) ?? 0;
361236
- if (inBase > 0 && inTheirs > 0 && inMerged < Math.min(inBase, inTheirs)) {
361237
- dropped.push(line);
361238
- seen.add(line);
361239
- }
361240
- }
361241
- return dropped;
361242
- }
361243
- function countLines2(lines) {
361244
- const counts = /* @__PURE__ */ new Map();
361245
- for (const line of lines) {
361246
- counts.set(line, (counts.get(line) ?? 0) + 1);
361247
- }
361248
- return counts;
361249
- }
361250
- function tryResolveConflict(oursLines, baseLines, theirsLines) {
361251
- if (baseLines.length === 0) {
361252
- return null;
361253
- }
361254
- const oursPatches = (0, import_node_diff3.diffPatch)(baseLines, oursLines);
361255
- const theirsPatches = (0, import_node_diff3.diffPatch)(baseLines, theirsLines);
361256
- if (oursPatches.length === 0) return theirsLines;
361257
- if (theirsPatches.length === 0) return oursLines;
361258
- if (patchesOverlap(oursPatches, theirsPatches)) {
361259
- return null;
361260
- }
361261
- return applyBothPatches(baseLines, oursPatches, theirsPatches);
361262
- }
361263
- function patchesOverlap(oursPatches, theirsPatches) {
361264
- for (const op4 of oursPatches) {
361265
- const oStart = op4.buffer1.offset;
361266
- const oEnd = oStart + op4.buffer1.length;
361267
- for (const tp3 of theirsPatches) {
361268
- const tStart = tp3.buffer1.offset;
361269
- const tEnd = tStart + tp3.buffer1.length;
361270
- if (op4.buffer1.length === 0 && tp3.buffer1.length === 0 && oStart === tStart) {
361271
- return true;
361272
- }
361273
- if (tp3.buffer1.length === 0 && tStart === oEnd) {
361274
- return true;
361275
- }
361276
- if (op4.buffer1.length === 0 && oStart === tEnd) {
361277
- return true;
361278
- }
361279
- if (oStart < tEnd && tStart < oEnd) {
361280
- return true;
361281
- }
361282
- }
361283
- }
361284
- return false;
361285
- }
361286
- function applyBothPatches(baseLines, oursPatches, theirsPatches) {
361287
- const allPatches = [
361288
- ...oursPatches.map((p14) => ({
361289
- offset: p14.buffer1.offset,
361290
- length: p14.buffer1.length,
361291
- replacement: p14.buffer2.chunk
361292
- })),
361293
- ...theirsPatches.map((p14) => ({
361294
- offset: p14.buffer1.offset,
361295
- length: p14.buffer1.length,
361296
- replacement: p14.buffer2.chunk
361297
- }))
361298
- ];
361299
- allPatches.sort((a3, b8) => b8.offset - a3.offset);
361300
- const result = [...baseLines];
361301
- for (const p14 of allPatches) {
361302
- result.splice(p14.offset, p14.length, ...p14.replacement);
361303
- }
361304
- return result;
361305
- }
361306
361095
  var BINARY_EXTENSIONS2 = /* @__PURE__ */ new Set([
361307
361096
  ".png",
361308
361097
  ".jpg",
@@ -361352,728 +361141,10 @@ var require_dist14 = __commonJS({
361352
361141
  ".pyo",
361353
361142
  ".DS_Store"
361354
361143
  ]);
361355
- var ReplayApplicator = class {
361356
- git;
361357
- lockManager;
361358
- outputDir;
361359
- renameCache = /* @__PURE__ */ new Map();
361360
- treeExistsCache = /* @__PURE__ */ new Map();
361361
- fileTheirsAccumulator = /* @__PURE__ */ new Map();
361362
- /**
361363
- * Apply mode for the current `applyPatches` invocation. Set at the start
361364
- * of `applyPatches`, read by `mergeFile` to decide:
361365
- * - whether to skip the intra-loop marker strip (kept in `applyPatches`)
361366
- * - whether to populate the accumulator after a conflicted merge
361367
- * (resolve mode populates so subsequent patches on the same file
361368
- * can use patch A's THEIRS as a structurally-correct merge base
361369
- * when their diff was authored against post-A structure)
361370
- * - whether to retry THEIRS reconstruction against the accumulator
361371
- * when the BASE-relative reconstruction produced markers from a
361372
- * cross-patch context mismatch
361373
- */
361374
- currentApplyMode = "replay";
361375
- /**
361376
- * Set of files that appear in 2+ patches in the current `applyPatches`
361377
- * invocation. Computed at the top of `applyPatches`. Read by `mergeFile`
361378
- * and `populateAccumulatorForPatch` to decide whether to use the patch's
361379
- * `theirs_snapshot` directly as THEIRS (snapshot-as-primary) or fall
361380
- * back to line-anchored reconstruction (FER-9525 cross-patch isolation).
361381
- *
361382
- * Snapshot-as-primary is correct when the patch owns its file (no other
361383
- * patch in this run touches it): the snapshot is what the customer
361384
- * intends for that file post-regen, regardless of generator structural
361385
- * changes that would invalidate the diff's line anchors.
361386
- *
361387
- * When a file IS shared, snapshots are CUMULATIVE across the patches
361388
- * touching it (each one's snapshot includes prior patches' contributions).
361389
- * Using a later patch's cumulative snapshot as its individual THEIRS
361390
- * would propagate prior patches' edits into the merge — surfacing nested
361391
- * conflicts at regions the patch alone never touched. Reconstruction
361392
- * via `applyPatchToContent` is required there.
361393
- */
361394
- sharedFiles = /* @__PURE__ */ new Set();
361395
- constructor(git, lockManager, outputDir) {
361396
- this.git = git;
361397
- this.lockManager = lockManager;
361398
- this.outputDir = outputDir;
361399
- }
361400
- /**
361401
- * Resolve the GenerationRecord for a patch's base_generation.
361402
- * Falls back to constructing an ad-hoc record from the commit's tree
361403
- * when base_generation isn't a tracked generation (commit-scan patches).
361404
- */
361405
- async resolveBaseGeneration(baseGeneration) {
361406
- const gen = this.lockManager.getGeneration(baseGeneration);
361407
- if (gen) return gen;
361408
- try {
361409
- const treeHash = await this.git.getTreeHash(baseGeneration);
361410
- return {
361411
- commit_sha: baseGeneration,
361412
- tree_hash: treeHash,
361413
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
361414
- cli_version: "unknown",
361415
- generator_versions: {}
361416
- };
361417
- } catch {
361418
- return void 0;
361419
- }
361420
- }
361421
- /** Reset inter-patch accumulator for a new cycle. */
361422
- resetAccumulator() {
361423
- this.fileTheirsAccumulator.clear();
361424
- }
361425
- /**
361426
- * @internal Test-only.
361427
- * Returns a defensive copy of the inter-patch accumulator. Used by the
361428
- * adversarial test suite (FER-9791) to assert invariant I2: after every
361429
- * applied patch, the accumulator has an entry for each file the patch
361430
- * touched, and the entry's content matches on-disk.
361431
- */
361432
- getAccumulatorSnapshot() {
361433
- const snapshot = /* @__PURE__ */ new Map();
361434
- for (const [path111, entry] of this.fileTheirsAccumulator) {
361435
- snapshot.set(path111, { content: entry.content, baseGeneration: entry.baseGeneration });
361436
- }
361437
- return snapshot;
361438
- }
361439
- /**
361440
- * Apply all patches, returning results for each.
361441
- * Skips patches that match exclude patterns in replay.yml
361442
- *
361443
- * `applyMode` controls the post-conflict marker strategy:
361444
- * - `"replay"` (default): strip conflict markers from disk between
361445
- * iterations whenever a later patch touches the same file. Lets the
361446
- * follow-up patch's `git apply --3way` see clean OURS content,
361447
- * which is what the silent-loss fix (PR #73) relies on. The replay
361448
- * pipeline calls `revertConflictingFiles` after the loop to clean
361449
- * any markers that survive.
361450
- * - `"resolve"`: keep markers on disk. The resolve command needs the
361451
- * customer to see them. Slow-path 3-way merge naturally preserves
361452
- * A's markers and B's clean writes when their regions don't overlap;
361453
- * when they do overlap, the customer gets nested markers and
361454
- * resolves manually. Either way they aren't silently dropped.
361455
- */
361456
- async applyPatches(patches, opts) {
361457
- const applyMode = opts?.applyMode ?? "replay";
361458
- this.currentApplyMode = applyMode;
361459
- this.resetAccumulator();
361460
- const fileFreq = /* @__PURE__ */ new Map();
361461
- for (const p14 of patches) {
361462
- for (const f4 of p14.files) {
361463
- fileFreq.set(f4, (fileFreq.get(f4) ?? 0) + 1);
361464
- }
361465
- }
361466
- this.sharedFiles = new Set(
361467
- Array.from(fileFreq.entries()).filter(([, n19]) => n19 > 1).map(([f4]) => f4)
361468
- );
361469
- const results = [];
361470
- for (let i9 = 0; i9 < patches.length; i9++) {
361471
- const patch5 = patches[i9];
361472
- if (this.isExcluded(patch5)) {
361473
- results.push({
361474
- patch: patch5,
361475
- status: "skipped",
361476
- method: "git-am"
361477
- });
361478
- continue;
361479
- }
361480
- const result = await this.applyPatchWithFallback(patch5);
361481
- results.push(result);
361482
- if (applyMode !== "resolve" && result.status === "conflict" && result.fileResults) {
361483
- const laterFiles = /* @__PURE__ */ new Set();
361484
- for (let j7 = i9 + 1; j7 < patches.length; j7++) {
361485
- for (const f4 of patches[j7].files) {
361486
- laterFiles.add(f4);
361487
- }
361488
- }
361489
- const resolvedToOriginal = /* @__PURE__ */ new Map();
361490
- if (result.resolvedFiles) {
361491
- for (const [orig, resolved] of Object.entries(result.resolvedFiles)) {
361492
- resolvedToOriginal.set(resolved, orig);
361493
- }
361494
- }
361495
- for (const fileResult of result.fileResults) {
361496
- if (fileResult.status !== "conflict") continue;
361497
- const originalPath = resolvedToOriginal.get(fileResult.file) ?? fileResult.file;
361498
- if (laterFiles.has(fileResult.file) || laterFiles.has(originalPath)) {
361499
- const filePath = (0, import_node_path210.join)(this.outputDir, fileResult.file);
361500
- try {
361501
- const content5 = await (0, import_promises195.readFile)(filePath, "utf-8");
361502
- const stripped = stripConflictMarkers(content5);
361503
- await (0, import_promises195.writeFile)(filePath, stripped);
361504
- } catch {
361505
- }
361506
- }
361507
- }
361508
- }
361509
- }
361510
- return results;
361511
- }
361512
- /**
361513
- * Populate accumulator after git apply succeeds, AND collect per-file
361514
- * results including any "dropped context lines" — lines that were
361515
- * present in BASE and THEIRS (unchanged context) but absent from the
361516
- * final on-disk state because OURS deleted them. This is the fast-path
361517
- * counterpart to ThreeWayMerge.computeDroppedContextLines used by the
361518
- * 3-way slow path; both must surface the same warning.
361519
- */
361520
- async populateAccumulatorForPatch(patch5, baseGen, currentTreeHash) {
361521
- const fileResults = [];
361522
- if (!baseGen) return fileResults;
361523
- const tempDir2 = await (0, import_promises195.mkdtemp)((0, import_node_path210.join)((0, import_node_os15.tmpdir)(), "replay-acc-"));
361524
- const { GitClient: GitClient2 } = await Promise.resolve().then(() => (init_GitClient(), GitClient_exports));
361525
- const tempGit = new GitClient2(tempDir2);
361526
- await tempGit.exec(["init"]);
361527
- await tempGit.exec(["config", "user.email", "replay@fern.com"]);
361528
- await tempGit.exec(["config", "user.name", "Fern Replay"]);
361529
- try {
361530
- for (const filePath of patch5.files) {
361531
- if (isBinaryFile(filePath)) continue;
361532
- const resolvedPath = await this.resolveFilePath(filePath, baseGen.tree_hash, currentTreeHash);
361533
- const base4 = await this.git.showFile(baseGen.tree_hash, filePath);
361534
- const snapshotForFile = patch5.theirs_snapshot?.[resolvedPath] ?? patch5.theirs_snapshot?.[filePath];
361535
- const fileIsShared = this.sharedFiles.has(resolvedPath) || this.sharedFiles.has(filePath);
361536
- const theirs = !fileIsShared && snapshotForFile != null ? snapshotForFile : await this.applyPatchToContent(base4, patch5.patch_content, filePath, tempGit, tempDir2);
361537
- const finalOnDisk = await (0, import_promises195.readFile)((0, import_node_path210.join)(this.outputDir, resolvedPath), "utf-8").catch(() => null);
361538
- const effectiveTheirs = theirs ?? finalOnDisk;
361539
- if (effectiveTheirs != null) {
361540
- this.fileTheirsAccumulator.set(resolvedPath, {
361541
- content: effectiveTheirs,
361542
- baseGeneration: patch5.base_generation
361543
- });
361544
- }
361545
- if (base4 != null && effectiveTheirs != null && finalOnDisk != null) {
361546
- const dropped = computeDroppedContextLinesForFile(base4, effectiveTheirs, finalOnDisk);
361547
- if (dropped.length > 0) {
361548
- fileResults.push({
361549
- file: resolvedPath,
361550
- status: "merged",
361551
- droppedContextLines: dropped
361552
- });
361553
- }
361554
- }
361555
- }
361556
- } finally {
361557
- await (0, import_promises195.rm)(tempDir2, { recursive: true }).catch(() => {
361558
- });
361559
- }
361560
- return fileResults;
361561
- }
361562
- async applyPatchWithFallback(patch5) {
361563
- const baseGen = await this.resolveBaseGeneration(patch5.base_generation);
361564
- const lock = this.lockManager.read();
361565
- const currentGen = lock.generations.find((g8) => g8.commit_sha === lock.current_generation);
361566
- const currentTreeHash = currentGen?.tree_hash ?? baseGen?.tree_hash ?? "";
361567
- const needsAccumulation = await Promise.all(
361568
- patch5.files.map(async (f4) => {
361569
- if (!baseGen) return false;
361570
- const resolved = await this.resolveFilePath(f4, baseGen.tree_hash, currentTreeHash);
361571
- return this.fileTheirsAccumulator.has(resolved);
361572
- })
361573
- ).then((results) => results.some(Boolean));
361574
- const resolvedFiles = {};
361575
- for (const filePath of patch5.files) {
361576
- const resolvedPath = baseGen ? await this.resolveFilePath(filePath, baseGen.tree_hash, currentTreeHash) : filePath;
361577
- if (resolvedPath !== filePath) {
361578
- resolvedFiles[filePath] = resolvedPath;
361579
- }
361580
- }
361581
- const patchIsRenameAware = /^rename from /m.test(patch5.patch_content);
361582
- const hasGeneratorRename = Object.keys(resolvedFiles).length > 0 && !patchIsRenameAware;
361583
- if (!needsAccumulation && !hasGeneratorRename) {
361584
- const snapshots = /* @__PURE__ */ new Map();
361585
- for (const filePath of patch5.files) {
361586
- const fullPath = (0, import_node_path210.join)(this.outputDir, filePath);
361587
- snapshots.set(filePath, await (0, import_promises195.readFile)(fullPath, "utf-8").catch(() => null));
361588
- }
361589
- try {
361590
- await this.git.execWithInput(["apply", "--3way"], patch5.patch_content);
361591
- const fastPathFileResults = await this.populateAccumulatorForPatch(patch5, baseGen, currentTreeHash);
361592
- return {
361593
- patch: patch5,
361594
- status: "applied",
361595
- method: "git-am",
361596
- ...fastPathFileResults.length > 0 && { fileResults: fastPathFileResults },
361597
- ...Object.keys(resolvedFiles).length > 0 && { resolvedFiles }
361598
- };
361599
- } catch {
361600
- for (const [filePath, content5] of snapshots) {
361601
- const fullPath = (0, import_node_path210.join)(this.outputDir, filePath);
361602
- if (content5 != null) {
361603
- await (0, import_promises195.writeFile)(fullPath, content5);
361604
- } else {
361605
- await (0, import_promises195.unlink)(fullPath).catch(() => {
361606
- });
361607
- }
361608
- }
361609
- }
361610
- }
361611
- return this.applyWithThreeWayMerge(patch5);
361612
- }
361613
- async applyWithThreeWayMerge(patch5) {
361614
- const fileResults = [];
361615
- const resolvedFiles = {};
361616
- const tempDir2 = await (0, import_promises195.mkdtemp)((0, import_node_path210.join)((0, import_node_os15.tmpdir)(), "replay-"));
361617
- const { GitClient: GitClient2 } = await Promise.resolve().then(() => (init_GitClient(), GitClient_exports));
361618
- const tempGit = new GitClient2(tempDir2);
361619
- await tempGit.exec(["init"]);
361620
- await tempGit.exec(["config", "user.email", "replay@fern.com"]);
361621
- await tempGit.exec(["config", "user.name", "Fern Replay"]);
361622
- try {
361623
- for (const filePath of patch5.files) {
361624
- if (isBinaryFile(filePath)) {
361625
- fileResults.push({
361626
- file: filePath,
361627
- status: "skipped",
361628
- reason: "binary-file"
361629
- });
361630
- continue;
361631
- }
361632
- const result = await this.mergeFile(patch5, filePath, tempGit, tempDir2);
361633
- if (result.file !== filePath) {
361634
- resolvedFiles[filePath] = result.file;
361635
- }
361636
- fileResults.push(result);
361637
- }
361638
- } finally {
361639
- await (0, import_promises195.rm)(tempDir2, { recursive: true }).catch(() => {
361640
- });
361641
- }
361642
- const conflictFiles = fileResults.filter((r5) => r5.status === "conflict");
361643
- const hasConflicts = conflictFiles.length > 0;
361644
- const conflictReason = hasConflicts ? conflictFiles.some((f4) => f4.conflictReason === "base-generation-mismatch") ? "base-generation-mismatch" : conflictFiles[0]?.conflictReason : void 0;
361645
- return {
361646
- patch: patch5,
361647
- status: hasConflicts ? "conflict" : "applied",
361648
- method: "3way-merge",
361649
- fileResults,
361650
- conflictReason,
361651
- ...Object.keys(resolvedFiles).length > 0 && { resolvedFiles }
361652
- };
361653
- }
361654
- async mergeFile(patch5, filePath, tempGit, tempDir2) {
361655
- try {
361656
- const baseGen = await this.resolveBaseGeneration(patch5.base_generation);
361657
- if (!baseGen) {
361658
- return { file: filePath, status: "skipped", reason: "base-generation-not-found" };
361659
- }
361660
- const lock = this.lockManager.read();
361661
- const currentGen = lock.generations.find((g8) => g8.commit_sha === lock.current_generation);
361662
- const currentTreeHash = currentGen?.tree_hash ?? baseGen.tree_hash;
361663
- const resolvedPath = await this.resolveFilePath(filePath, baseGen.tree_hash, currentTreeHash);
361664
- const metadata = {
361665
- patchId: patch5.id,
361666
- patchMessage: patch5.original_message,
361667
- baseGeneration: patch5.base_generation,
361668
- currentGeneration: lock.current_generation
361669
- };
361670
- let base4 = await this.git.showFile(baseGen.tree_hash, filePath);
361671
- let renameSourcePath;
361672
- if (!base4) {
361673
- const renameSource = this.extractRenameSource(patch5.patch_content, filePath);
361674
- if (renameSource) {
361675
- base4 = await this.git.showFile(baseGen.tree_hash, renameSource);
361676
- renameSourcePath = renameSource;
361677
- }
361678
- }
361679
- const oursPath = (0, import_node_path210.join)(this.outputDir, resolvedPath);
361680
- const ours = await (0, import_promises195.readFile)(oursPath, "utf-8").catch(() => null);
361681
- let ghostReconstructed = false;
361682
- let theirs = null;
361683
- if (!base4 && ours && !renameSourcePath) {
361684
- const treeReachable = await this.isTreeReachable(baseGen.tree_hash);
361685
- if (!treeReachable) {
361686
- const fileDiff = this.extractFileDiff(patch5.patch_content, filePath);
361687
- if (fileDiff) {
361688
- const { reconstructFromGhostPatch: reconstructFromGhostPatch2 } = await Promise.resolve().then(() => (init_HybridReconstruction(), HybridReconstruction_exports));
361689
- const result = reconstructFromGhostPatch2(fileDiff, ours);
361690
- if (result) {
361691
- base4 = result.base;
361692
- theirs = result.theirs;
361693
- ghostReconstructed = true;
361694
- }
361695
- }
361696
- }
361697
- }
361698
- const snapshotForFile = patch5.theirs_snapshot?.[resolvedPath] ?? patch5.theirs_snapshot?.[filePath];
361699
- const fileIsShared = this.sharedFiles.has(resolvedPath) || this.sharedFiles.has(filePath);
361700
- const useSnapshotAsPrimary = !fileIsShared && snapshotForFile != null;
361701
- if (!ghostReconstructed) {
361702
- if (useSnapshotAsPrimary) {
361703
- theirs = snapshotForFile;
361704
- } else {
361705
- theirs = await this.applyPatchToContent(
361706
- base4,
361707
- patch5.patch_content,
361708
- filePath,
361709
- tempGit,
361710
- tempDir2,
361711
- renameSourcePath
361712
- );
361713
- const reconstructionHasMarkers = theirs != null && (theirs.includes("<<<<<<< Generated") || theirs.includes(">>>>>>> Your customization"));
361714
- const baseHadMarkers = base4 != null && (base4.includes("<<<<<<< Generated") || base4.includes(">>>>>>> Your customization"));
361715
- if (reconstructionHasMarkers && !baseHadMarkers && snapshotForFile != null) {
361716
- theirs = snapshotForFile;
361717
- }
361718
- }
361719
- }
361720
- const accumulatorEntry = this.fileTheirsAccumulator.get(resolvedPath);
361721
- if (theirs) {
361722
- const theirsHasMarkers = theirs.includes("<<<<<<< Generated") || theirs.includes(">>>>>>> Your customization");
361723
- const baseHasMarkers = base4 != null && (base4.includes("<<<<<<< Generated") || base4.includes(">>>>>>> Your customization"));
361724
- if (theirsHasMarkers && !baseHasMarkers) {
361725
- if (accumulatorEntry) {
361726
- theirs = null;
361727
- } else {
361728
- return {
361729
- file: resolvedPath,
361730
- status: "skipped",
361731
- reason: "stale-conflict-markers"
361732
- };
361733
- }
361734
- }
361735
- }
361736
- let useAccumulatorAsMergeBase = false;
361737
- if (accumulatorEntry && (theirs == null || base4 == null)) {
361738
- theirs = await this.applyPatchToContent(
361739
- accumulatorEntry.content,
361740
- patch5.patch_content,
361741
- filePath,
361742
- tempGit,
361743
- tempDir2
361744
- );
361745
- if (theirs != null) {
361746
- useAccumulatorAsMergeBase = true;
361747
- } else if (await this.isPatchAlreadyApplied(
361748
- patch5.patch_content,
361749
- filePath,
361750
- accumulatorEntry.content,
361751
- tempGit,
361752
- tempDir2
361753
- )) {
361754
- theirs = accumulatorEntry.content;
361755
- useAccumulatorAsMergeBase = true;
361756
- }
361757
- }
361758
- if (theirs) {
361759
- const theirsHasMarkers = theirs.includes("<<<<<<< Generated") || theirs.includes(">>>>>>> Your customization");
361760
- const accBaseHasMarkers = accumulatorEntry != null && (accumulatorEntry.content.includes("<<<<<<< Generated") || accumulatorEntry.content.includes(">>>>>>> Your customization"));
361761
- if (theirsHasMarkers && !accBaseHasMarkers && !(base4 != null && (base4.includes("<<<<<<< Generated") || base4.includes(">>>>>>> Your customization")))) {
361762
- return {
361763
- file: resolvedPath,
361764
- status: "skipped",
361765
- reason: "stale-conflict-markers"
361766
- };
361767
- }
361768
- }
361769
- let effective_theirs = theirs;
361770
- let baseMismatchSkipped = false;
361771
- if (theirs != null && base4 != null && !useAccumulatorAsMergeBase) {
361772
- if (accumulatorEntry && accumulatorEntry.baseGeneration === patch5.base_generation) {
361773
- try {
361774
- const preMerged = threeWayMerge(base4, accumulatorEntry.content, theirs);
361775
- if (!preMerged.hasConflicts) {
361776
- effective_theirs = preMerged.content;
361777
- } else {
361778
- effective_theirs = theirs;
361779
- }
361780
- } catch {
361781
- effective_theirs = theirs;
361782
- }
361783
- } else if (accumulatorEntry) {
361784
- baseMismatchSkipped = true;
361785
- }
361786
- }
361787
- if (base4 == null && ours == null && effective_theirs != null) {
361788
- this.fileTheirsAccumulator.set(resolvedPath, {
361789
- content: effective_theirs,
361790
- baseGeneration: patch5.base_generation
361791
- });
361792
- const outDir2 = (0, import_node_path210.dirname)(oursPath);
361793
- await (0, import_promises195.mkdir)(outDir2, { recursive: true });
361794
- await (0, import_promises195.writeFile)(oursPath, effective_theirs);
361795
- return { file: resolvedPath, status: "merged", reason: "new-file" };
361796
- }
361797
- if (base4 == null && ours != null && effective_theirs != null && !useAccumulatorAsMergeBase) {
361798
- const merged2 = threeWayMerge("", ours, effective_theirs);
361799
- const outDir2 = (0, import_node_path210.dirname)(oursPath);
361800
- await (0, import_promises195.mkdir)(outDir2, { recursive: true });
361801
- await (0, import_promises195.writeFile)(oursPath, merged2.content);
361802
- if (merged2.hasConflicts) {
361803
- return {
361804
- file: resolvedPath,
361805
- status: "conflict",
361806
- conflicts: merged2.conflicts,
361807
- conflictReason: "new-file-both",
361808
- conflictMetadata: metadata
361809
- };
361810
- }
361811
- this.fileTheirsAccumulator.set(resolvedPath, {
361812
- content: merged2.content,
361813
- baseGeneration: patch5.base_generation
361814
- });
361815
- return { file: resolvedPath, status: "merged" };
361816
- }
361817
- if (effective_theirs == null) {
361818
- return {
361819
- file: resolvedPath,
361820
- status: "skipped",
361821
- reason: "missing-content"
361822
- };
361823
- }
361824
- if (base4 == null && !useAccumulatorAsMergeBase || ours == null) {
361825
- return {
361826
- file: resolvedPath,
361827
- status: "skipped",
361828
- reason: "missing-content"
361829
- };
361830
- }
361831
- const mergeBase = useAccumulatorAsMergeBase && accumulatorEntry ? accumulatorEntry.content : base4;
361832
- if (mergeBase == null) {
361833
- return {
361834
- file: resolvedPath,
361835
- status: "skipped",
361836
- reason: "missing-content"
361837
- };
361838
- }
361839
- const merged = threeWayMerge(mergeBase, ours, effective_theirs);
361840
- const outDir = (0, import_node_path210.dirname)(oursPath);
361841
- await (0, import_promises195.mkdir)(outDir, { recursive: true });
361842
- await (0, import_promises195.writeFile)(oursPath, merged.content);
361843
- const populateAccumulator = effective_theirs != null && (!merged.hasConflicts || this.currentApplyMode === "resolve");
361844
- if (populateAccumulator) {
361845
- this.fileTheirsAccumulator.set(resolvedPath, {
361846
- content: effective_theirs,
361847
- baseGeneration: patch5.base_generation
361848
- });
361849
- }
361850
- if (merged.hasConflicts) {
361851
- return {
361852
- file: resolvedPath,
361853
- status: "conflict",
361854
- conflicts: merged.conflicts,
361855
- conflictReason: baseMismatchSkipped ? "base-generation-mismatch" : "same-line-edit",
361856
- conflictMetadata: metadata
361857
- };
361858
- }
361859
- return {
361860
- file: resolvedPath,
361861
- status: "merged",
361862
- ...merged.droppedContextLines && merged.droppedContextLines.length > 0 ? { droppedContextLines: merged.droppedContextLines } : {}
361863
- };
361864
- } catch (error50) {
361865
- return {
361866
- file: filePath,
361867
- status: "skipped",
361868
- reason: `error: ${error50 instanceof Error ? error50.message : String(error50)}`
361869
- };
361870
- }
361871
- }
361872
- async isTreeReachable(treeHash) {
361873
- let result = this.treeExistsCache.get(treeHash);
361874
- if (result === void 0) {
361875
- result = await this.git.treeExists(treeHash);
361876
- this.treeExistsCache.set(treeHash, result);
361877
- }
361878
- return result;
361879
- }
361880
- isExcluded(patch5) {
361881
- const config5 = this.lockManager.getCustomizationsConfig();
361882
- if (!config5.exclude) return false;
361883
- return patch5.files.some((file4) => config5.exclude.some((pattern) => (0, import_minimatch8.minimatch)(file4, pattern)));
361884
- }
361885
- async resolveFilePath(filePath, baseTreeHash, currentTreeHash) {
361886
- const config5 = this.lockManager.getCustomizationsConfig();
361887
- if (config5.moves) {
361888
- for (const move of config5.moves) {
361889
- if ((0, import_minimatch8.minimatch)(filePath, move.from) || filePath === move.from) {
361890
- if (filePath === move.from) {
361891
- return move.to;
361892
- }
361893
- const fromBase = move.from.replace(/\*\*.*$/, "");
361894
- const toBase = move.to.replace(/\*\*.*$/, "");
361895
- if (filePath.startsWith(fromBase)) {
361896
- return toBase + filePath.slice(fromBase.length);
361897
- }
361898
- }
361899
- }
361900
- }
361901
- const cacheKey = `${baseTreeHash}:${currentTreeHash}`;
361902
- let renames = this.renameCache.get(cacheKey);
361903
- if (!renames) {
361904
- renames = await this.git.detectRenames(baseTreeHash, currentTreeHash);
361905
- this.renameCache.set(cacheKey, renames);
361906
- }
361907
- const gitRename = renames.find((r5) => r5.from === filePath);
361908
- if (gitRename) {
361909
- return gitRename.to;
361910
- }
361911
- return filePath;
361912
- }
361913
- async applyPatchToContent(base4, patchContent, filePath, tempGit, tempDir2, sourceFilePath) {
361914
- if (!base4) {
361915
- return this.extractNewFileFromPatch(patchContent, filePath);
361916
- }
361917
- const fileDiff = this.extractFileDiff(patchContent, filePath);
361918
- if (!fileDiff) return null;
361919
- try {
361920
- if (sourceFilePath) {
361921
- const tempSourcePath = (0, import_node_path210.join)(tempDir2, sourceFilePath);
361922
- await (0, import_promises195.mkdir)((0, import_node_path210.dirname)(tempSourcePath), { recursive: true });
361923
- await (0, import_promises195.writeFile)(tempSourcePath, base4);
361924
- await tempGit.exec(["add", sourceFilePath]);
361925
- await tempGit.exec([
361926
- "commit",
361927
- "-m",
361928
- `base for rename ${sourceFilePath} -> ${filePath}`,
361929
- "--allow-empty"
361930
- ]);
361931
- await tempGit.execWithInput(["apply", "--allow-empty"], fileDiff);
361932
- const tempTargetPath = (0, import_node_path210.join)(tempDir2, filePath);
361933
- return await (0, import_promises195.readFile)(tempTargetPath, "utf-8");
361934
- }
361935
- const tempFilePath = (0, import_node_path210.join)(tempDir2, filePath);
361936
- await (0, import_promises195.mkdir)((0, import_node_path210.dirname)(tempFilePath), { recursive: true });
361937
- await (0, import_promises195.writeFile)(tempFilePath, base4);
361938
- await tempGit.exec(["add", filePath]);
361939
- await tempGit.exec(["commit", "-m", `base for ${filePath}`, "--allow-empty"]);
361940
- await tempGit.execWithInput(["apply", "--allow-empty"], fileDiff);
361941
- return await (0, import_promises195.readFile)(tempFilePath, "utf-8");
361942
- } catch {
361943
- return null;
361944
- }
361945
- }
361946
- /**
361947
- * Detects whether a patch's additions are already present in `content`.
361948
- * Uses `git apply --reverse --check` — if the reverse patch applies cleanly,
361949
- * the forward patch is effectively a no-op (its +lines are already there and
361950
- * its -lines are already gone). Used to distinguish "patch already applied"
361951
- * from "patch base mismatch" after a forward-apply fails.
361952
- */
361953
- async isPatchAlreadyApplied(patchContent, filePath, content5, tempGit, tempDir2) {
361954
- const fileDiff = this.extractFileDiff(patchContent, filePath);
361955
- if (!fileDiff) return false;
361956
- try {
361957
- const tempFilePath = (0, import_node_path210.join)(tempDir2, filePath);
361958
- await (0, import_promises195.mkdir)((0, import_node_path210.dirname)(tempFilePath), { recursive: true });
361959
- await (0, import_promises195.writeFile)(tempFilePath, content5);
361960
- await tempGit.exec(["add", filePath]);
361961
- await tempGit.exec(["commit", "-m", `check already-applied ${filePath}`, "--allow-empty"]);
361962
- await tempGit.execWithInput(["apply", "--reverse", "--check", "--allow-empty"], fileDiff);
361963
- return true;
361964
- } catch {
361965
- return false;
361966
- }
361967
- }
361968
- extractFileDiff(patchContent, filePath) {
361969
- const lines = patchContent.split("\n");
361970
- const diffLines = [];
361971
- let inTargetFile = false;
361972
- for (const line of lines) {
361973
- if (line.startsWith("diff --git")) {
361974
- if (inTargetFile) {
361975
- break;
361976
- }
361977
- if (isDiffLineForFile(line, filePath)) {
361978
- inTargetFile = true;
361979
- diffLines.push(line);
361980
- }
361981
- continue;
361982
- }
361983
- if (inTargetFile) {
361984
- diffLines.push(line);
361985
- }
361986
- }
361987
- return diffLines.length > 0 ? diffLines.join("\n") + "\n" : null;
361988
- }
361989
- extractRenameSource(patchContent, targetFilePath) {
361990
- const lines = patchContent.split("\n");
361991
- let inTargetFile = false;
361992
- for (const line of lines) {
361993
- if (line.startsWith("diff --git")) {
361994
- if (inTargetFile) break;
361995
- inTargetFile = isDiffLineForFile(line, targetFilePath);
361996
- continue;
361997
- }
361998
- if (!inTargetFile) continue;
361999
- if (line.startsWith("@@")) break;
362000
- if (line.startsWith("rename from ")) {
362001
- return line.slice("rename from ".length);
362002
- }
362003
- }
362004
- return null;
362005
- }
362006
- extractNewFileFromPatch(patchContent, filePath) {
362007
- const lines = patchContent.split("\n");
362008
- const addedLines = [];
362009
- let inTargetFile = false;
362010
- let inHunk = false;
362011
- let isNewFile = false;
362012
- let noTrailingNewline = false;
362013
- for (const line of lines) {
362014
- if (line.startsWith("diff --git")) {
362015
- if (inTargetFile) break;
362016
- inTargetFile = isDiffLineForFile(line, filePath);
362017
- inHunk = false;
362018
- isNewFile = false;
362019
- continue;
362020
- }
362021
- if (!inTargetFile) continue;
362022
- if (!inHunk) {
362023
- if (line === "--- /dev/null" || line.startsWith("new file mode")) {
362024
- isNewFile = true;
362025
- }
362026
- }
362027
- if (line.startsWith("@@")) {
362028
- if (!isNewFile) return null;
362029
- inHunk = true;
362030
- continue;
362031
- }
362032
- if (!inHunk) continue;
362033
- if (line === "\") {
362034
- noTrailingNewline = true;
362035
- continue;
362036
- }
362037
- if (line.startsWith("+") && !line.startsWith("+++")) {
362038
- addedLines.push(line.slice(1));
362039
- }
362040
- }
362041
- if (addedLines.length === 0) return null;
362042
- return addedLines.join("\n") + (noTrailingNewline ? "" : "\n");
362043
- }
362044
- };
362045
361144
  function isBinaryFile(filePath) {
362046
361145
  const ext2 = (0, import_node_path210.extname)(filePath).toLowerCase();
362047
361146
  return BINARY_EXTENSIONS2.has(ext2);
362048
361147
  }
362049
- function computeDroppedContextLinesForFile(base4, theirs, finalContent) {
362050
- const baseLines = base4.split("\n");
362051
- const theirsLines = theirs.split("\n");
362052
- const finalLines = finalContent.split("\n");
362053
- const baseCounts = /* @__PURE__ */ new Map();
362054
- const theirsCounts = /* @__PURE__ */ new Map();
362055
- const finalCounts = /* @__PURE__ */ new Map();
362056
- for (const l9 of baseLines) baseCounts.set(l9, (baseCounts.get(l9) ?? 0) + 1);
362057
- for (const l9 of theirsLines) theirsCounts.set(l9, (theirsCounts.get(l9) ?? 0) + 1);
362058
- for (const l9 of finalLines) finalCounts.set(l9, (finalCounts.get(l9) ?? 0) + 1);
362059
- const dropped = [];
362060
- const seen = /* @__PURE__ */ new Set();
362061
- for (const line of baseLines) {
362062
- if (seen.has(line)) continue;
362063
- const inBase = baseCounts.get(line) ?? 0;
362064
- const inTheirs = theirsCounts.get(line) ?? 0;
362065
- const inFinal = finalCounts.get(line) ?? 0;
362066
- if (inBase > 0 && inTheirs > 0 && inFinal < Math.min(inBase, inTheirs)) {
362067
- dropped.push(line);
362068
- seen.add(line);
362069
- }
362070
- }
362071
- return dropped;
362072
- }
362073
- function isDiffLineForFile(diffLine, filePath) {
362074
- const match3 = diffLine.match(/^diff --git a\/.+ b\/(.+)$/);
362075
- return match3 !== null && match3[1] === filePath;
362076
- }
362077
361148
  var INFRASTRUCTURE_FILES = /* @__PURE__ */ new Set([".fernignore"]);
362078
361149
  function parsePatchFileHeaders(patchContent) {
362079
361150
  const results = [];
@@ -362106,6 +361177,21 @@ var require_dist14 = __commonJS({
362106
361177
  flush3();
362107
361178
  return results;
362108
361179
  }
361180
+ async function capturePatchSnapshot(git, files, treeish) {
361181
+ const snapshot = {};
361182
+ for (const file4 of files) {
361183
+ if (isBinaryFile(file4)) continue;
361184
+ const content5 = await git.showFile(treeish, file4).catch(() => null);
361185
+ if (content5 != null) snapshot[file4] = content5;
361186
+ }
361187
+ return snapshot;
361188
+ }
361189
+ function filterInfrastructureAndSensitiveFiles(rawFilesOutput) {
361190
+ const allFiles = rawFilesOutput.trim().split("\n").filter(Boolean).filter((f4) => !INFRASTRUCTURE_FILES.has(f4)).filter((f4) => !f4.startsWith(".fern/"));
361191
+ const sensitiveFiles = allFiles.filter((f4) => isSensitiveFile(f4));
361192
+ const files = allFiles.filter((f4) => !isSensitiveFile(f4));
361193
+ return { files, sensitiveFiles };
361194
+ }
362109
361195
  var ReplayDetector = class {
362110
361196
  git;
362111
361197
  lockManager;
@@ -362140,7 +361226,7 @@ var require_dist14 = __commonJS({
362140
361226
  return this.detectPatchesInRange(lastGen.commit_sha, lastGen, lock);
362141
361227
  }
362142
361228
  /**
362143
- * FER-10201 — Non-linear-history detection via `merge-base(prevGen, HEAD)`.
361229
+ * Non-linear-history detection via `merge-base(prevGen, HEAD)`.
362144
361230
  *
362145
361231
  * After a squash-merge of a Fern-bot PR, `lastGen.commit_sha` (the previous
362146
361232
  * `[fern-generated]`) is orphaned but still in git's object database. The
@@ -362172,7 +361258,7 @@ var require_dist14 = __commonJS({
362172
361258
  return this.detectPatchesInRange(mergeBase, lastGen, lock);
362173
361259
  }
362174
361260
  /**
362175
- * FER-10201 — Walk `${rangeStart}..HEAD`, classify each commit, emit one
361261
+ * Walk `${rangeStart}..HEAD`, classify each commit, emit one
362176
361262
  * `StoredPatch` per customer commit. Body shared by the linear path
362177
361263
  * (rangeStart = lastGen.commit_sha) and the merge-base path.
362178
361264
  *
@@ -362229,9 +361315,7 @@ var require_dist14 = __commonJS({
362229
361315
  continue;
362230
361316
  }
362231
361317
  const filesOutput = await this.git.exec(["diff-tree", "--no-commit-id", "--name-only", "-r", commit.sha]);
362232
- const allFiles = filesOutput.trim().split("\n").filter(Boolean).filter((f4) => !INFRASTRUCTURE_FILES.has(f4)).filter((f4) => !f4.startsWith(".fern/"));
362233
- const sensitiveFiles = allFiles.filter((f4) => isSensitiveFile(f4));
362234
- const files = allFiles.filter((f4) => !isSensitiveFile(f4));
361318
+ const { files, sensitiveFiles } = filterInfrastructureAndSensitiveFiles(filesOutput);
362235
361319
  if (sensitiveFiles.length > 0) {
362236
361320
  this.warnings.push(
362237
361321
  `Sensitive file(s) excluded from patch for commit ${commit.sha.slice(0, 7)}: ${sensitiveFiles.join(", ")}. These files will not be tracked by replay.`
@@ -362252,12 +361336,7 @@ var require_dist14 = __commonJS({
362252
361336
  if (files.length === 0) {
362253
361337
  continue;
362254
361338
  }
362255
- const theirsSnapshot = {};
362256
- for (const file4 of files) {
362257
- if (isBinaryFile(file4)) continue;
362258
- const content5 = await this.git.showFile(commit.sha, file4).catch(() => null);
362259
- if (content5 != null) theirsSnapshot[file4] = content5;
362260
- }
361339
+ const theirsSnapshot = await capturePatchSnapshot(this.git, files, commit.sha);
362261
361340
  newPatches.push({
362262
361341
  id: `patch-${commit.sha.slice(0, 8)}`,
362263
361342
  content_hash: contentHash,
@@ -362345,7 +361424,7 @@ var require_dist14 = __commonJS({
362345
361424
  *
362346
361425
  * This ensures content hashes match across the no-patches and normal-
362347
361426
  * regeneration flows, which store formatPatch and git-diff formats
362348
- * respectively (FER-9850, D5-D9).
361427
+ * respectively.
362349
361428
  */
362350
361429
  computeContentHash(patchContent) {
362351
361430
  const lines = patchContent.split("\n");
@@ -362355,8 +361434,8 @@ var require_dist14 = __commonJS({
362355
361434
  return `sha256:${(0, import_node_crypto7.createHash)("sha256").update(normalized).digest("hex")}`;
362356
361435
  }
362357
361436
  /**
362358
- * FER-9805 — Drop patches whose files were transiently created and then
362359
- * deleted within the current linear detection window, and are absent from
361437
+ * Drop patches whose files were transiently created and then deleted
361438
+ * within the current linear detection window, and are absent from
362360
361439
  * HEAD at the end of the window.
362361
361440
  *
362362
361441
  * Rationale: on a merge commit, createMergeCompositePatch already diffs
@@ -362381,8 +361460,8 @@ var require_dist14 = __commonJS({
362381
361460
  * single files from a multi-file patch would require a follow-up that
362382
361461
  * regenerates the patch content via `git format-patch -- <files>`. No
362383
361462
  * current failing test exercises this, and leaving the patch intact
362384
- * preserves today's behaviour. TODO(FER-9805): revisit if a future
362385
- * adversarial test demands per-file stripping.
361463
+ * preserves today's behaviour. Revisit if a future adversarial test
361464
+ * demands per-file stripping.
362386
361465
  */
362387
361466
  async collapseNetZeroFiles(patches) {
362388
361467
  if (patches.length === 0) return patches;
@@ -362443,12 +361522,10 @@ var require_dist14 = __commonJS({
362443
361522
  firstParent,
362444
361523
  mergeCommit.sha
362445
361524
  ]);
362446
- const allMergeFiles = filesOutput.trim().split("\n").filter(Boolean).filter((f4) => !INFRASTRUCTURE_FILES.has(f4)).filter((f4) => !f4.startsWith(".fern/"));
362447
- const sensitiveMergeFiles = allMergeFiles.filter((f4) => isSensitiveFile(f4));
362448
- const files = allMergeFiles.filter((f4) => !isSensitiveFile(f4));
362449
- if (sensitiveMergeFiles.length > 0) {
361525
+ const { files, sensitiveFiles } = filterInfrastructureAndSensitiveFiles(filesOutput);
361526
+ if (sensitiveFiles.length > 0) {
362450
361527
  this.warnings.push(
362451
- `Sensitive file(s) excluded from merge patch for commit ${mergeCommit.sha.slice(0, 7)}: ${sensitiveMergeFiles.join(", ")}. These files will not be tracked by replay.`
361528
+ `Sensitive file(s) excluded from merge patch for commit ${mergeCommit.sha.slice(0, 7)}: ${sensitiveFiles.join(", ")}. These files will not be tracked by replay.`
362452
361529
  );
362453
361530
  }
362454
361531
  if (files.length === 0) return null;
@@ -362464,12 +361541,7 @@ var require_dist14 = __commonJS({
362464
361541
  if (lock.patches.some((p14) => p14.content_hash === contentHash) || forgottenHashes.has(contentHash)) {
362465
361542
  return null;
362466
361543
  }
362467
- const theirsSnapshot = {};
362468
- for (const file4 of files) {
362469
- if (isBinaryFile(file4)) continue;
362470
- const content5 = await this.git.showFile(mergeCommit.sha, file4).catch(() => null);
362471
- if (content5 != null) theirsSnapshot[file4] = content5;
362472
- }
361544
+ const theirsSnapshot = await capturePatchSnapshot(this.git, files, mergeCommit.sha);
362473
361545
  return {
362474
361546
  id: `patch-merge-${mergeCommit.sha.slice(0, 8)}`,
362475
361547
  content_hash: contentHash,
@@ -362493,13 +361565,24 @@ var require_dist14 = __commonJS({
362493
361565
  if (!diffBase) {
362494
361566
  return this.detectPatchesViaCommitScan();
362495
361567
  }
361568
+ const lockForGuard = this.lockManager.read();
361569
+ const knownGenerations = new Set(lockForGuard.generations.map((g8) => g8.commit_sha));
361570
+ let resolvedBaseGeneration = commitKnownMissing ? diffBase : lastGen.commit_sha;
361571
+ if (commitKnownMissing && !knownGenerations.has(diffBase)) {
361572
+ const fallback2 = lockForGuard.current_generation ?? lockForGuard.generations[lockForGuard.generations.length - 1]?.commit_sha;
361573
+ if (!fallback2) {
361574
+ this.warnings.push(
361575
+ `Composite-patch fallback skipped: diff base ${diffBase.slice(0, 7)} is not a recorded generation and the lockfile has no recorded generations to anchor on. Customer customizations on disk are preserved; tracked patches will resume on the next regeneration.`
361576
+ );
361577
+ return { patches: [], revertedPatchIds: [] };
361578
+ }
361579
+ resolvedBaseGeneration = fallback2;
361580
+ }
362496
361581
  const filesOutput = await this.git.exec(["diff", "--name-only", diffBase, "HEAD"]);
362497
- const allTreeFiles = filesOutput.trim().split("\n").filter(Boolean).filter((f4) => !INFRASTRUCTURE_FILES.has(f4)).filter((f4) => !f4.startsWith(".fern/"));
362498
- const sensitiveTreeFiles = allTreeFiles.filter((f4) => isSensitiveFile(f4));
362499
- const files = allTreeFiles.filter((f4) => !isSensitiveFile(f4));
362500
- if (sensitiveTreeFiles.length > 0) {
361582
+ const { files, sensitiveFiles } = filterInfrastructureAndSensitiveFiles(filesOutput);
361583
+ if (sensitiveFiles.length > 0) {
362501
361584
  this.warnings.push(
362502
- `Sensitive file(s) excluded from composite patch: ${sensitiveTreeFiles.join(", ")}. These files will not be tracked by replay.`
361585
+ `Sensitive file(s) excluded from composite patch: ${sensitiveFiles.join(", ")}. These files will not be tracked by replay.`
362503
361586
  );
362504
361587
  }
362505
361588
  if (files.length === 0) return { patches: [], revertedPatchIds: [] };
@@ -362511,21 +361594,19 @@ var require_dist14 = __commonJS({
362511
361594
  return { patches: [], revertedPatchIds: [] };
362512
361595
  }
362513
361596
  const headSha = (await this.git.exec(["rev-parse", "HEAD"])).trim();
362514
- const theirsSnapshot = {};
362515
- for (const file4 of files) {
362516
- if (isBinaryFile(file4)) continue;
362517
- const content5 = await this.git.showFile("HEAD", file4).catch(() => null);
362518
- if (content5 != null) theirsSnapshot[file4] = content5;
362519
- }
361597
+ const theirsSnapshot = await capturePatchSnapshot(this.git, files, "HEAD");
362520
361598
  const compositePatch = {
362521
361599
  id: `patch-composite-${headSha.slice(0, 8)}`,
362522
361600
  content_hash: contentHash,
362523
361601
  original_commit: headSha,
362524
361602
  original_message: "Customer customizations (composite)",
362525
361603
  original_author: "composite",
362526
- // Use diffBase when commit is unreachable — the applicator needs a reachable
362527
- // reference to find base file content. diffBase may be the tree_hash.
362528
- base_generation: commitKnownMissing ? diffBase : lastGen.commit_sha,
361604
+ // Anchor `base_generation` on a SHA the applicator's
361605
+ // `resolveBaseGeneration` can find — always a member of
361606
+ // `lock.generations`. When `diffBase` is a recorded
361607
+ // generation, use it directly; otherwise the guard above
361608
+ // has already mapped it onto `current_generation`.
361609
+ base_generation: resolvedBaseGeneration,
362529
361610
  files,
362530
361611
  patch_content: diff2,
362531
361612
  ...Object.keys(theirsSnapshot).length > 0 ? { theirs_snapshot: theirsSnapshot } : {},
@@ -362543,6 +361624,8 @@ var require_dist14 = __commonJS({
362543
361624
  */
362544
361625
  async detectPatchesViaCommitScan() {
362545
361626
  const lock = this.lockManager.read();
361627
+ const knownGenerations = new Set(lock.generations.map((g8) => g8.commit_sha));
361628
+ const fallbackAnchor = lock.current_generation ?? lock.generations[lock.generations.length - 1]?.commit_sha;
362546
361629
  const log4 = await this.git.exec([
362547
361630
  "log",
362548
361631
  "--max-count=200",
@@ -362584,12 +361667,10 @@ var require_dist14 = __commonJS({
362584
361667
  continue;
362585
361668
  }
362586
361669
  const filesOutput = await this.git.exec(["diff-tree", "--no-commit-id", "--name-only", "-r", commit.sha]);
362587
- const allScanFiles = filesOutput.trim().split("\n").filter(Boolean).filter((f4) => !INFRASTRUCTURE_FILES.has(f4));
362588
- const sensitiveScanFiles = allScanFiles.filter((f4) => isSensitiveFile(f4));
362589
- const files = allScanFiles.filter((f4) => !isSensitiveFile(f4));
362590
- if (sensitiveScanFiles.length > 0) {
361670
+ const { files, sensitiveFiles } = filterInfrastructureAndSensitiveFiles(filesOutput);
361671
+ if (sensitiveFiles.length > 0) {
362591
361672
  this.warnings.push(
362592
- `Sensitive file(s) excluded from patch for commit ${commit.sha.slice(0, 7)}: ${sensitiveScanFiles.join(", ")}. These files will not be tracked by replay.`
361673
+ `Sensitive file(s) excluded from patch for commit ${commit.sha.slice(0, 7)}: ${sensitiveFiles.join(", ")}. These files will not be tracked by replay.`
362593
361674
  );
362594
361675
  if (files.length > 0) {
362595
361676
  if (parents.length === 0) {
@@ -362611,19 +361692,25 @@ var require_dist14 = __commonJS({
362611
361692
  continue;
362612
361693
  }
362613
361694
  const parentSha = parents[0];
362614
- const theirsSnapshot = {};
362615
- for (const file4 of files) {
362616
- if (isBinaryFile(file4)) continue;
362617
- const content5 = await this.git.showFile(commit.sha, file4).catch(() => null);
362618
- if (content5 != null) theirsSnapshot[file4] = content5;
361695
+ let baseGeneration;
361696
+ if (knownGenerations.has(parentSha)) {
361697
+ baseGeneration = parentSha;
361698
+ } else if (fallbackAnchor) {
361699
+ baseGeneration = fallbackAnchor;
361700
+ } else {
361701
+ this.warnings.push(
361702
+ `Skipping commit ${commit.sha.slice(0, 7)} (${commit.message.split("\n")[0]}): no recorded generation in the lockfile to anchor on. The customer's state on disk is preserved; tracked patches will resume on the next regeneration.`
361703
+ );
361704
+ continue;
362619
361705
  }
361706
+ const theirsSnapshot = await capturePatchSnapshot(this.git, files, commit.sha);
362620
361707
  newPatches.push({
362621
361708
  id: `patch-${commit.sha.slice(0, 8)}`,
362622
361709
  content_hash: contentHash,
362623
361710
  original_commit: commit.sha,
362624
361711
  original_message: commit.message,
362625
361712
  original_author: `${commit.authorName} <${commit.authorEmail}>`,
362626
- base_generation: parentSha,
361713
+ base_generation: baseGeneration,
362627
361714
  files,
362628
361715
  patch_content: patchContent,
362629
361716
  ...Object.keys(theirsSnapshot).length > 0 ? { theirs_snapshot: theirsSnapshot } : {}
@@ -362668,6 +361755,1040 @@ var require_dist14 = __commonJS({
362668
361755
  return lock.generations.find((g8) => g8.commit_sha === lock.current_generation);
362669
361756
  }
362670
361757
  };
361758
+ var import_node_diff3 = require_diff3();
361759
+ function threeWayMerge(base4, ours, theirs) {
361760
+ const baseLines = base4.split("\n");
361761
+ const oursLines = ours.split("\n");
361762
+ const theirsLines = theirs.split("\n");
361763
+ const regions = (0, import_node_diff3.diff3Merge)(oursLines, baseLines, theirsLines);
361764
+ const outputLines = [];
361765
+ const conflicts2 = [];
361766
+ let currentLine = 1;
361767
+ for (const region of regions) {
361768
+ if (region.ok) {
361769
+ outputLines.push(...region.ok);
361770
+ currentLine += region.ok.length;
361771
+ } else if (region.conflict) {
361772
+ const resolved = tryResolveConflict(
361773
+ region.conflict.a,
361774
+ // ours (generator)
361775
+ region.conflict.o,
361776
+ // base
361777
+ region.conflict.b
361778
+ // theirs (user)
361779
+ );
361780
+ if (resolved !== null) {
361781
+ outputLines.push(...resolved);
361782
+ currentLine += resolved.length;
361783
+ } else {
361784
+ const startLine = currentLine;
361785
+ outputLines.push("<<<<<<< Generated");
361786
+ outputLines.push(...region.conflict.a);
361787
+ outputLines.push("=======");
361788
+ outputLines.push(...region.conflict.b);
361789
+ outputLines.push(">>>>>>> Your customization");
361790
+ const conflictLines = region.conflict.a.length + region.conflict.b.length + 3;
361791
+ conflicts2.push({
361792
+ startLine,
361793
+ endLine: startLine + conflictLines - 1,
361794
+ ours: region.conflict.a,
361795
+ theirs: region.conflict.b
361796
+ });
361797
+ currentLine += conflictLines;
361798
+ }
361799
+ }
361800
+ }
361801
+ const result = {
361802
+ content: outputLines.join("\n"),
361803
+ hasConflicts: conflicts2.length > 0,
361804
+ conflicts: conflicts2
361805
+ };
361806
+ if (conflicts2.length === 0) {
361807
+ const dropped = computeDroppedContextLines(baseLines, theirsLines, outputLines);
361808
+ if (dropped.length > 0) {
361809
+ result.droppedContextLines = dropped;
361810
+ }
361811
+ }
361812
+ return result;
361813
+ }
361814
+ function computeDroppedContextLines(baseLines, theirsLines, mergedLines) {
361815
+ const baseCounts = countLines2(baseLines);
361816
+ const theirsCounts = countLines2(theirsLines);
361817
+ const mergedCounts = countLines2(mergedLines);
361818
+ const dropped = [];
361819
+ const seen = /* @__PURE__ */ new Set();
361820
+ for (const line of baseLines) {
361821
+ if (seen.has(line)) continue;
361822
+ const inBase = baseCounts.get(line) ?? 0;
361823
+ const inTheirs = theirsCounts.get(line) ?? 0;
361824
+ const inMerged = mergedCounts.get(line) ?? 0;
361825
+ if (inBase > 0 && inTheirs > 0 && inMerged < Math.min(inBase, inTheirs)) {
361826
+ dropped.push(line);
361827
+ seen.add(line);
361828
+ }
361829
+ }
361830
+ return dropped;
361831
+ }
361832
+ function countLines2(lines) {
361833
+ const counts = /* @__PURE__ */ new Map();
361834
+ for (const line of lines) {
361835
+ counts.set(line, (counts.get(line) ?? 0) + 1);
361836
+ }
361837
+ return counts;
361838
+ }
361839
+ function tryResolveConflict(oursLines, baseLines, theirsLines) {
361840
+ if (baseLines.length === 0) {
361841
+ return null;
361842
+ }
361843
+ const oursPatches = (0, import_node_diff3.diffPatch)(baseLines, oursLines);
361844
+ const theirsPatches = (0, import_node_diff3.diffPatch)(baseLines, theirsLines);
361845
+ if (oursPatches.length === 0) return theirsLines;
361846
+ if (theirsPatches.length === 0) return oursLines;
361847
+ if (patchesOverlap(oursPatches, theirsPatches)) {
361848
+ return null;
361849
+ }
361850
+ return applyBothPatches(baseLines, oursPatches, theirsPatches);
361851
+ }
361852
+ function patchesOverlap(oursPatches, theirsPatches) {
361853
+ for (const op4 of oursPatches) {
361854
+ const oStart = op4.buffer1.offset;
361855
+ const oEnd = oStart + op4.buffer1.length;
361856
+ for (const tp3 of theirsPatches) {
361857
+ const tStart = tp3.buffer1.offset;
361858
+ const tEnd = tStart + tp3.buffer1.length;
361859
+ if (op4.buffer1.length === 0 && tp3.buffer1.length === 0 && oStart === tStart) {
361860
+ return true;
361861
+ }
361862
+ if (tp3.buffer1.length === 0 && tStart === oEnd) {
361863
+ return true;
361864
+ }
361865
+ if (op4.buffer1.length === 0 && oStart === tEnd) {
361866
+ return true;
361867
+ }
361868
+ if (oStart < tEnd && tStart < oEnd) {
361869
+ return true;
361870
+ }
361871
+ }
361872
+ }
361873
+ return false;
361874
+ }
361875
+ function applyBothPatches(baseLines, oursPatches, theirsPatches) {
361876
+ const allPatches = [
361877
+ ...oursPatches.map((p14) => ({
361878
+ offset: p14.buffer1.offset,
361879
+ length: p14.buffer1.length,
361880
+ replacement: p14.buffer2.chunk
361881
+ })),
361882
+ ...theirsPatches.map((p14) => ({
361883
+ offset: p14.buffer1.offset,
361884
+ length: p14.buffer1.length,
361885
+ replacement: p14.buffer2.chunk
361886
+ }))
361887
+ ];
361888
+ allPatches.sort((a3, b8) => b8.offset - a3.offset);
361889
+ const result = [...baseLines];
361890
+ for (const p14 of allPatches) {
361891
+ result.splice(p14.offset, p14.length, ...p14.replacement);
361892
+ }
361893
+ return result;
361894
+ }
361895
+ var import_promises310 = require("fs/promises");
361896
+ var import_node_os15 = require("os");
361897
+ var import_node_path52 = require("path");
361898
+ var import_minimatch8 = require_commonjs3();
361899
+ var MergeAccumulator = class {
361900
+ constructor(mode, sharedFiles) {
361901
+ this.mode = mode;
361902
+ this.sharedFiles = sharedFiles;
361903
+ }
361904
+ entries = /* @__PURE__ */ new Map();
361905
+ /** Was this file touched by 2+ patches in the current `applyPatches` invocation? */
361906
+ isShared(path111) {
361907
+ return this.sharedFiles.has(path111);
361908
+ }
361909
+ /** Has any prior patch already recorded a result for this path? */
361910
+ has(path111) {
361911
+ return this.entries.has(path111);
361912
+ }
361913
+ /** Look up the accumulated content + base generation for this path. */
361914
+ lookup(path111) {
361915
+ return this.entries.get(path111);
361916
+ }
361917
+ /**
361918
+ * True if any of the patch's files have a prior accumulator entry — in
361919
+ * which case the fast path (`git apply --3way`) cannot honor prior
361920
+ * patches' contributions and we must route through the slow path.
361921
+ */
361922
+ shouldSkipFastPath(patch5) {
361923
+ return patch5.files.some((f4) => this.entries.has(f4));
361924
+ }
361925
+ /** Record a successful (clean) merge result. */
361926
+ recordMerge(path111, content5, baseGeneration) {
361927
+ this.entries.set(path111, { content: content5, baseGeneration });
361928
+ }
361929
+ /**
361930
+ * Record a conflicted merge result. Populates only in `"resolve"` mode
361931
+ * — replay mode keeps the accumulator clean of marker-laden content
361932
+ * since markers will be stripped from disk after the apply loop.
361933
+ */
361934
+ recordConflict(path111, content5, baseGeneration) {
361935
+ if (this.mode === "resolve") {
361936
+ this.entries.set(path111, { content: content5, baseGeneration });
361937
+ }
361938
+ }
361939
+ /**
361940
+ * Defensive copy for test introspection. Used by invariant I2 to assert
361941
+ * that after every applied patch, the accumulator has an entry for each
361942
+ * file the patch touched (see `__tests__/invariants/i2-accumulator-correctness.ts`).
361943
+ *
361944
+ * @internal
361945
+ */
361946
+ snapshot() {
361947
+ const copy = /* @__PURE__ */ new Map();
361948
+ for (const [path111, entry] of this.entries) {
361949
+ copy.set(path111, { content: entry.content, baseGeneration: entry.baseGeneration });
361950
+ }
361951
+ return copy;
361952
+ }
361953
+ };
361954
+ var import_promises195 = require("fs/promises");
361955
+ var import_node_path310 = require("path");
361956
+ async function resolveInputs(args) {
361957
+ const {
361958
+ patch: patch5,
361959
+ filePath,
361960
+ git,
361961
+ lockManager,
361962
+ outputDir,
361963
+ isTreeReachable,
361964
+ resolveBaseGeneration,
361965
+ resolveFilePath,
361966
+ extractRenameSource,
361967
+ extractFileDiff: extractFileDiff2
361968
+ } = args;
361969
+ const baseGen = await resolveBaseGeneration(patch5.base_generation);
361970
+ if (!baseGen) {
361971
+ return {
361972
+ resolvedPath: filePath,
361973
+ metadata: {
361974
+ patchId: patch5.id,
361975
+ patchMessage: patch5.original_message,
361976
+ baseGeneration: patch5.base_generation,
361977
+ currentGeneration: lockManager.read().current_generation
361978
+ },
361979
+ base: null,
361980
+ ours: null,
361981
+ oursPath: (0, import_node_path310.join)(outputDir, filePath),
361982
+ renameSourcePath: void 0,
361983
+ ghostTheirs: null,
361984
+ earlyExit: { status: "skipped", reason: "base-generation-not-found" }
361985
+ };
361986
+ }
361987
+ const lock = lockManager.read();
361988
+ const currentGen = lock.generations.find((g8) => g8.commit_sha === lock.current_generation);
361989
+ const currentTreeHash = currentGen?.tree_hash ?? baseGen.tree_hash;
361990
+ const resolvedPath = await resolveFilePath(filePath, baseGen.tree_hash, currentTreeHash);
361991
+ const metadata = {
361992
+ patchId: patch5.id,
361993
+ patchMessage: patch5.original_message,
361994
+ baseGeneration: patch5.base_generation,
361995
+ currentGeneration: lock.current_generation
361996
+ };
361997
+ let base4 = await git.showFile(baseGen.tree_hash, filePath);
361998
+ let renameSourcePath;
361999
+ if (!base4) {
362000
+ const renameSource = extractRenameSource(patch5.patch_content, filePath);
362001
+ if (renameSource) {
362002
+ base4 = await git.showFile(baseGen.tree_hash, renameSource);
362003
+ renameSourcePath = renameSource;
362004
+ }
362005
+ }
362006
+ const oursPath = (0, import_node_path310.join)(outputDir, resolvedPath);
362007
+ const ours = await (0, import_promises195.readFile)(oursPath, "utf-8").catch(() => null);
362008
+ let ghostTheirs = null;
362009
+ if (!base4 && ours && !renameSourcePath) {
362010
+ const treeReachable = await isTreeReachable(baseGen.tree_hash);
362011
+ if (!treeReachable) {
362012
+ const fileDiff = extractFileDiff2(patch5.patch_content, filePath);
362013
+ if (fileDiff) {
362014
+ const { reconstructFromGhostPatch: reconstructFromGhostPatch2 } = await Promise.resolve().then(() => (init_HybridReconstruction(), HybridReconstruction_exports));
362015
+ const result = reconstructFromGhostPatch2(fileDiff, ours);
362016
+ if (result) {
362017
+ base4 = result.base;
362018
+ ghostTheirs = result.theirs;
362019
+ }
362020
+ }
362021
+ }
362022
+ }
362023
+ return {
362024
+ resolvedPath,
362025
+ metadata,
362026
+ base: base4,
362027
+ ours,
362028
+ oursPath,
362029
+ renameSourcePath,
362030
+ ghostTheirs
362031
+ };
362032
+ }
362033
+ var CONFLICT_OPENER = "<<<<<<< Generated";
362034
+ var CONFLICT_SEPARATOR = "=======";
362035
+ var CONFLICT_CLOSER = ">>>>>>> Your customization";
362036
+ function trimCR(line) {
362037
+ return line.endsWith("\r") ? line.slice(0, -1) : line;
362038
+ }
362039
+ function findConflictRanges(lines) {
362040
+ const ranges = [];
362041
+ let i9 = 0;
362042
+ while (i9 < lines.length) {
362043
+ if (trimCR(lines[i9]) === CONFLICT_OPENER) {
362044
+ let separatorIdx = -1;
362045
+ let j7 = i9 + 1;
362046
+ let found = false;
362047
+ while (j7 < lines.length) {
362048
+ const trimmed2 = trimCR(lines[j7]);
362049
+ if (trimmed2 === CONFLICT_OPENER) {
362050
+ break;
362051
+ }
362052
+ if (separatorIdx === -1 && trimmed2 === CONFLICT_SEPARATOR) {
362053
+ separatorIdx = j7;
362054
+ } else if (separatorIdx !== -1 && trimmed2 === CONFLICT_CLOSER) {
362055
+ ranges.push({ start: i9, separator: separatorIdx, end: j7 });
362056
+ i9 = j7;
362057
+ found = true;
362058
+ break;
362059
+ }
362060
+ j7++;
362061
+ }
362062
+ if (!found) {
362063
+ i9++;
362064
+ continue;
362065
+ }
362066
+ }
362067
+ i9++;
362068
+ }
362069
+ return ranges;
362070
+ }
362071
+ function stripConflictMarkers(content5) {
362072
+ const lines = content5.split(/\r?\n/);
362073
+ const ranges = findConflictRanges(lines);
362074
+ if (ranges.length === 0) {
362075
+ return content5;
362076
+ }
362077
+ const result = [];
362078
+ let rangeIdx = 0;
362079
+ for (let i9 = 0; i9 < lines.length; i9++) {
362080
+ if (rangeIdx < ranges.length) {
362081
+ const range2 = ranges[rangeIdx];
362082
+ if (i9 === range2.start) {
362083
+ continue;
362084
+ }
362085
+ if (i9 > range2.start && i9 < range2.separator) {
362086
+ result.push(lines[i9]);
362087
+ continue;
362088
+ }
362089
+ if (i9 === range2.separator) {
362090
+ continue;
362091
+ }
362092
+ if (i9 > range2.separator && i9 < range2.end) {
362093
+ continue;
362094
+ }
362095
+ if (i9 === range2.end) {
362096
+ rangeIdx++;
362097
+ continue;
362098
+ }
362099
+ }
362100
+ result.push(lines[i9]);
362101
+ }
362102
+ return result.join("\n");
362103
+ }
362104
+ function hasConflictMarkerStrings(content5) {
362105
+ if (content5 == null) return false;
362106
+ return content5.includes(CONFLICT_OPENER) || content5.includes(CONFLICT_CLOSER);
362107
+ }
362108
+ async function buildMergePlan(args) {
362109
+ const {
362110
+ inputs,
362111
+ patch: patch5,
362112
+ filePath,
362113
+ accumulator,
362114
+ tempGit,
362115
+ tempDir: tempDir2,
362116
+ applyPatchToContent: applyPatchToContent2,
362117
+ isPatchAlreadyApplied
362118
+ } = args;
362119
+ const { base: base4, ours, resolvedPath, renameSourcePath, ghostTheirs } = inputs;
362120
+ let theirs = ghostTheirs;
362121
+ const ghostReconstructed = ghostTheirs != null;
362122
+ const snapshotForFile = patch5.theirs_snapshot?.[resolvedPath] ?? patch5.theirs_snapshot?.[filePath];
362123
+ const fileIsShared = accumulator.isShared(resolvedPath) || accumulator.isShared(filePath);
362124
+ const useSnapshotAsPrimary = !fileIsShared && snapshotForFile != null;
362125
+ let primarySource = ghostReconstructed ? "ghost" : "reconstructed";
362126
+ if (!ghostReconstructed) {
362127
+ if (useSnapshotAsPrimary) {
362128
+ theirs = snapshotForFile;
362129
+ primarySource = "snapshot";
362130
+ } else {
362131
+ theirs = await applyPatchToContent2(
362132
+ base4,
362133
+ patch5.patch_content,
362134
+ filePath,
362135
+ tempGit,
362136
+ tempDir2,
362137
+ renameSourcePath
362138
+ );
362139
+ const reconstructionHasMarkers = hasConflictMarkerStrings(theirs);
362140
+ const baseHadMarkers = hasConflictMarkerStrings(base4);
362141
+ if (reconstructionHasMarkers && !baseHadMarkers && snapshotForFile != null) {
362142
+ theirs = snapshotForFile;
362143
+ }
362144
+ }
362145
+ }
362146
+ const accumulatorEntry = accumulator.lookup(resolvedPath);
362147
+ if (theirs) {
362148
+ const theirsHasMarkers = hasConflictMarkerStrings(theirs);
362149
+ const baseHasMarkers = hasConflictMarkerStrings(base4);
362150
+ if (theirsHasMarkers && !baseHasMarkers) {
362151
+ if (accumulatorEntry) {
362152
+ theirs = null;
362153
+ } else {
362154
+ return { kind: "skipped", reason: "stale-conflict-markers" };
362155
+ }
362156
+ }
362157
+ }
362158
+ let useAccumulatorAsMergeBase = false;
362159
+ if (accumulatorEntry && (theirs == null || base4 == null)) {
362160
+ theirs = await applyPatchToContent2(
362161
+ accumulatorEntry.content,
362162
+ patch5.patch_content,
362163
+ filePath,
362164
+ tempGit,
362165
+ tempDir2
362166
+ );
362167
+ if (theirs != null) {
362168
+ useAccumulatorAsMergeBase = true;
362169
+ } else if (await isPatchAlreadyApplied(
362170
+ patch5.patch_content,
362171
+ filePath,
362172
+ accumulatorEntry.content,
362173
+ tempGit,
362174
+ tempDir2
362175
+ )) {
362176
+ theirs = accumulatorEntry.content;
362177
+ useAccumulatorAsMergeBase = true;
362178
+ }
362179
+ }
362180
+ if (theirs) {
362181
+ const theirsHasMarkers = hasConflictMarkerStrings(theirs);
362182
+ const accBaseHasMarkers = accumulatorEntry != null && hasConflictMarkerStrings(accumulatorEntry.content);
362183
+ if (theirsHasMarkers && !accBaseHasMarkers && !hasConflictMarkerStrings(base4)) {
362184
+ return { kind: "skipped", reason: "stale-conflict-markers" };
362185
+ }
362186
+ }
362187
+ let effective_theirs = theirs;
362188
+ let conflictReasonHint;
362189
+ if (theirs != null && base4 != null && !useAccumulatorAsMergeBase) {
362190
+ if (accumulatorEntry && accumulatorEntry.baseGeneration === patch5.base_generation) {
362191
+ try {
362192
+ const preMerged = threeWayMerge(base4, accumulatorEntry.content, theirs);
362193
+ if (!preMerged.hasConflicts) {
362194
+ effective_theirs = preMerged.content;
362195
+ } else {
362196
+ effective_theirs = theirs;
362197
+ }
362198
+ } catch {
362199
+ effective_theirs = theirs;
362200
+ }
362201
+ } else if (accumulatorEntry) {
362202
+ conflictReasonHint = "base-generation-mismatch";
362203
+ }
362204
+ }
362205
+ if (base4 == null && ours == null && effective_theirs != null) {
362206
+ return { kind: "new-file-only-user", theirs: effective_theirs };
362207
+ }
362208
+ if (base4 == null && ours != null && effective_theirs != null && !useAccumulatorAsMergeBase) {
362209
+ return { kind: "new-file-both", theirs: effective_theirs };
362210
+ }
362211
+ if (effective_theirs == null) {
362212
+ return { kind: "skipped", reason: "missing-content" };
362213
+ }
362214
+ if (base4 == null && !useAccumulatorAsMergeBase || ours == null) {
362215
+ return { kind: "skipped", reason: "missing-content" };
362216
+ }
362217
+ if (useAccumulatorAsMergeBase) {
362218
+ const mergeBase = accumulatorEntry?.content ?? base4;
362219
+ if (mergeBase == null) {
362220
+ return { kind: "skipped", reason: "missing-content" };
362221
+ }
362222
+ return {
362223
+ kind: "accumulator-base",
362224
+ theirs: effective_theirs,
362225
+ mergeBase,
362226
+ conflictReasonHint
362227
+ };
362228
+ }
362229
+ return {
362230
+ kind: primarySource,
362231
+ base: base4,
362232
+ theirs: effective_theirs,
362233
+ mergeBase: base4,
362234
+ conflictReasonHint
362235
+ };
362236
+ }
362237
+ var import_promises210 = require("fs/promises");
362238
+ var import_node_path43 = require("path");
362239
+ async function runMergeAndRecord(args) {
362240
+ const { plan, inputs, patch: patch5, accumulator } = args;
362241
+ const { resolvedPath, metadata, oursPath, ours } = inputs;
362242
+ if (plan.kind === "skipped") {
362243
+ return { file: resolvedPath, status: "skipped", reason: plan.reason };
362244
+ }
362245
+ if (plan.kind === "new-file-only-user") {
362246
+ accumulator.recordMerge(resolvedPath, plan.theirs, patch5.base_generation);
362247
+ await (0, import_promises210.mkdir)((0, import_node_path43.dirname)(oursPath), { recursive: true });
362248
+ await (0, import_promises210.writeFile)(oursPath, plan.theirs);
362249
+ return { file: resolvedPath, status: "merged", reason: "new-file" };
362250
+ }
362251
+ if (plan.kind === "new-file-both") {
362252
+ const merged2 = threeWayMerge("", ours, plan.theirs);
362253
+ await (0, import_promises210.mkdir)((0, import_node_path43.dirname)(oursPath), { recursive: true });
362254
+ await (0, import_promises210.writeFile)(oursPath, merged2.content);
362255
+ if (merged2.hasConflicts) {
362256
+ return {
362257
+ file: resolvedPath,
362258
+ status: "conflict",
362259
+ conflicts: merged2.conflicts,
362260
+ conflictReason: "new-file-both",
362261
+ conflictMetadata: metadata
362262
+ };
362263
+ }
362264
+ accumulator.recordMerge(resolvedPath, merged2.content, patch5.base_generation);
362265
+ return { file: resolvedPath, status: "merged" };
362266
+ }
362267
+ const merged = threeWayMerge(plan.mergeBase, ours, plan.theirs);
362268
+ await (0, import_promises210.mkdir)((0, import_node_path43.dirname)(oursPath), { recursive: true });
362269
+ await (0, import_promises210.writeFile)(oursPath, merged.content);
362270
+ if (merged.hasConflicts) {
362271
+ accumulator.recordConflict(resolvedPath, plan.theirs, patch5.base_generation);
362272
+ return {
362273
+ file: resolvedPath,
362274
+ status: "conflict",
362275
+ conflicts: merged.conflicts,
362276
+ conflictReason: plan.conflictReasonHint ?? "same-line-edit",
362277
+ conflictMetadata: metadata
362278
+ };
362279
+ }
362280
+ accumulator.recordMerge(resolvedPath, plan.theirs, patch5.base_generation);
362281
+ return {
362282
+ file: resolvedPath,
362283
+ status: "merged",
362284
+ ...merged.droppedContextLines && merged.droppedContextLines.length > 0 ? { droppedContextLines: merged.droppedContextLines } : {}
362285
+ };
362286
+ }
362287
+ var ReplayApplicator = class {
362288
+ git;
362289
+ lockManager;
362290
+ outputDir;
362291
+ renameCache = /* @__PURE__ */ new Map();
362292
+ treeExistsCache = /* @__PURE__ */ new Map();
362293
+ /**
362294
+ * Inter-patch THEIRS coordination. Created fresh in each `applyPatches`
362295
+ * invocation with the run's apply mode + precomputed shared-file set.
362296
+ * See `src/accumulator/MergeAccumulator.ts` for the full contract.
362297
+ *
362298
+ * Initialized to an empty default in the constructor so type narrowing
362299
+ * works for callers that read it before any `applyPatches` invocation
362300
+ * (e.g., the `getAccumulatorSnapshot` test accessor).
362301
+ */
362302
+ accumulator = new MergeAccumulator("replay", /* @__PURE__ */ new Set());
362303
+ constructor(git, lockManager, outputDir) {
362304
+ this.git = git;
362305
+ this.lockManager = lockManager;
362306
+ this.outputDir = outputDir;
362307
+ }
362308
+ /**
362309
+ * Resolve the GenerationRecord for a patch's base_generation.
362310
+ * Falls back to constructing an ad-hoc record from the commit's tree
362311
+ * when base_generation isn't a tracked generation (commit-scan patches).
362312
+ */
362313
+ async resolveBaseGeneration(baseGeneration) {
362314
+ const gen = this.lockManager.getGeneration(baseGeneration);
362315
+ if (gen) return gen;
362316
+ try {
362317
+ const treeHash = await this.git.getTreeHash(baseGeneration);
362318
+ return {
362319
+ commit_sha: baseGeneration,
362320
+ tree_hash: treeHash,
362321
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
362322
+ cli_version: "unknown",
362323
+ generator_versions: {}
362324
+ };
362325
+ } catch {
362326
+ return void 0;
362327
+ }
362328
+ }
362329
+ /**
362330
+ * @internal Test-only.
362331
+ * Returns a defensive copy of the inter-patch accumulator. Used by
362332
+ * invariant I2 to assert that after every applied patch, the
362333
+ * accumulator has an entry for each file the patch touched, and the
362334
+ * entry's content matches on-disk.
362335
+ */
362336
+ getAccumulatorSnapshot() {
362337
+ return this.accumulator.snapshot();
362338
+ }
362339
+ /**
362340
+ * Apply all patches, returning results for each.
362341
+ * Skips patches that match exclude patterns in replay.yml
362342
+ *
362343
+ * `applyMode` controls the post-conflict marker strategy:
362344
+ * - `"replay"` (default): strip conflict markers from disk between
362345
+ * iterations whenever a later patch touches the same file. Lets the
362346
+ * follow-up patch's `git apply --3way` see clean OURS content,
362347
+ * which is what the silent-loss fix (PR #73) relies on. The replay
362348
+ * pipeline calls `revertConflictingFiles` after the loop to clean
362349
+ * any markers that survive.
362350
+ * - `"resolve"`: keep markers on disk. The resolve command needs the
362351
+ * customer to see them. Slow-path 3-way merge naturally preserves
362352
+ * A's markers and B's clean writes when their regions don't overlap;
362353
+ * when they do overlap, the customer gets nested markers and
362354
+ * resolves manually. Either way they aren't silently dropped.
362355
+ */
362356
+ async applyPatches(patches, opts) {
362357
+ const applyMode = opts?.applyMode ?? "replay";
362358
+ const fileFreq = /* @__PURE__ */ new Map();
362359
+ for (const p14 of patches) {
362360
+ for (const f4 of p14.files) {
362361
+ fileFreq.set(f4, (fileFreq.get(f4) ?? 0) + 1);
362362
+ }
362363
+ }
362364
+ const sharedFiles = new Set(
362365
+ Array.from(fileFreq.entries()).filter(([, n19]) => n19 > 1).map(([f4]) => f4)
362366
+ );
362367
+ this.accumulator = new MergeAccumulator(applyMode, sharedFiles);
362368
+ const results = [];
362369
+ for (let i9 = 0; i9 < patches.length; i9++) {
362370
+ const patch5 = patches[i9];
362371
+ if (this.isExcluded(patch5)) {
362372
+ results.push({
362373
+ patch: patch5,
362374
+ status: "skipped",
362375
+ method: "git-am"
362376
+ });
362377
+ continue;
362378
+ }
362379
+ const result = await this.applyPatchWithFallback(patch5);
362380
+ results.push(result);
362381
+ if (applyMode !== "resolve" && result.status === "conflict" && result.fileResults) {
362382
+ const laterFiles = /* @__PURE__ */ new Set();
362383
+ for (let j7 = i9 + 1; j7 < patches.length; j7++) {
362384
+ for (const f4 of patches[j7].files) {
362385
+ laterFiles.add(f4);
362386
+ }
362387
+ }
362388
+ const resolvedToOriginal = /* @__PURE__ */ new Map();
362389
+ if (result.resolvedFiles) {
362390
+ for (const [orig, resolved] of Object.entries(result.resolvedFiles)) {
362391
+ resolvedToOriginal.set(resolved, orig);
362392
+ }
362393
+ }
362394
+ for (const fileResult of result.fileResults) {
362395
+ if (fileResult.status !== "conflict") continue;
362396
+ const originalPath = resolvedToOriginal.get(fileResult.file) ?? fileResult.file;
362397
+ if (laterFiles.has(fileResult.file) || laterFiles.has(originalPath)) {
362398
+ const filePath = (0, import_node_path52.join)(this.outputDir, fileResult.file);
362399
+ try {
362400
+ const content5 = await (0, import_promises310.readFile)(filePath, "utf-8");
362401
+ const stripped = stripConflictMarkers(content5);
362402
+ await (0, import_promises310.writeFile)(filePath, stripped);
362403
+ } catch {
362404
+ }
362405
+ }
362406
+ }
362407
+ }
362408
+ }
362409
+ return results;
362410
+ }
362411
+ /**
362412
+ * Populate accumulator after git apply succeeds, AND collect per-file
362413
+ * results including any "dropped context lines" — lines that were
362414
+ * present in BASE and THEIRS (unchanged context) but absent from the
362415
+ * final on-disk state because OURS deleted them. This is the fast-path
362416
+ * counterpart to ThreeWayMerge.computeDroppedContextLines used by the
362417
+ * 3-way slow path; both must surface the same warning.
362418
+ */
362419
+ async populateAccumulatorForPatch(patch5, baseGen, currentTreeHash) {
362420
+ const fileResults = [];
362421
+ if (!baseGen) return fileResults;
362422
+ const tempDir2 = await (0, import_promises310.mkdtemp)((0, import_node_path52.join)((0, import_node_os15.tmpdir)(), "replay-acc-"));
362423
+ const { GitClient: GitClient2 } = await Promise.resolve().then(() => (init_GitClient(), GitClient_exports));
362424
+ const tempGit = new GitClient2(tempDir2);
362425
+ await tempGit.exec(["init"]);
362426
+ await tempGit.exec(["config", "user.email", "replay@fern.com"]);
362427
+ await tempGit.exec(["config", "user.name", "Fern Replay"]);
362428
+ try {
362429
+ for (const filePath of patch5.files) {
362430
+ if (isBinaryFile(filePath)) continue;
362431
+ const resolvedPath = await this.resolveFilePath(filePath, baseGen.tree_hash, currentTreeHash);
362432
+ const base4 = await this.git.showFile(baseGen.tree_hash, filePath);
362433
+ const snapshotForFile = patch5.theirs_snapshot?.[resolvedPath] ?? patch5.theirs_snapshot?.[filePath];
362434
+ const fileIsShared = this.accumulator.isShared(resolvedPath) || this.accumulator.isShared(filePath);
362435
+ const theirs = !fileIsShared && snapshotForFile != null ? snapshotForFile : await this.applyPatchToContent(base4, patch5.patch_content, filePath, tempGit, tempDir2);
362436
+ const finalOnDisk = await (0, import_promises310.readFile)((0, import_node_path52.join)(this.outputDir, resolvedPath), "utf-8").catch(() => null);
362437
+ const effectiveTheirs = theirs ?? finalOnDisk;
362438
+ if (effectiveTheirs != null) {
362439
+ this.accumulator.recordMerge(resolvedPath, effectiveTheirs, patch5.base_generation);
362440
+ }
362441
+ if (base4 != null && effectiveTheirs != null && finalOnDisk != null) {
362442
+ const dropped = computeDroppedContextLinesForFile(base4, effectiveTheirs, finalOnDisk);
362443
+ if (dropped.length > 0) {
362444
+ fileResults.push({
362445
+ file: resolvedPath,
362446
+ status: "merged",
362447
+ droppedContextLines: dropped
362448
+ });
362449
+ }
362450
+ }
362451
+ }
362452
+ } finally {
362453
+ await (0, import_promises310.rm)(tempDir2, { recursive: true }).catch(() => {
362454
+ });
362455
+ }
362456
+ return fileResults;
362457
+ }
362458
+ async applyPatchWithFallback(patch5) {
362459
+ const baseGen = await this.resolveBaseGeneration(patch5.base_generation);
362460
+ const lock = this.lockManager.read();
362461
+ const currentGen = lock.generations.find((g8) => g8.commit_sha === lock.current_generation);
362462
+ const currentTreeHash = currentGen?.tree_hash ?? baseGen?.tree_hash ?? "";
362463
+ const needsAccumulation = await Promise.all(
362464
+ patch5.files.map(async (f4) => {
362465
+ if (!baseGen) return false;
362466
+ const resolved = await this.resolveFilePath(f4, baseGen.tree_hash, currentTreeHash);
362467
+ return this.accumulator.has(resolved);
362468
+ })
362469
+ ).then((results) => results.some(Boolean));
362470
+ const resolvedFiles = {};
362471
+ for (const filePath of patch5.files) {
362472
+ const resolvedPath = baseGen ? await this.resolveFilePath(filePath, baseGen.tree_hash, currentTreeHash) : filePath;
362473
+ if (resolvedPath !== filePath) {
362474
+ resolvedFiles[filePath] = resolvedPath;
362475
+ }
362476
+ }
362477
+ const patchIsRenameAware = /^rename from /m.test(patch5.patch_content);
362478
+ const hasGeneratorRename = Object.keys(resolvedFiles).length > 0 && !patchIsRenameAware;
362479
+ if (!needsAccumulation && !hasGeneratorRename) {
362480
+ const snapshots = /* @__PURE__ */ new Map();
362481
+ for (const filePath of patch5.files) {
362482
+ const fullPath = (0, import_node_path52.join)(this.outputDir, filePath);
362483
+ snapshots.set(filePath, await (0, import_promises310.readFile)(fullPath, "utf-8").catch(() => null));
362484
+ }
362485
+ try {
362486
+ await this.git.execWithInput(["apply", "--3way"], patch5.patch_content);
362487
+ const fastPathFileResults = await this.populateAccumulatorForPatch(patch5, baseGen, currentTreeHash);
362488
+ return {
362489
+ patch: patch5,
362490
+ status: "applied",
362491
+ method: "git-am",
362492
+ ...fastPathFileResults.length > 0 && { fileResults: fastPathFileResults },
362493
+ ...Object.keys(resolvedFiles).length > 0 && { resolvedFiles }
362494
+ };
362495
+ } catch {
362496
+ for (const [filePath, content5] of snapshots) {
362497
+ const fullPath = (0, import_node_path52.join)(this.outputDir, filePath);
362498
+ if (content5 != null) {
362499
+ await (0, import_promises310.writeFile)(fullPath, content5);
362500
+ } else {
362501
+ await (0, import_promises310.unlink)(fullPath).catch(() => {
362502
+ });
362503
+ }
362504
+ }
362505
+ }
362506
+ }
362507
+ return this.applyWithThreeWayMerge(patch5);
362508
+ }
362509
+ async applyWithThreeWayMerge(patch5) {
362510
+ const fileResults = [];
362511
+ const resolvedFiles = {};
362512
+ const tempDir2 = await (0, import_promises310.mkdtemp)((0, import_node_path52.join)((0, import_node_os15.tmpdir)(), "replay-"));
362513
+ const { GitClient: GitClient2 } = await Promise.resolve().then(() => (init_GitClient(), GitClient_exports));
362514
+ const tempGit = new GitClient2(tempDir2);
362515
+ await tempGit.exec(["init"]);
362516
+ await tempGit.exec(["config", "user.email", "replay@fern.com"]);
362517
+ await tempGit.exec(["config", "user.name", "Fern Replay"]);
362518
+ try {
362519
+ for (const filePath of patch5.files) {
362520
+ if (isBinaryFile(filePath)) {
362521
+ fileResults.push({
362522
+ file: filePath,
362523
+ status: "skipped",
362524
+ reason: "binary-file"
362525
+ });
362526
+ continue;
362527
+ }
362528
+ const result = await this.mergeFile(patch5, filePath, tempGit, tempDir2);
362529
+ if (result.file !== filePath) {
362530
+ resolvedFiles[filePath] = result.file;
362531
+ }
362532
+ fileResults.push(result);
362533
+ }
362534
+ } finally {
362535
+ await (0, import_promises310.rm)(tempDir2, { recursive: true }).catch(() => {
362536
+ });
362537
+ }
362538
+ const conflictFiles = fileResults.filter((r5) => r5.status === "conflict");
362539
+ const hasConflicts = conflictFiles.length > 0;
362540
+ const conflictReason = hasConflicts ? conflictFiles.some((f4) => f4.conflictReason === "base-generation-mismatch") ? "base-generation-mismatch" : conflictFiles[0]?.conflictReason : void 0;
362541
+ return {
362542
+ patch: patch5,
362543
+ status: hasConflicts ? "conflict" : "applied",
362544
+ method: "3way-merge",
362545
+ fileResults,
362546
+ conflictReason,
362547
+ ...Object.keys(resolvedFiles).length > 0 && { resolvedFiles }
362548
+ };
362549
+ }
362550
+ async mergeFile(patch5, filePath, tempGit, tempDir2) {
362551
+ try {
362552
+ const inputs = await resolveInputs({
362553
+ patch: patch5,
362554
+ filePath,
362555
+ git: this.git,
362556
+ lockManager: this.lockManager,
362557
+ outputDir: this.outputDir,
362558
+ isTreeReachable: (h5) => this.isTreeReachable(h5),
362559
+ resolveBaseGeneration: (s9) => this.resolveBaseGeneration(s9),
362560
+ resolveFilePath: (f4, b8, c6) => this.resolveFilePath(f4, b8, c6),
362561
+ extractRenameSource: (p14, f4) => this.extractRenameSource(p14, f4),
362562
+ extractFileDiff: (p14, f4) => this.extractFileDiff(p14, f4)
362563
+ });
362564
+ if (inputs.earlyExit) {
362565
+ return { file: filePath, ...inputs.earlyExit };
362566
+ }
362567
+ const plan = await buildMergePlan({
362568
+ inputs,
362569
+ patch: patch5,
362570
+ filePath,
362571
+ accumulator: this.accumulator,
362572
+ tempGit,
362573
+ tempDir: tempDir2,
362574
+ applyPatchToContent: (b8, p14, f4, g8, d5, s9) => this.applyPatchToContent(b8, p14, f4, g8, d5, s9),
362575
+ isPatchAlreadyApplied: (p14, f4, c6, g8, d5) => this.isPatchAlreadyApplied(p14, f4, c6, g8, d5)
362576
+ });
362577
+ return await runMergeAndRecord({
362578
+ plan,
362579
+ inputs,
362580
+ patch: patch5,
362581
+ accumulator: this.accumulator
362582
+ });
362583
+ } catch (error50) {
362584
+ return {
362585
+ file: filePath,
362586
+ status: "skipped",
362587
+ reason: `error: ${error50 instanceof Error ? error50.message : String(error50)}`
362588
+ };
362589
+ }
362590
+ }
362591
+ async isTreeReachable(treeHash) {
362592
+ let result = this.treeExistsCache.get(treeHash);
362593
+ if (result === void 0) {
362594
+ result = await this.git.treeExists(treeHash);
362595
+ this.treeExistsCache.set(treeHash, result);
362596
+ }
362597
+ return result;
362598
+ }
362599
+ isExcluded(patch5) {
362600
+ const config5 = this.lockManager.getCustomizationsConfig();
362601
+ if (!config5.exclude) return false;
362602
+ return patch5.files.some((file4) => config5.exclude.some((pattern) => (0, import_minimatch8.minimatch)(file4, pattern)));
362603
+ }
362604
+ async resolveFilePath(filePath, baseTreeHash, currentTreeHash) {
362605
+ const config5 = this.lockManager.getCustomizationsConfig();
362606
+ if (config5.moves) {
362607
+ for (const move of config5.moves) {
362608
+ if ((0, import_minimatch8.minimatch)(filePath, move.from) || filePath === move.from) {
362609
+ if (filePath === move.from) {
362610
+ return move.to;
362611
+ }
362612
+ const fromBase = move.from.replace(/\*\*.*$/, "");
362613
+ const toBase = move.to.replace(/\*\*.*$/, "");
362614
+ if (filePath.startsWith(fromBase)) {
362615
+ return toBase + filePath.slice(fromBase.length);
362616
+ }
362617
+ }
362618
+ }
362619
+ }
362620
+ const cacheKey = `${baseTreeHash}:${currentTreeHash}`;
362621
+ let renames = this.renameCache.get(cacheKey);
362622
+ if (!renames) {
362623
+ renames = await this.git.detectRenames(baseTreeHash, currentTreeHash);
362624
+ this.renameCache.set(cacheKey, renames);
362625
+ }
362626
+ const gitRename = renames.find((r5) => r5.from === filePath);
362627
+ if (gitRename) {
362628
+ return gitRename.to;
362629
+ }
362630
+ return filePath;
362631
+ }
362632
+ async applyPatchToContent(base4, patchContent, filePath, tempGit, tempDir2, sourceFilePath) {
362633
+ if (!base4) {
362634
+ return this.extractNewFileFromPatch(patchContent, filePath);
362635
+ }
362636
+ const fileDiff = this.extractFileDiff(patchContent, filePath);
362637
+ if (!fileDiff) return null;
362638
+ try {
362639
+ if (sourceFilePath) {
362640
+ const tempSourcePath = (0, import_node_path52.join)(tempDir2, sourceFilePath);
362641
+ await (0, import_promises310.mkdir)((0, import_node_path52.dirname)(tempSourcePath), { recursive: true });
362642
+ await (0, import_promises310.writeFile)(tempSourcePath, base4);
362643
+ await tempGit.exec(["add", sourceFilePath]);
362644
+ await tempGit.exec([
362645
+ "commit",
362646
+ "-m",
362647
+ `base for rename ${sourceFilePath} -> ${filePath}`,
362648
+ "--allow-empty"
362649
+ ]);
362650
+ await tempGit.execWithInput(["apply", "--allow-empty"], fileDiff);
362651
+ const tempTargetPath = (0, import_node_path52.join)(tempDir2, filePath);
362652
+ return await (0, import_promises310.readFile)(tempTargetPath, "utf-8");
362653
+ }
362654
+ const tempFilePath = (0, import_node_path52.join)(tempDir2, filePath);
362655
+ await (0, import_promises310.mkdir)((0, import_node_path52.dirname)(tempFilePath), { recursive: true });
362656
+ await (0, import_promises310.writeFile)(tempFilePath, base4);
362657
+ await tempGit.exec(["add", filePath]);
362658
+ await tempGit.exec(["commit", "-m", `base for ${filePath}`, "--allow-empty"]);
362659
+ await tempGit.execWithInput(["apply", "--allow-empty"], fileDiff);
362660
+ return await (0, import_promises310.readFile)(tempFilePath, "utf-8");
362661
+ } catch {
362662
+ return null;
362663
+ }
362664
+ }
362665
+ /**
362666
+ * Detects whether a patch's additions are already present in `content`.
362667
+ * Uses `git apply --reverse --check` — if the reverse patch applies cleanly,
362668
+ * the forward patch is effectively a no-op (its +lines are already there and
362669
+ * its -lines are already gone). Used to distinguish "patch already applied"
362670
+ * from "patch base mismatch" after a forward-apply fails.
362671
+ */
362672
+ async isPatchAlreadyApplied(patchContent, filePath, content5, tempGit, tempDir2) {
362673
+ const fileDiff = this.extractFileDiff(patchContent, filePath);
362674
+ if (!fileDiff) return false;
362675
+ try {
362676
+ const tempFilePath = (0, import_node_path52.join)(tempDir2, filePath);
362677
+ await (0, import_promises310.mkdir)((0, import_node_path52.dirname)(tempFilePath), { recursive: true });
362678
+ await (0, import_promises310.writeFile)(tempFilePath, content5);
362679
+ await tempGit.exec(["add", filePath]);
362680
+ await tempGit.exec(["commit", "-m", `check already-applied ${filePath}`, "--allow-empty"]);
362681
+ await tempGit.execWithInput(["apply", "--reverse", "--check", "--allow-empty"], fileDiff);
362682
+ return true;
362683
+ } catch {
362684
+ return false;
362685
+ }
362686
+ }
362687
+ extractFileDiff(patchContent, filePath) {
362688
+ const lines = patchContent.split("\n");
362689
+ const diffLines = [];
362690
+ let inTargetFile = false;
362691
+ for (const line of lines) {
362692
+ if (line.startsWith("diff --git")) {
362693
+ if (inTargetFile) {
362694
+ break;
362695
+ }
362696
+ if (isDiffLineForFile(line, filePath)) {
362697
+ inTargetFile = true;
362698
+ diffLines.push(line);
362699
+ }
362700
+ continue;
362701
+ }
362702
+ if (inTargetFile) {
362703
+ diffLines.push(line);
362704
+ }
362705
+ }
362706
+ return diffLines.length > 0 ? diffLines.join("\n") + "\n" : null;
362707
+ }
362708
+ extractRenameSource(patchContent, targetFilePath) {
362709
+ const lines = patchContent.split("\n");
362710
+ let inTargetFile = false;
362711
+ for (const line of lines) {
362712
+ if (line.startsWith("diff --git")) {
362713
+ if (inTargetFile) break;
362714
+ inTargetFile = isDiffLineForFile(line, targetFilePath);
362715
+ continue;
362716
+ }
362717
+ if (!inTargetFile) continue;
362718
+ if (line.startsWith("@@")) break;
362719
+ if (line.startsWith("rename from ")) {
362720
+ return line.slice("rename from ".length);
362721
+ }
362722
+ }
362723
+ return null;
362724
+ }
362725
+ extractNewFileFromPatch(patchContent, filePath) {
362726
+ const lines = patchContent.split("\n");
362727
+ const addedLines = [];
362728
+ let inTargetFile = false;
362729
+ let inHunk = false;
362730
+ let isNewFile = false;
362731
+ let noTrailingNewline = false;
362732
+ for (const line of lines) {
362733
+ if (line.startsWith("diff --git")) {
362734
+ if (inTargetFile) break;
362735
+ inTargetFile = isDiffLineForFile(line, filePath);
362736
+ inHunk = false;
362737
+ isNewFile = false;
362738
+ continue;
362739
+ }
362740
+ if (!inTargetFile) continue;
362741
+ if (!inHunk) {
362742
+ if (line === "--- /dev/null" || line.startsWith("new file mode")) {
362743
+ isNewFile = true;
362744
+ }
362745
+ }
362746
+ if (line.startsWith("@@")) {
362747
+ if (!isNewFile) return null;
362748
+ inHunk = true;
362749
+ continue;
362750
+ }
362751
+ if (!inHunk) continue;
362752
+ if (line === "\") {
362753
+ noTrailingNewline = true;
362754
+ continue;
362755
+ }
362756
+ if (line.startsWith("+") && !line.startsWith("+++")) {
362757
+ addedLines.push(line.slice(1));
362758
+ }
362759
+ }
362760
+ if (addedLines.length === 0) return null;
362761
+ return addedLines.join("\n") + (noTrailingNewline ? "" : "\n");
362762
+ }
362763
+ };
362764
+ function computeDroppedContextLinesForFile(base4, theirs, finalContent) {
362765
+ const baseLines = base4.split("\n");
362766
+ const theirsLines = theirs.split("\n");
362767
+ const finalLines = finalContent.split("\n");
362768
+ const baseCounts = /* @__PURE__ */ new Map();
362769
+ const theirsCounts = /* @__PURE__ */ new Map();
362770
+ const finalCounts = /* @__PURE__ */ new Map();
362771
+ for (const l9 of baseLines) baseCounts.set(l9, (baseCounts.get(l9) ?? 0) + 1);
362772
+ for (const l9 of theirsLines) theirsCounts.set(l9, (theirsCounts.get(l9) ?? 0) + 1);
362773
+ for (const l9 of finalLines) finalCounts.set(l9, (finalCounts.get(l9) ?? 0) + 1);
362774
+ const dropped = [];
362775
+ const seen = /* @__PURE__ */ new Set();
362776
+ for (const line of baseLines) {
362777
+ if (seen.has(line)) continue;
362778
+ const inBase = baseCounts.get(line) ?? 0;
362779
+ const inTheirs = theirsCounts.get(line) ?? 0;
362780
+ const inFinal = finalCounts.get(line) ?? 0;
362781
+ if (inBase > 0 && inTheirs > 0 && inFinal < Math.min(inBase, inTheirs)) {
362782
+ dropped.push(line);
362783
+ seen.add(line);
362784
+ }
362785
+ }
362786
+ return dropped;
362787
+ }
362788
+ function isDiffLineForFile(diffLine, filePath) {
362789
+ const match3 = diffLine.match(/^diff --git a\/.+ b\/(.+)$/);
362790
+ return match3 !== null && match3[1] === filePath;
362791
+ }
362671
362792
  var ReplayCommitter = class {
362672
362793
  git;
362673
362794
  outputDir;
@@ -362770,13 +362891,13 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
362770
362891
  }
362771
362892
  };
362772
362893
  var import_node_fs210 = require("fs");
362773
- var import_promises410 = require("fs/promises");
362774
- var import_node_path52 = require("path");
362775
- var import_minimatch22 = require_commonjs3();
362894
+ var import_promises610 = require("fs/promises");
362895
+ var import_node_path82 = require("path");
362896
+ var import_minimatch32 = require_commonjs3();
362776
362897
  init_GitClient();
362777
- var import_promises210 = require("fs/promises");
362898
+ var import_promises410 = require("fs/promises");
362778
362899
  var import_node_os22 = require("os");
362779
- var import_node_path310 = require("path");
362900
+ var import_node_path62 = require("path");
362780
362901
  var import_node_child_process8 = require("child_process");
362781
362902
  init_HybridReconstruction();
362782
362903
  function normalizeHunkBody(hunk) {
@@ -362983,17 +363104,17 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
362983
363104
  }
362984
363105
  async function unifiedDiff(beforeContent, afterContent, filePath) {
362985
363106
  if (beforeContent === afterContent) return "";
362986
- const tmp18 = await (0, import_promises210.mkdtemp)((0, import_node_path310.join)((0, import_node_os22.tmpdir)(), "fern-replay-pp-"));
363107
+ const tmp18 = await (0, import_promises410.mkdtemp)((0, import_node_path62.join)((0, import_node_os22.tmpdir)(), "fern-replay-pp-"));
362987
363108
  try {
362988
- const beforePath = (0, import_node_path310.join)(tmp18, "before");
362989
- const afterPath = (0, import_node_path310.join)(tmp18, "after");
362990
- await (0, import_promises210.writeFile)(beforePath, beforeContent, "utf-8");
362991
- await (0, import_promises210.writeFile)(afterPath, afterContent, "utf-8");
363109
+ const beforePath = (0, import_node_path62.join)(tmp18, "before");
363110
+ const afterPath = (0, import_node_path62.join)(tmp18, "after");
363111
+ await (0, import_promises410.writeFile)(beforePath, beforeContent, "utf-8");
363112
+ await (0, import_promises410.writeFile)(afterPath, afterContent, "utf-8");
362992
363113
  const raw = await runGitDiffNoIndex(beforePath, afterPath);
362993
363114
  if (!raw.trim()) return "";
362994
363115
  return rewriteDiffHeaders(raw, filePath);
362995
363116
  } finally {
362996
- await (0, import_promises210.rm)(tmp18, { recursive: true, force: true });
363117
+ await (0, import_promises410.rm)(tmp18, { recursive: true, force: true });
362997
363118
  }
362998
363119
  }
362999
363120
  function runGitDiffNoIndex(beforePath, afterPath) {
@@ -363064,27 +363185,513 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
363064
363185
  const trailer = hasTrailingNewline ? "" : "\n\";
363065
363186
  return header + "\n" + body + trailer + "\n";
363066
363187
  }
363188
+ var import_minimatch22 = require_commonjs3();
363189
+ var FileOwnership = class {
363190
+ constructor(git) {
363191
+ this.git = git;
363192
+ }
363193
+ /**
363194
+ * Canonical resolution. Returns true if `file` is user-owned per:
363195
+ *
363196
+ * 1. `patch.user_owned === true` — authoritative, persisted flag.
363197
+ * 2. `file === ".fernignore"` — always user-owned.
363198
+ * 3. File matches a `.fernignore` pattern (via minimatch).
363199
+ * 4. Patch content shows `--- /dev/null` for this file (legacy-safe
363200
+ * signal for patches predating `user_owned` or whose flag was lost).
363201
+ * 5. Tree-based check against `patch.base_generation` when reachable
363202
+ * AND distinct from `currentGenSha`. Unreachable base → conservative
363203
+ * user-owned (silent-absorption safety net), with a one-time warning
363204
+ * per base SHA.
363205
+ * 6. Final fallback: check `currentGenSha` when no usable base exists
363206
+ * (legacy one-gen lockfile).
363207
+ */
363208
+ async resolveCanonical(ctx) {
363209
+ const { file: file4, originalFileName, patch: patch5, currentGenSha, fernignorePatterns, warnedGens, warnings } = ctx;
363210
+ if (patch5.user_owned) return true;
363211
+ if (file4 === ".fernignore") return true;
363212
+ if (fernignorePatterns.some((p14) => file4 === p14 || (0, import_minimatch22.minimatch)(file4, p14))) return true;
363213
+ if (fileIsCreationInPatchContent(patch5.patch_content, originalFileName ?? file4)) {
363214
+ return true;
363215
+ }
363216
+ const base4 = patch5.base_generation;
363217
+ if (base4 && base4 !== currentGenSha) {
363218
+ const reachable = await this.git.commitExists(base4) || await this.git.treeExists(base4);
363219
+ if (!reachable) {
363220
+ if (!warnedGens.has(base4)) {
363221
+ warnedGens.add(base4);
363222
+ warnings.push(
363223
+ `Base generation ${base4.slice(0, 7)} is unreachable (shallow clone or pruned history). Treating affected files as user-owned to avoid silent data loss. Run \`git fetch --unshallow\` or avoid \`git gc --prune\` to restore full history.`
363224
+ );
363225
+ }
363226
+ return true;
363227
+ }
363228
+ return await this.git.showFile(base4, originalFileName ?? file4) === null;
363229
+ }
363230
+ return await this.git.showFile(currentGenSha, originalFileName ?? file4) === null;
363231
+ }
363232
+ };
363233
+ function fileIsCreationInPatchContent(patchContent, file4) {
363234
+ const lines = patchContent.split("\n");
363235
+ let inFileSection = false;
363236
+ for (const line of lines) {
363237
+ if (line.startsWith("diff --git ")) {
363238
+ inFileSection = line.includes(` b/${file4}`) || line.endsWith(` b/${file4}`);
363239
+ continue;
363240
+ }
363241
+ if (inFileSection && line.startsWith("--- ")) {
363242
+ return line === "--- /dev/null";
363243
+ }
363244
+ }
363245
+ return false;
363246
+ }
363247
+ var FirstGenerationFlow = class {
363248
+ constructor(service) {
363249
+ this.service = service;
363250
+ }
363251
+ async prepare(options2) {
363252
+ if (options2?.dryRun) {
363253
+ const headSha = await this.service.git.exec(["rev-parse", "HEAD"]).then((s9) => s9.trim()).catch(() => "");
363254
+ return {
363255
+ flow: "first-generation",
363256
+ previousGenerationSha: null,
363257
+ currentGenerationSha: headSha,
363258
+ baseBranchHead: options2.baseBranchHead ?? null,
363259
+ _prepared: {
363260
+ kind: "terminal",
363261
+ flow: "first-generation",
363262
+ preparedReport: firstGenerationReport()
363263
+ }
363264
+ };
363265
+ }
363266
+ const commitOpts = options2 ? {
363267
+ cliVersion: options2.cliVersion ?? "unknown",
363268
+ generatorVersions: options2.generatorVersions ?? {},
363269
+ baseBranchHead: options2.baseBranchHead
363270
+ } : void 0;
363271
+ await this.service.committer.commitGeneration("Initial SDK generation", commitOpts);
363272
+ const genRecord = await this.service.committer.createGenerationRecord(commitOpts);
363273
+ this.service.lockManager.initializeInMemory(genRecord);
363274
+ return {
363275
+ flow: "first-generation",
363276
+ previousGenerationSha: null,
363277
+ currentGenerationSha: genRecord.commit_sha,
363278
+ baseBranchHead: options2?.baseBranchHead ?? null,
363279
+ _prepared: {
363280
+ kind: "active",
363281
+ flow: "first-generation",
363282
+ patchesToApply: [],
363283
+ newPatchIds: /* @__PURE__ */ new Set(),
363284
+ detectorWarnings: [],
363285
+ revertedCount: 0,
363286
+ genSha: genRecord.commit_sha,
363287
+ optionsSnapshot: options2
363288
+ }
363289
+ };
363290
+ }
363291
+ async apply(_prep) {
363292
+ this.service.lockManager.save();
363293
+ return firstGenerationReport();
363294
+ }
363295
+ };
363296
+ function firstGenerationReport() {
363297
+ return {
363298
+ flow: "first-generation",
363299
+ patchesDetected: 0,
363300
+ patchesApplied: 0,
363301
+ patchesWithConflicts: 0,
363302
+ patchesSkipped: 0,
363303
+ conflicts: []
363304
+ };
363305
+ }
363306
+ var SkipApplicationFlow = class {
363307
+ constructor(service) {
363308
+ this.service = service;
363309
+ }
363310
+ async prepare(options2) {
363311
+ const commitOpts = options2 ? {
363312
+ cliVersion: options2.cliVersion ?? "unknown",
363313
+ generatorVersions: options2.generatorVersions ?? {},
363314
+ baseBranchHead: options2.baseBranchHead
363315
+ } : void 0;
363316
+ await this.service.committer.commitGeneration("Update SDK (replay skipped)", commitOpts);
363317
+ await this.service.cleanupStaleConflictMarkers();
363318
+ const genRecord = await this.service.committer.createGenerationRecord(commitOpts);
363319
+ let previousGenerationSha = null;
363320
+ try {
363321
+ const lock = this.service.lockManager.read();
363322
+ previousGenerationSha = lock.current_generation ?? null;
363323
+ this.service.lockManager.addGeneration(genRecord);
363324
+ } catch (error50) {
363325
+ if (error50 instanceof LockfileNotFoundError) {
363326
+ this.service.lockManager.initializeInMemory(genRecord);
363327
+ } else {
363328
+ throw error50;
363329
+ }
363330
+ }
363331
+ this.service.lockManager.setReplaySkippedAt((/* @__PURE__ */ new Date()).toISOString());
363332
+ return {
363333
+ flow: "skip-application",
363334
+ previousGenerationSha,
363335
+ currentGenerationSha: genRecord.commit_sha,
363336
+ baseBranchHead: options2?.baseBranchHead ?? null,
363337
+ _prepared: {
363338
+ kind: "active",
363339
+ flow: "skip-application",
363340
+ patchesToApply: [],
363341
+ newPatchIds: /* @__PURE__ */ new Set(),
363342
+ detectorWarnings: [],
363343
+ revertedCount: 0,
363344
+ genSha: genRecord.commit_sha,
363345
+ optionsSnapshot: options2
363346
+ }
363347
+ };
363348
+ }
363349
+ async apply(_prep, options2) {
363350
+ this.service.lockManager.save();
363351
+ const credentialWarnings = [...this.service.lockManager.warnings];
363352
+ if (!options2?.stageOnly) {
363353
+ await this.service.committer.commitReplay(0);
363354
+ } else {
363355
+ await this.service.committer.stageAll();
363356
+ }
363357
+ return {
363358
+ flow: "skip-application",
363359
+ patchesDetected: 0,
363360
+ patchesApplied: 0,
363361
+ patchesWithConflicts: 0,
363362
+ patchesSkipped: 0,
363363
+ conflicts: [],
363364
+ warnings: credentialWarnings.length > 0 ? credentialWarnings : void 0
363365
+ };
363366
+ }
363367
+ };
363368
+ var NoPatchesFlow = class {
363369
+ constructor(service) {
363370
+ this.service = service;
363371
+ }
363372
+ async prepare(options2) {
363373
+ const preLock = this.service.lockManager.read();
363374
+ const previousGenerationSha = preLock.current_generation ?? null;
363375
+ let { patches: newPatches, revertedPatchIds } = await this.service.detector.detectNewPatches();
363376
+ const detectorWarnings = [...this.service.detector.warnings];
363377
+ if (options2?.dryRun) {
363378
+ const dryRunWarnings = [...detectorWarnings, ...this.service.warnings];
363379
+ const preparedReport = {
363380
+ flow: "no-patches",
363381
+ patchesDetected: newPatches.length,
363382
+ patchesApplied: 0,
363383
+ patchesWithConflicts: 0,
363384
+ patchesSkipped: 0,
363385
+ patchesReverted: revertedPatchIds.length,
363386
+ conflicts: [],
363387
+ wouldApply: newPatches,
363388
+ warnings: dryRunWarnings.length > 0 ? dryRunWarnings : void 0
363389
+ };
363390
+ const headSha = await this.service.git.exec(["rev-parse", "HEAD"]).then((s9) => s9.trim()).catch(() => "");
363391
+ return {
363392
+ flow: "no-patches",
363393
+ previousGenerationSha,
363394
+ currentGenerationSha: headSha,
363395
+ baseBranchHead: options2.baseBranchHead ?? null,
363396
+ _prepared: {
363397
+ kind: "terminal",
363398
+ flow: "no-patches",
363399
+ preparedReport
363400
+ }
363401
+ };
363402
+ }
363403
+ {
363404
+ const preCurrentGen = preLock.current_generation;
363405
+ const uniqueFiles = [...new Set(newPatches.flatMap((p14) => p14.files))];
363406
+ const absorbedFiles = /* @__PURE__ */ new Set();
363407
+ for (const file4 of uniqueFiles) {
363408
+ const diff2 = await this.service.git.exec(["diff", preCurrentGen, "HEAD", "--", file4]).catch(() => null);
363409
+ if (diff2 !== null && !diff2.trim()) {
363410
+ absorbedFiles.add(file4);
363411
+ }
363412
+ }
363413
+ if (absorbedFiles.size > 0) {
363414
+ newPatches = newPatches.filter(
363415
+ (p14) => p14.files.length === 0 || !p14.files.every((f4) => absorbedFiles.has(f4))
363416
+ );
363417
+ }
363418
+ }
363419
+ for (const id2 of revertedPatchIds) {
363420
+ try {
363421
+ this.service.lockManager.removePatch(id2);
363422
+ } catch {
363423
+ }
363424
+ }
363425
+ const commitOpts = options2 ? {
363426
+ cliVersion: options2.cliVersion ?? "unknown",
363427
+ generatorVersions: options2.generatorVersions ?? {},
363428
+ baseBranchHead: options2.baseBranchHead
363429
+ } : void 0;
363430
+ await this.service.committer.commitGeneration("Update SDK", commitOpts);
363431
+ await this.service.cleanupStaleConflictMarkers();
363432
+ const genRecord = await this.service.committer.createGenerationRecord(commitOpts);
363433
+ this.service.lockManager.addGeneration(genRecord);
363434
+ return {
363435
+ flow: "no-patches",
363436
+ previousGenerationSha,
363437
+ currentGenerationSha: genRecord.commit_sha,
363438
+ baseBranchHead: options2?.baseBranchHead ?? null,
363439
+ _prepared: {
363440
+ kind: "active",
363441
+ flow: "no-patches",
363442
+ patchesToApply: newPatches,
363443
+ newPatchIds: new Set(newPatches.map((p14) => p14.id)),
363444
+ detectorWarnings,
363445
+ revertedCount: revertedPatchIds.length,
363446
+ genSha: genRecord.commit_sha,
363447
+ optionsSnapshot: options2
363448
+ }
363449
+ };
363450
+ }
363451
+ async apply(prep, options2) {
363452
+ assertActive(prep);
363453
+ const newPatches = prep._prepared.patchesToApply;
363454
+ const detectorWarnings = prep._prepared.detectorWarnings;
363455
+ const genSha = prep._prepared.genSha;
363456
+ let results = [];
363457
+ if (newPatches.length > 0) {
363458
+ results = await this.service.applicator.applyPatches(newPatches);
363459
+ this.service._lastApplyResults = results;
363460
+ this.service.recordDroppedContextLineWarnings(results);
363461
+ this.service.revertConflictingFiles(results);
363462
+ for (const result of results) {
363463
+ if (result.status === "conflict") {
363464
+ result.patch.status = "unresolved";
363465
+ }
363466
+ }
363467
+ }
363468
+ const rebaseCounts = await this.service.rebasePatches(results, genSha);
363469
+ for (const patch5 of newPatches) {
363470
+ if (!rebaseCounts.absorbedPatchIds.has(patch5.id)) {
363471
+ this.service.lockManager.addPatch(patch5);
363472
+ }
363473
+ }
363474
+ this.service.lockManager.save();
363475
+ this.service.warnings.push(...this.service.lockManager.warnings);
363476
+ if (newPatches.length > 0) {
363477
+ if (!options2?.stageOnly) {
363478
+ const appliedCount = results.filter((r5) => r5.status === "applied").length;
363479
+ const buckets = computeCommitBuckets(results, rebaseCounts.absorbedPatchIds, newPatches);
363480
+ await this.service.committer.commitReplay(appliedCount, newPatches, void 0, { buckets });
363481
+ } else {
363482
+ await this.service.committer.stageAll();
363483
+ }
363484
+ }
363485
+ const warnings = [...detectorWarnings, ...this.service.warnings];
363486
+ return this.service.buildReport(
363487
+ "no-patches",
363488
+ newPatches,
363489
+ results,
363490
+ prep._prepared.optionsSnapshot,
363491
+ warnings,
363492
+ rebaseCounts,
363493
+ void 0,
363494
+ prep._prepared.revertedCount
363495
+ );
363496
+ }
363497
+ };
363498
+ var NormalRegenerationFlow = class {
363499
+ constructor(service) {
363500
+ this.service = service;
363501
+ }
363502
+ async prepare(options2) {
363503
+ const preLock = this.service.lockManager.read();
363504
+ const previousGenerationSha = preLock.current_generation ?? null;
363505
+ if (options2?.dryRun) {
363506
+ const existingPatches2 = this.service.lockManager.getPatches();
363507
+ const { patches: newPatches2, revertedPatchIds: dryRunReverted } = await this.service.detector.detectNewPatches();
363508
+ const warnings = [...this.service.detector.warnings, ...this.service.warnings];
363509
+ const allPatches2 = [...existingPatches2, ...newPatches2];
363510
+ const preparedReport = {
363511
+ flow: "normal-regeneration",
363512
+ patchesDetected: allPatches2.length,
363513
+ patchesApplied: 0,
363514
+ patchesWithConflicts: 0,
363515
+ patchesSkipped: 0,
363516
+ patchesReverted: dryRunReverted.length,
363517
+ conflicts: [],
363518
+ wouldApply: allPatches2,
363519
+ warnings: warnings.length > 0 ? warnings : void 0
363520
+ };
363521
+ const headSha = await this.service.git.exec(["rev-parse", "HEAD"]).then((s9) => s9.trim()).catch(() => "");
363522
+ return {
363523
+ flow: "normal-regeneration",
363524
+ previousGenerationSha,
363525
+ currentGenerationSha: headSha,
363526
+ baseBranchHead: options2.baseBranchHead ?? null,
363527
+ _prepared: {
363528
+ kind: "terminal",
363529
+ flow: "normal-regeneration",
363530
+ preparedReport
363531
+ }
363532
+ };
363533
+ }
363534
+ let existingPatches = this.service.lockManager.getPatches();
363535
+ const preRebasePatchIds = new Set(existingPatches.map((p14) => p14.id));
363536
+ const preRebaseCounts = await this.service.preGenerationRebase(existingPatches);
363537
+ const postRebasePatchIds = new Set(this.service.lockManager.getPatches().map((p14) => p14.id));
363538
+ const removedByPreRebase = existingPatches.filter(
363539
+ (p14) => preRebasePatchIds.has(p14.id) && !postRebasePatchIds.has(p14.id)
363540
+ );
363541
+ existingPatches = this.service.lockManager.getPatches();
363542
+ const seenHashes = /* @__PURE__ */ new Map();
363543
+ for (const p14 of existingPatches) {
363544
+ const priorPatchId = seenHashes.get(p14.content_hash);
363545
+ if (priorPatchId !== void 0) {
363546
+ this.service.lockManager.removePatch(p14.id);
363547
+ this.service.warnings.push(
363548
+ `Consolidated patch ${p14.id} (commit ${(p14.original_commit || "<missing>").slice(0, 7)}) into prior patch ${priorPatchId}: byte-identical patch_content. Per-commit attribution preserved in git log; lockfile collapsed to avoid duplicate-apply corruption on next regen.`
363549
+ );
363550
+ } else {
363551
+ seenHashes.set(p14.content_hash, p14.id);
363552
+ }
363553
+ }
363554
+ existingPatches = this.service.lockManager.getPatches();
363555
+ let { patches: newPatches, revertedPatchIds } = await this.service.detector.detectNewPatches();
363556
+ const detectorWarnings = [...this.service.detector.warnings];
363557
+ if (removedByPreRebase.length > 0) {
363558
+ const removedOriginalCommits = new Set(removedByPreRebase.map((p14) => p14.original_commit));
363559
+ const removedOriginalMessages = new Set(removedByPreRebase.map((p14) => p14.original_message));
363560
+ newPatches = newPatches.filter((p14) => {
363561
+ if (removedOriginalCommits.has(p14.original_commit)) return false;
363562
+ if (isRevertCommit(p14.original_message)) {
363563
+ const revertedMsg = parseRevertedMessage(p14.original_message);
363564
+ if (revertedMsg && removedOriginalMessages.has(revertedMsg)) return false;
363565
+ }
363566
+ return true;
363567
+ });
363568
+ }
363569
+ for (const id2 of revertedPatchIds) {
363570
+ try {
363571
+ this.service.lockManager.removePatch(id2);
363572
+ } catch {
363573
+ }
363574
+ }
363575
+ const revertedSet = new Set(revertedPatchIds);
363576
+ existingPatches = existingPatches.filter((p14) => !revertedSet.has(p14.id));
363577
+ const allPatches = [...existingPatches, ...newPatches];
363578
+ const commitOpts = options2 ? {
363579
+ cliVersion: options2.cliVersion ?? "unknown",
363580
+ generatorVersions: options2.generatorVersions ?? {},
363581
+ baseBranchHead: options2.baseBranchHead
363582
+ } : void 0;
363583
+ await this.service.committer.commitGeneration("Update SDK", commitOpts);
363584
+ await this.service.cleanupStaleConflictMarkers();
363585
+ const genRecord = await this.service.committer.createGenerationRecord(commitOpts);
363586
+ this.service.lockManager.addGeneration(genRecord);
363587
+ return {
363588
+ flow: "normal-regeneration",
363589
+ previousGenerationSha,
363590
+ currentGenerationSha: genRecord.commit_sha,
363591
+ baseBranchHead: options2?.baseBranchHead ?? null,
363592
+ _prepared: {
363593
+ kind: "active",
363594
+ flow: "normal-regeneration",
363595
+ patchesToApply: allPatches,
363596
+ newPatchIds: new Set(newPatches.map((p14) => p14.id)),
363597
+ preRebaseCounts,
363598
+ detectorWarnings,
363599
+ revertedCount: revertedPatchIds.length,
363600
+ genSha: genRecord.commit_sha,
363601
+ optionsSnapshot: options2
363602
+ }
363603
+ };
363604
+ }
363605
+ async apply(prep, options2) {
363606
+ assertActive(prep);
363607
+ const allPatches = prep._prepared.patchesToApply;
363608
+ const newPatchIds = prep._prepared.newPatchIds;
363609
+ const detectorWarnings = prep._prepared.detectorWarnings;
363610
+ const genSha = prep._prepared.genSha;
363611
+ const results = await this.service.applicator.applyPatches(allPatches);
363612
+ this.service._lastApplyResults = results;
363613
+ this.service.recordDroppedContextLineWarnings(results);
363614
+ this.service.revertConflictingFiles(results);
363615
+ for (const result of results) {
363616
+ if (result.status === "conflict") {
363617
+ result.patch.status = "unresolved";
363618
+ }
363619
+ }
363620
+ const rebaseCounts = await this.service.rebasePatches(results, genSha);
363621
+ const newPatches = allPatches.filter((p14) => newPatchIds.has(p14.id));
363622
+ for (const patch5 of newPatches) {
363623
+ if (!rebaseCounts.absorbedPatchIds.has(patch5.id)) {
363624
+ this.service.lockManager.addPatch(patch5);
363625
+ }
363626
+ }
363627
+ for (const result of results) {
363628
+ if (result.status === "conflict") {
363629
+ try {
363630
+ this.service.lockManager.markPatchUnresolved(result.patch.id);
363631
+ } catch {
363632
+ }
363633
+ }
363634
+ }
363635
+ this.service.lockManager.save();
363636
+ this.service.warnings.push(...this.service.lockManager.warnings);
363637
+ if (options2?.stageOnly) {
363638
+ await this.service.committer.stageAll();
363639
+ } else {
363640
+ const appliedCount = results.filter((r5) => r5.status === "applied").length;
363641
+ const buckets = computeCommitBuckets(results, rebaseCounts.absorbedPatchIds, allPatches);
363642
+ await this.service.committer.commitReplay(appliedCount, allPatches, void 0, { buckets });
363643
+ }
363644
+ const warnings = [...detectorWarnings, ...this.service.warnings];
363645
+ return this.service.buildReport(
363646
+ "normal-regeneration",
363647
+ allPatches,
363648
+ results,
363649
+ prep._prepared.optionsSnapshot,
363650
+ warnings,
363651
+ rebaseCounts,
363652
+ prep._prepared.preRebaseCounts,
363653
+ prep._prepared.revertedCount
363654
+ );
363655
+ }
363656
+ };
363657
+ function assertActive(prep) {
363658
+ if (prep._prepared.kind !== "active") {
363659
+ throw new Error(
363660
+ `Flow.apply() called with terminal preparation (kind=${prep._prepared.kind}); terminal cases must be handled by applyPreparedReplay's dispatcher`
363661
+ );
363662
+ }
363663
+ }
363067
363664
  var ReplayService = class {
363665
+ /** @internal Used by Flow implementations in `src/flows/`. */
363068
363666
  git;
363667
+ /** @internal Used by Flow implementations in `src/flows/`. */
363069
363668
  detector;
363669
+ /** @internal Used by Flow implementations in `src/flows/`. */
363070
363670
  applicator;
363671
+ /** @internal Used by Flow implementations in `src/flows/`. */
363071
363672
  committer;
363673
+ /** @internal Used by Flow implementations in `src/flows/`. */
363072
363674
  lockManager;
363675
+ /** @internal Used by Flow implementations in `src/flows/`. */
363676
+ fileOwnership;
363677
+ /** @internal Used by Flow implementations in `src/flows/`. */
363073
363678
  outputDir;
363074
363679
  /** @internal Test-only. Captures the last ReplayResult[] returned from applicator.applyPatches(). */
363075
363680
  _lastApplyResults = [];
363076
363681
  /**
363077
363682
  * Service-level warnings accumulated during a single runReplay() call.
363078
363683
  * Merged with `detector.warnings` into `ReplayReport.warnings` by each flow handler.
363079
- * Populated by `isFileUserOwned` when a patch's base generation tree is unreachable
363080
- * (shallow clone / aggressive `git gc --prune`) and we fall back to a conservative
363081
- * heuristic. Cleared at the top of `runReplay()`.
363684
+ * Populated by `FileOwnership.resolveCanonical` when a patch's base generation tree
363685
+ * is unreachable (shallow clone / aggressive `git gc --prune`) and we fall back to a
363686
+ * conservative heuristic. Cleared at the top of `runReplay()`.
363687
+ *
363688
+ * @internal Used by Flow implementations in `src/flows/`.
363082
363689
  */
363083
363690
  warnings = [];
363084
363691
  /**
363085
363692
  * @internal Test-only accessor.
363086
363693
  * Returns the ReplayApplicator instance used for the last run. Tests use this
363087
- * to inspect the inter-patch accumulator after runReplay() completes (FER-9791, I2).
363694
+ * to inspect the inter-patch accumulator after runReplay() completes (invariant I2).
363088
363695
  */
363089
363696
  get applicatorRef() {
363090
363697
  return this.applicator;
@@ -363092,8 +363699,8 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
363092
363699
  /**
363093
363700
  * @internal Test-only accessor.
363094
363701
  * Returns the ReplayResult[] from the most recent applyPatches() call. Tests use
363095
- * this to filter applied-vs-conflicted-vs-absorbed patches for invariant assertions
363096
- * (FER-9791). Returns an empty array if no patches have been applied yet.
363702
+ * this to filter applied-vs-conflicted-vs-absorbed patches for invariant
363703
+ * assertions. Returns an empty array if no patches have been applied yet.
363097
363704
  */
363098
363705
  getLastResults() {
363099
363706
  return this._lastApplyResults;
@@ -363106,6 +363713,7 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
363106
363713
  this.detector = new ReplayDetector(git, this.lockManager, outputDir);
363107
363714
  this.applicator = new ReplayApplicator(git, this.lockManager, outputDir);
363108
363715
  this.committer = new ReplayCommitter(git, outputDir);
363716
+ this.fileOwnership = new FileOwnership(git);
363109
363717
  }
363110
363718
  /**
363111
363719
  * Phase 1 of the two-phase replay flow. Runs preGenerationRebase (when applicable),
@@ -363122,18 +363730,7 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
363122
363730
  async prepareReplay(options2) {
363123
363731
  this.warnings = [];
363124
363732
  this.detector.warnings.length = 0;
363125
- if (options2?.skipApplication) {
363126
- return this._prepareSkipApplication(options2);
363127
- }
363128
- const flow4 = this.determineFlow();
363129
- switch (flow4) {
363130
- case "first-generation":
363131
- return this._prepareFirstGeneration(options2);
363132
- case "no-patches":
363133
- return this._prepareNoPatchesRegeneration(options2);
363134
- case "normal-regeneration":
363135
- return this._prepareNormalRegeneration(options2);
363136
- }
363733
+ return this.selectFlow(options2).prepare(options2);
363137
363734
  }
363138
363735
  /**
363139
363736
  * Phase 2 of the two-phase replay flow. Applies patches, rebases them against
@@ -363145,18 +363742,33 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
363145
363742
  * its own post-commit logic), this returns the precomputed report.
363146
363743
  */
363147
363744
  async applyPreparedReplay(prep, options2) {
363148
- if (prep._prepared.terminal) {
363745
+ if (prep._prepared.kind === "terminal") {
363149
363746
  return prep._prepared.preparedReport;
363150
363747
  }
363151
- switch (prep._prepared.flow) {
363748
+ return this.flowForKind(prep._prepared.flow).apply(prep, options2);
363749
+ }
363750
+ /**
363751
+ * Construct the flow for the current run. `selectFlow` is called from
363752
+ * `prepareReplay` and decides based on options + lockfile state.
363753
+ */
363754
+ selectFlow(options2) {
363755
+ if (options2?.skipApplication) return new SkipApplicationFlow(this);
363756
+ return this.flowForKind(this.determineFlow());
363757
+ }
363758
+ /**
363759
+ * Construct a flow given its kind. Used by `applyPreparedReplay` to
363760
+ * route to the same kind that was selected in phase 1.
363761
+ */
363762
+ flowForKind(kind) {
363763
+ switch (kind) {
363152
363764
  case "first-generation":
363153
- return this._applyFirstGeneration(prep);
363154
- case "skip-application":
363155
- return this._applySkipApplication(prep, options2);
363765
+ return new FirstGenerationFlow(this);
363156
363766
  case "no-patches":
363157
- return this._applyNoPatchesRegeneration(prep, options2);
363767
+ return new NoPatchesFlow(this);
363158
363768
  case "normal-regeneration":
363159
- return this._applyNormalRegeneration(prep, options2);
363769
+ return new NormalRegenerationFlow(this);
363770
+ case "skip-application":
363771
+ return new SkipApplicationFlow(this);
363160
363772
  }
363161
363773
  }
363162
363774
  /**
@@ -363240,423 +363852,12 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
363240
363852
  throw error50;
363241
363853
  }
363242
363854
  }
363243
- firstGenerationReport() {
363244
- return {
363245
- flow: "first-generation",
363246
- patchesDetected: 0,
363247
- patchesApplied: 0,
363248
- patchesWithConflicts: 0,
363249
- patchesSkipped: 0,
363250
- conflicts: []
363251
- };
363252
- }
363253
- async _prepareFirstGeneration(options2) {
363254
- if (options2?.dryRun) {
363255
- const headSha = await this.git.exec(["rev-parse", "HEAD"]).then((s9) => s9.trim()).catch(() => "");
363256
- return {
363257
- flow: "first-generation",
363258
- previousGenerationSha: null,
363259
- currentGenerationSha: headSha,
363260
- baseBranchHead: options2.baseBranchHead ?? null,
363261
- _prepared: {
363262
- flow: "first-generation",
363263
- terminal: true,
363264
- preparedReport: this.firstGenerationReport(),
363265
- patchesToApply: [],
363266
- newPatchIds: /* @__PURE__ */ new Set(),
363267
- detectorWarnings: [],
363268
- revertedCount: 0,
363269
- genSha: headSha,
363270
- optionsSnapshot: options2
363271
- }
363272
- };
363273
- }
363274
- const commitOpts = options2 ? {
363275
- cliVersion: options2.cliVersion ?? "unknown",
363276
- generatorVersions: options2.generatorVersions ?? {},
363277
- baseBranchHead: options2.baseBranchHead
363278
- } : void 0;
363279
- await this.committer.commitGeneration("Initial SDK generation", commitOpts);
363280
- const genRecord = await this.committer.createGenerationRecord(commitOpts);
363281
- this.lockManager.initializeInMemory(genRecord);
363282
- return {
363283
- flow: "first-generation",
363284
- previousGenerationSha: null,
363285
- currentGenerationSha: genRecord.commit_sha,
363286
- baseBranchHead: options2?.baseBranchHead ?? null,
363287
- _prepared: {
363288
- flow: "first-generation",
363289
- terminal: false,
363290
- patchesToApply: [],
363291
- newPatchIds: /* @__PURE__ */ new Set(),
363292
- detectorWarnings: [],
363293
- revertedCount: 0,
363294
- genSha: genRecord.commit_sha,
363295
- optionsSnapshot: options2
363296
- }
363297
- };
363298
- }
363299
- async _applyFirstGeneration(_prep) {
363300
- this.lockManager.save();
363301
- return this.firstGenerationReport();
363302
- }
363303
- /**
363304
- * Skip-application mode phase 1: commit the generation and update the
363305
- * in-memory lockfile, but don't detect or apply patches. Sets a marker
363306
- * so the next normal run skips revert detection in preGenerationRebase().
363307
- *
363308
- * Disk save is deferred to `_applySkipApplication`.
363309
- */
363310
- async _prepareSkipApplication(options2) {
363311
- const commitOpts = options2 ? {
363312
- cliVersion: options2.cliVersion ?? "unknown",
363313
- generatorVersions: options2.generatorVersions ?? {},
363314
- baseBranchHead: options2.baseBranchHead
363315
- } : void 0;
363316
- await this.committer.commitGeneration("Update SDK (replay skipped)", commitOpts);
363317
- await this.cleanupStaleConflictMarkers();
363318
- const genRecord = await this.committer.createGenerationRecord(commitOpts);
363319
- let previousGenerationSha = null;
363320
- try {
363321
- const lock = this.lockManager.read();
363322
- previousGenerationSha = lock.current_generation ?? null;
363323
- this.lockManager.addGeneration(genRecord);
363324
- } catch (error50) {
363325
- if (error50 instanceof LockfileNotFoundError) {
363326
- this.lockManager.initializeInMemory(genRecord);
363327
- } else {
363328
- throw error50;
363329
- }
363330
- }
363331
- this.lockManager.setReplaySkippedAt((/* @__PURE__ */ new Date()).toISOString());
363332
- return {
363333
- flow: "skip-application",
363334
- previousGenerationSha,
363335
- currentGenerationSha: genRecord.commit_sha,
363336
- baseBranchHead: options2?.baseBranchHead ?? null,
363337
- _prepared: {
363338
- flow: "skip-application",
363339
- terminal: false,
363340
- patchesToApply: [],
363341
- newPatchIds: /* @__PURE__ */ new Set(),
363342
- detectorWarnings: [],
363343
- revertedCount: 0,
363344
- genSha: genRecord.commit_sha,
363345
- optionsSnapshot: options2
363346
- }
363347
- };
363348
- }
363349
- async _applySkipApplication(_prep, options2) {
363350
- this.lockManager.save();
363351
- const credentialWarnings = [...this.lockManager.warnings];
363352
- if (!options2?.stageOnly) {
363353
- await this.committer.commitReplay(0);
363354
- } else {
363355
- await this.committer.stageAll();
363356
- }
363357
- return {
363358
- flow: "skip-application",
363359
- patchesDetected: 0,
363360
- patchesApplied: 0,
363361
- patchesWithConflicts: 0,
363362
- patchesSkipped: 0,
363363
- conflicts: [],
363364
- warnings: credentialWarnings.length > 0 ? credentialWarnings : void 0
363365
- };
363366
- }
363367
- async _prepareNoPatchesRegeneration(options2) {
363368
- const preLock = this.lockManager.read();
363369
- const previousGenerationSha = preLock.current_generation ?? null;
363370
- let { patches: newPatches, revertedPatchIds } = await this.detector.detectNewPatches();
363371
- const detectorWarnings = [...this.detector.warnings];
363372
- if (options2?.dryRun) {
363373
- const dryRunWarnings = [...detectorWarnings, ...this.warnings];
363374
- const preparedReport = {
363375
- flow: "no-patches",
363376
- patchesDetected: newPatches.length,
363377
- patchesApplied: 0,
363378
- patchesWithConflicts: 0,
363379
- patchesSkipped: 0,
363380
- patchesReverted: revertedPatchIds.length,
363381
- conflicts: [],
363382
- wouldApply: newPatches,
363383
- warnings: dryRunWarnings.length > 0 ? dryRunWarnings : void 0
363384
- };
363385
- const headSha = await this.git.exec(["rev-parse", "HEAD"]).then((s9) => s9.trim()).catch(() => "");
363386
- return {
363387
- flow: "no-patches",
363388
- previousGenerationSha,
363389
- currentGenerationSha: headSha,
363390
- baseBranchHead: options2.baseBranchHead ?? null,
363391
- _prepared: {
363392
- flow: "no-patches",
363393
- terminal: true,
363394
- preparedReport,
363395
- patchesToApply: [],
363396
- newPatchIds: /* @__PURE__ */ new Set(),
363397
- detectorWarnings,
363398
- revertedCount: revertedPatchIds.length,
363399
- genSha: headSha,
363400
- optionsSnapshot: options2
363401
- }
363402
- };
363403
- }
363404
- {
363405
- const preCurrentGen = preLock.current_generation;
363406
- const uniqueFiles = [...new Set(newPatches.flatMap((p14) => p14.files))];
363407
- const absorbedFiles = /* @__PURE__ */ new Set();
363408
- for (const file4 of uniqueFiles) {
363409
- const diff2 = await this.git.exec(["diff", preCurrentGen, "HEAD", "--", file4]).catch(() => null);
363410
- if (diff2 !== null && !diff2.trim()) {
363411
- absorbedFiles.add(file4);
363412
- }
363413
- }
363414
- if (absorbedFiles.size > 0) {
363415
- newPatches = newPatches.filter(
363416
- (p14) => p14.files.length === 0 || !p14.files.every((f4) => absorbedFiles.has(f4))
363417
- );
363418
- }
363419
- }
363420
- for (const id2 of revertedPatchIds) {
363421
- try {
363422
- this.lockManager.removePatch(id2);
363423
- } catch {
363424
- }
363425
- }
363426
- const commitOpts = options2 ? {
363427
- cliVersion: options2.cliVersion ?? "unknown",
363428
- generatorVersions: options2.generatorVersions ?? {},
363429
- baseBranchHead: options2.baseBranchHead
363430
- } : void 0;
363431
- await this.committer.commitGeneration("Update SDK", commitOpts);
363432
- await this.cleanupStaleConflictMarkers();
363433
- const genRecord = await this.committer.createGenerationRecord(commitOpts);
363434
- this.lockManager.addGeneration(genRecord);
363435
- return {
363436
- flow: "no-patches",
363437
- previousGenerationSha,
363438
- currentGenerationSha: genRecord.commit_sha,
363439
- baseBranchHead: options2?.baseBranchHead ?? null,
363440
- _prepared: {
363441
- flow: "no-patches",
363442
- terminal: false,
363443
- patchesToApply: newPatches,
363444
- newPatchIds: new Set(newPatches.map((p14) => p14.id)),
363445
- detectorWarnings,
363446
- revertedCount: revertedPatchIds.length,
363447
- genSha: genRecord.commit_sha,
363448
- optionsSnapshot: options2
363449
- }
363450
- };
363451
- }
363452
- async _applyNoPatchesRegeneration(prep, options2) {
363453
- const newPatches = prep._prepared.patchesToApply;
363454
- const detectorWarnings = prep._prepared.detectorWarnings;
363455
- const genSha = prep._prepared.genSha;
363456
- let results = [];
363457
- if (newPatches.length > 0) {
363458
- results = await this.applicator.applyPatches(newPatches);
363459
- this._lastApplyResults = results;
363460
- this.recordDroppedContextLineWarnings(results);
363461
- this.revertConflictingFiles(results);
363462
- for (const result of results) {
363463
- if (result.status === "conflict") {
363464
- result.patch.status = "unresolved";
363465
- }
363466
- }
363467
- }
363468
- const rebaseCounts = await this.rebasePatches(results, genSha);
363469
- for (const patch5 of newPatches) {
363470
- if (!rebaseCounts.absorbedPatchIds.has(patch5.id)) {
363471
- this.lockManager.addPatch(patch5);
363472
- }
363473
- }
363474
- this.lockManager.save();
363475
- this.warnings.push(...this.lockManager.warnings);
363476
- if (newPatches.length > 0) {
363477
- if (!options2?.stageOnly) {
363478
- const appliedCount = results.filter((r5) => r5.status === "applied").length;
363479
- const buckets = computeCommitBuckets(results, rebaseCounts.absorbedPatchIds, newPatches);
363480
- await this.committer.commitReplay(appliedCount, newPatches, void 0, { buckets });
363481
- } else {
363482
- await this.committer.stageAll();
363483
- }
363484
- }
363485
- const warnings = [...detectorWarnings, ...this.warnings];
363486
- return this.buildReport(
363487
- "no-patches",
363488
- newPatches,
363489
- results,
363490
- prep._prepared.optionsSnapshot,
363491
- warnings,
363492
- rebaseCounts,
363493
- void 0,
363494
- prep._prepared.revertedCount
363495
- );
363496
- }
363497
- async _prepareNormalRegeneration(options2) {
363498
- const preLock = this.lockManager.read();
363499
- const previousGenerationSha = preLock.current_generation ?? null;
363500
- if (options2?.dryRun) {
363501
- const existingPatches2 = this.lockManager.getPatches();
363502
- const { patches: newPatches2, revertedPatchIds: dryRunReverted } = await this.detector.detectNewPatches();
363503
- const warnings = [...this.detector.warnings, ...this.warnings];
363504
- const allPatches2 = [...existingPatches2, ...newPatches2];
363505
- const preparedReport = {
363506
- flow: "normal-regeneration",
363507
- patchesDetected: allPatches2.length,
363508
- patchesApplied: 0,
363509
- patchesWithConflicts: 0,
363510
- patchesSkipped: 0,
363511
- patchesReverted: dryRunReverted.length,
363512
- conflicts: [],
363513
- wouldApply: allPatches2,
363514
- warnings: warnings.length > 0 ? warnings : void 0
363515
- };
363516
- const headSha = await this.git.exec(["rev-parse", "HEAD"]).then((s9) => s9.trim()).catch(() => "");
363517
- return {
363518
- flow: "normal-regeneration",
363519
- previousGenerationSha,
363520
- currentGenerationSha: headSha,
363521
- baseBranchHead: options2.baseBranchHead ?? null,
363522
- _prepared: {
363523
- flow: "normal-regeneration",
363524
- terminal: true,
363525
- preparedReport,
363526
- patchesToApply: [],
363527
- newPatchIds: /* @__PURE__ */ new Set(),
363528
- detectorWarnings: [...this.detector.warnings],
363529
- revertedCount: dryRunReverted.length,
363530
- genSha: headSha,
363531
- optionsSnapshot: options2
363532
- }
363533
- };
363534
- }
363535
- let existingPatches = this.lockManager.getPatches();
363536
- const preRebasePatchIds = new Set(existingPatches.map((p14) => p14.id));
363537
- const preRebaseCounts = await this.preGenerationRebase(existingPatches);
363538
- const postRebasePatchIds = new Set(this.lockManager.getPatches().map((p14) => p14.id));
363539
- const removedByPreRebase = existingPatches.filter(
363540
- (p14) => preRebasePatchIds.has(p14.id) && !postRebasePatchIds.has(p14.id)
363541
- );
363542
- existingPatches = this.lockManager.getPatches();
363543
- const seenHashes = /* @__PURE__ */ new Map();
363544
- for (const p14 of existingPatches) {
363545
- const priorPatchId = seenHashes.get(p14.content_hash);
363546
- if (priorPatchId !== void 0) {
363547
- this.lockManager.removePatch(p14.id);
363548
- this.warnings.push(
363549
- `Consolidated patch ${p14.id} (commit ${(p14.original_commit || "<missing>").slice(0, 7)}) into prior patch ${priorPatchId}: byte-identical patch_content. Per-commit attribution preserved in git log; lockfile collapsed to avoid duplicate-apply corruption on next regen.`
363550
- );
363551
- } else {
363552
- seenHashes.set(p14.content_hash, p14.id);
363553
- }
363554
- }
363555
- existingPatches = this.lockManager.getPatches();
363556
- let { patches: newPatches, revertedPatchIds } = await this.detector.detectNewPatches();
363557
- const detectorWarnings = [...this.detector.warnings];
363558
- if (removedByPreRebase.length > 0) {
363559
- const removedOriginalCommits = new Set(removedByPreRebase.map((p14) => p14.original_commit));
363560
- const removedOriginalMessages = new Set(removedByPreRebase.map((p14) => p14.original_message));
363561
- newPatches = newPatches.filter((p14) => {
363562
- if (removedOriginalCommits.has(p14.original_commit)) return false;
363563
- if (isRevertCommit(p14.original_message)) {
363564
- const revertedMsg = parseRevertedMessage(p14.original_message);
363565
- if (revertedMsg && removedOriginalMessages.has(revertedMsg)) return false;
363566
- }
363567
- return true;
363568
- });
363569
- }
363570
- for (const id2 of revertedPatchIds) {
363571
- try {
363572
- this.lockManager.removePatch(id2);
363573
- } catch {
363574
- }
363575
- }
363576
- const revertedSet = new Set(revertedPatchIds);
363577
- existingPatches = existingPatches.filter((p14) => !revertedSet.has(p14.id));
363578
- const allPatches = [...existingPatches, ...newPatches];
363579
- const commitOpts = options2 ? {
363580
- cliVersion: options2.cliVersion ?? "unknown",
363581
- generatorVersions: options2.generatorVersions ?? {},
363582
- baseBranchHead: options2.baseBranchHead
363583
- } : void 0;
363584
- await this.committer.commitGeneration("Update SDK", commitOpts);
363585
- await this.cleanupStaleConflictMarkers();
363586
- const genRecord = await this.committer.createGenerationRecord(commitOpts);
363587
- this.lockManager.addGeneration(genRecord);
363588
- return {
363589
- flow: "normal-regeneration",
363590
- previousGenerationSha,
363591
- currentGenerationSha: genRecord.commit_sha,
363592
- baseBranchHead: options2?.baseBranchHead ?? null,
363593
- _prepared: {
363594
- flow: "normal-regeneration",
363595
- terminal: false,
363596
- patchesToApply: allPatches,
363597
- newPatchIds: new Set(newPatches.map((p14) => p14.id)),
363598
- preRebaseCounts,
363599
- detectorWarnings,
363600
- revertedCount: revertedPatchIds.length,
363601
- genSha: genRecord.commit_sha,
363602
- optionsSnapshot: options2
363603
- }
363604
- };
363605
- }
363606
- async _applyNormalRegeneration(prep, options2) {
363607
- const allPatches = prep._prepared.patchesToApply;
363608
- const newPatchIds = prep._prepared.newPatchIds;
363609
- const detectorWarnings = prep._prepared.detectorWarnings;
363610
- const genSha = prep._prepared.genSha;
363611
- const results = await this.applicator.applyPatches(allPatches);
363612
- this._lastApplyResults = results;
363613
- this.recordDroppedContextLineWarnings(results);
363614
- this.revertConflictingFiles(results);
363615
- for (const result of results) {
363616
- if (result.status === "conflict") {
363617
- result.patch.status = "unresolved";
363618
- }
363619
- }
363620
- const rebaseCounts = await this.rebasePatches(results, genSha);
363621
- const newPatches = allPatches.filter((p14) => newPatchIds.has(p14.id));
363622
- for (const patch5 of newPatches) {
363623
- if (!rebaseCounts.absorbedPatchIds.has(patch5.id)) {
363624
- this.lockManager.addPatch(patch5);
363625
- }
363626
- }
363627
- for (const result of results) {
363628
- if (result.status === "conflict") {
363629
- try {
363630
- this.lockManager.markPatchUnresolved(result.patch.id);
363631
- } catch {
363632
- }
363633
- }
363634
- }
363635
- this.lockManager.save();
363636
- this.warnings.push(...this.lockManager.warnings);
363637
- if (options2?.stageOnly) {
363638
- await this.committer.stageAll();
363639
- } else {
363640
- const appliedCount = results.filter((r5) => r5.status === "applied").length;
363641
- const buckets = computeCommitBuckets(results, rebaseCounts.absorbedPatchIds, allPatches);
363642
- await this.committer.commitReplay(appliedCount, allPatches, void 0, { buckets });
363643
- }
363644
- const warnings = [...detectorWarnings, ...this.warnings];
363645
- return this.buildReport(
363646
- "normal-regeneration",
363647
- allPatches,
363648
- results,
363649
- prep._prepared.optionsSnapshot,
363650
- warnings,
363651
- rebaseCounts,
363652
- prep._prepared.preRebaseCounts,
363653
- prep._prepared.revertedCount
363654
- );
363655
- }
363656
363855
  /**
363657
363856
  * Rebase cleanly applied patches so they are relative to the current generation.
363658
363857
  * This prevents recurring conflicts on subsequent regenerations.
363659
363858
  * Returns the number of patches rebased.
363859
+ *
363860
+ * @internal Used by Flow implementations in `src/flows/`.
363660
363861
  */
363661
363862
  async rebasePatches(results, currentGenSha) {
363662
363863
  let absorbed = 0;
@@ -363666,6 +363867,7 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
363666
363867
  const seenContentHashes = /* @__PURE__ */ new Map();
363667
363868
  const absorbedPatchIds = /* @__PURE__ */ new Set();
363668
363869
  const fernignorePatterns = this.readFernignorePatterns();
363870
+ const warnedGens = /* @__PURE__ */ new Set();
363669
363871
  for (const result of results) {
363670
363872
  if (result.resolvedFiles && Object.keys(result.resolvedFiles).length > 0) {
363671
363873
  const patch5 = result.patch;
@@ -363710,21 +363912,20 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
363710
363912
  }
363711
363913
  }
363712
363914
  const isUserOwnedPerFile = await Promise.all(
363713
- patch5.files.map(async (file4) => {
363714
- if (file4 === ".fernignore") return true;
363715
- if (fernignorePatterns.some((p14) => file4 === p14 || (0, import_minimatch22.minimatch)(file4, p14))) {
363716
- return true;
363717
- }
363718
- if (patch5.user_owned) return true;
363719
- if (this.fileIsCreationInPatchContent(patch5.patch_content, originalByResolved[file4] ?? file4)) {
363720
- return true;
363721
- }
363722
- const content5 = await this.git.showFile(currentGenSha, file4);
363723
- return content5 === null;
363724
- })
363915
+ patch5.files.map(
363916
+ (file4) => this.fileOwnership.resolveCanonical({
363917
+ file: file4,
363918
+ originalFileName: originalByResolved[file4],
363919
+ patch: patch5,
363920
+ currentGenSha,
363921
+ fernignorePatterns,
363922
+ warnedGens,
363923
+ warnings: this.warnings
363924
+ })
363925
+ )
363725
363926
  );
363726
363927
  const hasProtectedFiles = patch5.files.some(
363727
- (file4) => file4 === ".fernignore" || fernignorePatterns.some((p14) => file4 === p14 || (0, import_minimatch22.minimatch)(file4, p14))
363928
+ (file4) => file4 === ".fernignore" || fernignorePatterns.some((p14) => file4 === p14 || (0, import_minimatch32.minimatch)(file4, p14))
363728
363929
  );
363729
363930
  const allUserOwned = isUserOwnedPerFile.every(Boolean);
363730
363931
  if (allUserOwned && patch5.files.length > 0) {
@@ -363848,7 +364049,7 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
363848
364049
  for (const file4 of files) {
363849
364050
  if (isBinaryFile(file4)) continue;
363850
364051
  try {
363851
- const content5 = await (0, import_promises410.readFile)((0, import_node_path52.join)(this.outputDir, file4), "utf-8");
364052
+ const content5 = await (0, import_promises610.readFile)((0, import_node_path82.join)(this.outputDir, file4), "utf-8");
363852
364053
  snapshot[file4] = content5;
363853
364054
  } catch {
363854
364055
  }
@@ -363856,10 +364057,16 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
363856
364057
  return snapshot;
363857
364058
  }
363858
364059
  /**
363859
- * FER-10203: returns true when a `[fern-autoversion]` commit between
363860
- * `fromSha` and `toSha` touched any of the patch's `files`. Re-classifies
363861
- * each grep match via `isGenerationCommit` so a customer commit that
363862
- * merely quotes the marker in its body doesn't false-positive.
364060
+ * Detects autoversion contamination — returns true when a
364061
+ * `[fern-autoversion]` commit between `fromSha` and `toSha` touched any
364062
+ * of the patch's `files`. Re-classifies each grep match via
364063
+ * `isGenerationCommit` so a customer commit that merely quotes the
364064
+ * marker in its body doesn't false-positive.
364065
+ *
364066
+ * Used by preGenerationRebase to skip the customer-content rebuild for
364067
+ * patches whose files were last touched by an autoversion step — a
364068
+ * naive `git diff currentGen HEAD` there would capture the
364069
+ * placeholder→semver swap as customer customization.
363863
364070
  */
363864
364071
  async hasAutoversionContamination(fromSha, toSha, files) {
363865
364072
  if (files.length === 0) return false;
@@ -363888,10 +364095,15 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
363888
364095
  return false;
363889
364096
  }
363890
364097
  /**
363891
- * FER-10203: rebuild patch_content from `theirs_snapshot` (captured at
363892
- * detection time, predates pipeline content) rather than a HEAD-relative
363893
- * diff. Returns `null` when snapshot is missing or doesn't cover every
363894
- * file in `patch.files` — caller leaves the patch unchanged.
364098
+ * Rebuild patch_content from `theirs_snapshot` (captured at detection
364099
+ * time, predates pipeline content) rather than a HEAD-relative diff.
364100
+ * Returns `null` when snapshot is missing or doesn't cover every file
364101
+ * in `patch.files` — caller leaves the patch unchanged.
364102
+ *
364103
+ * Used as the autoversion-contamination escape hatch: when the
364104
+ * HEAD-relative diff would capture pipeline content (autoversion's
364105
+ * placeholder swap) as customer customization, the snapshot path
364106
+ * preserves the customer's actual intent.
363895
364107
  */
363896
364108
  async computeSnapshotBasedPatchDiff(patch5, currentGen) {
363897
364109
  if (!patch5.theirs_snapshot) return null;
@@ -363939,7 +364151,7 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
363939
364151
  * **Skips patches sharing files with other patches.** HEAD's content
363940
364152
  * for a file shared by N patches is cumulative across all of them, so
363941
364153
  * a HEAD-fallback would falsely encode other patches' contributions
363942
- * into one patch's snapshot — breaking FER-9525 cross-patch isolation
364154
+ * into one patch's snapshot — breaking cross-patch isolation
363943
364155
  * if the snapshot fallback later fires. Per-patch snapshots only
363944
364156
  * make sense when the patch owns its file. Multi-patch-same-file
363945
364157
  * legacy lockfiles keep using line-anchored reconstruction (the
@@ -364001,72 +364213,6 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
364001
364213
  );
364002
364214
  }
364003
364215
  }
364004
- /**
364005
- * Determine whether `file` is user-owned (never produced by the generator).
364006
- *
364007
- * Resolution order (first match wins):
364008
- * 1. `patch.user_owned === true` — authoritative, persisted across cycles.
364009
- * 2. `.fernignore` itself, or file matches a .fernignore pattern.
364010
- * 3. Legacy-safe signal: `patch_content` shows this file as a `--- /dev/null`
364011
- * creation. Works for patches that predate the `user_owned` field or
364012
- * whose flag was lost. Immune to rename tricks — relies on raw patch text.
364013
- * 4. Tree-based check against `patch.base_generation` when it is reachable
364014
- * AND distinct from `currentGenSha`. If the base tree is unreachable
364015
- * (shallow clone / aggressive `git gc --prune`), emit a one-time
364016
- * customer-actionable warning (deduplicated via warnedGens) and
364017
- * conservatively treat as user-owned — strictly safer than silent
364018
- * absorption (FER-9809).
364019
- * 5. Final fallback — check `currentGenSha` when there is no usable base
364020
- * (legacy one-gen lockfile). Preserves pre-fix behavior for that edge.
364021
- *
364022
- * `originalFileName` is the pre-rename path when the generator moved the
364023
- * file between the patch's base and the current generation. Tree lookups
364024
- * at ancestor generations use the original path — that's where the
364025
- * generator produced the content.
364026
- */
364027
- async isFileUserOwned(file4, patch5, currentGenSha, fernignorePatterns, warnedGens, originalFileName) {
364028
- if (patch5.user_owned) return true;
364029
- if (file4 === ".fernignore") return true;
364030
- if (fernignorePatterns.some((p14) => file4 === p14 || (0, import_minimatch22.minimatch)(file4, p14))) return true;
364031
- if (this.fileIsCreationInPatchContent(patch5.patch_content, originalFileName ?? file4)) {
364032
- return true;
364033
- }
364034
- const base4 = patch5.base_generation;
364035
- if (base4 && base4 !== currentGenSha) {
364036
- const reachable = await this.git.commitExists(base4) || await this.git.treeExists(base4);
364037
- if (!reachable) {
364038
- if (!warnedGens.has(base4)) {
364039
- warnedGens.add(base4);
364040
- this.warnings.push(
364041
- `Base generation ${base4.slice(0, 7)} is unreachable (shallow clone or pruned history). Treating affected files as user-owned to avoid silent data loss. Run \`git fetch --unshallow\` or avoid \`git gc --prune\` to restore full history.`
364042
- );
364043
- }
364044
- return true;
364045
- }
364046
- return await this.git.showFile(base4, originalFileName ?? file4) === null;
364047
- }
364048
- return await this.git.showFile(currentGenSha, originalFileName ?? file4) === null;
364049
- }
364050
- /**
364051
- * Returns true if `file`'s diff section in `patchContent` starts with
364052
- * `--- /dev/null`, meaning this file was introduced by the patch (user
364053
- * creation). Used as a legacy-safe fallback when `patch.user_owned` is
364054
- * absent or cleared.
364055
- */
364056
- fileIsCreationInPatchContent(patchContent, file4) {
364057
- const lines = patchContent.split("\n");
364058
- let inFileSection = false;
364059
- for (const line of lines) {
364060
- if (line.startsWith("diff --git ")) {
364061
- inFileSection = line.includes(` b/${file4}`) || line.endsWith(` b/${file4}`);
364062
- continue;
364063
- }
364064
- if (inFileSection && line.startsWith("--- ")) {
364065
- return line === "--- /dev/null";
364066
- }
364067
- }
364068
- return false;
364069
- }
364070
364216
  /**
364071
364217
  * For conflict patches with mixed results (some files merged, some conflicted),
364072
364218
  * check if the cleanly merged files were absorbed by the generator (empty diff).
@@ -364085,7 +364231,15 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
364085
364231
  const warnedGens = /* @__PURE__ */ new Set();
364086
364232
  const generatorCleanFiles = [];
364087
364233
  for (const file4 of cleanFiles) {
364088
- if (await this.isFileUserOwned(file4, patch5, currentGenSha, fernignorePatterns, warnedGens)) {
364234
+ const userOwned = await this.fileOwnership.resolveCanonical({
364235
+ file: file4,
364236
+ patch: patch5,
364237
+ currentGenSha,
364238
+ fernignorePatterns,
364239
+ warnedGens,
364240
+ warnings: this.warnings
364241
+ });
364242
+ if (userOwned) {
364089
364243
  continue;
364090
364244
  }
364091
364245
  generatorCleanFiles.push(file4);
@@ -364114,6 +364268,7 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
364114
364268
  * Pre-generation rebase: update patches using the customer's current state.
364115
364269
  * Called BEFORE commitGeneration() while HEAD has customer code.
364116
364270
  */
364271
+ /** @internal Used by Flow implementations in `src/flows/`. */
364117
364272
  async preGenerationRebase(patches) {
364118
364273
  const lock = this.lockManager.read();
364119
364274
  const currentGen = lock.current_generation;
@@ -364152,7 +364307,15 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
364152
364307
  if (patch5.user_owned) return true;
364153
364308
  if (patch5.files.length === 0) return false;
364154
364309
  for (const file4 of patch5.files) {
364155
- if (!await this.isFileUserOwned(file4, patch5, currentGen, fernignorePatterns, warnedGens)) {
364310
+ const userOwned = await this.fileOwnership.resolveCanonical({
364311
+ file: file4,
364312
+ patch: patch5,
364313
+ currentGenSha: currentGen,
364314
+ fernignorePatterns,
364315
+ warnedGens,
364316
+ warnings: this.warnings
364317
+ });
364318
+ if (!userOwned) {
364156
364319
  return false;
364157
364320
  }
364158
364321
  }
@@ -364202,7 +364365,7 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
364202
364365
  );
364203
364366
  if (snapHasStaleMarkers) continue;
364204
364367
  const isProtectedSnap = patch5.files.some(
364205
- (file4) => file4 === ".fernignore" || fernignorePatterns.some((p14) => file4 === p14 || (0, import_minimatch22.minimatch)(file4, p14))
364368
+ (file4) => file4 === ".fernignore" || fernignorePatterns.some((p14) => file4 === p14 || (0, import_minimatch32.minimatch)(file4, p14))
364206
364369
  );
364207
364370
  if (isProtectedSnap) continue;
364208
364371
  const snapHash = this.detector.computeContentHash(snapshotDiff);
@@ -364237,7 +364400,7 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
364237
364400
  continue;
364238
364401
  }
364239
364402
  const isProtected = patch5.files.some(
364240
- (file4) => file4 === ".fernignore" || fernignorePatterns.some((p14) => file4 === p14 || (0, import_minimatch22.minimatch)(file4, p14))
364403
+ (file4) => file4 === ".fernignore" || fernignorePatterns.some((p14) => file4 === p14 || (0, import_minimatch32.minimatch)(file4, p14))
364241
364404
  );
364242
364405
  if (isProtected) {
364243
364406
  continue;
@@ -364344,6 +364507,7 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
364344
364507
  * relied on them. This is the "surface or preserve, never silently drop"
364345
364508
  * contract for legitimate-deletion cases.
364346
364509
  */
364510
+ /** @internal Used by Flow implementations in `src/flows/`. */
364347
364511
  recordDroppedContextLineWarnings(results) {
364348
364512
  const PREVIEW_COUNT = 5;
364349
364513
  for (const result of results) {
@@ -364366,12 +364530,13 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
364366
364530
  * After applyPatches(), strip conflict markers from conflicting files
364367
364531
  * so only clean content is committed. Keeps the Generated (OURS) side.
364368
364532
  */
364533
+ /** @internal Used by Flow implementations in `src/flows/`. */
364369
364534
  revertConflictingFiles(results) {
364370
364535
  for (const result of results) {
364371
364536
  if (result.status !== "conflict" || !result.fileResults) continue;
364372
364537
  for (const fileResult of result.fileResults) {
364373
364538
  if (fileResult.status !== "conflict") continue;
364374
- const filePath = (0, import_node_path52.join)(this.outputDir, fileResult.file);
364539
+ const filePath = (0, import_node_path82.join)(this.outputDir, fileResult.file);
364375
364540
  try {
364376
364541
  const content5 = (0, import_node_fs210.readFileSync)(filePath, "utf-8");
364377
364542
  const stripped = stripConflictMarkers(content5);
@@ -364387,13 +364552,14 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
364387
364552
  * Restores files to their clean generated state from HEAD.
364388
364553
  * Skips .fernignore-protected files to prevent overwriting user content.
364389
364554
  */
364555
+ /** @internal Used by Flow implementations in `src/flows/`. */
364390
364556
  async cleanupStaleConflictMarkers() {
364391
364557
  const markerFiles = await this.git.exec(["grep", "-l", "<<<<<<< Generated", "--", "."]).catch(() => "");
364392
364558
  const files = markerFiles.trim().split("\n").filter(Boolean);
364393
364559
  if (files.length === 0) return;
364394
364560
  const fernignorePatterns = this.readFernignorePatterns();
364395
364561
  for (const file4 of files) {
364396
- if (fernignorePatterns.some((pattern) => (0, import_minimatch22.minimatch)(file4, pattern))) continue;
364562
+ if (fernignorePatterns.some((pattern) => (0, import_minimatch32.minimatch)(file4, pattern))) continue;
364397
364563
  try {
364398
364564
  await this.git.exec(["checkout", "HEAD", "--", file4]);
364399
364565
  } catch {
@@ -364401,10 +364567,11 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
364401
364567
  }
364402
364568
  }
364403
364569
  readFernignorePatterns() {
364404
- const fernignorePath = (0, import_node_path52.join)(this.outputDir, ".fernignore");
364570
+ const fernignorePath = (0, import_node_path82.join)(this.outputDir, ".fernignore");
364405
364571
  if (!(0, import_node_fs210.existsSync)(fernignorePath)) return [];
364406
364572
  return (0, import_node_fs210.readFileSync)(fernignorePath, "utf-8").split("\n").map((line) => line.trim()).filter((line) => line && !line.startsWith("#"));
364407
364573
  }
364574
+ /** @internal Used by Flow implementations in `src/flows/`. */
364408
364575
  buildReport(flow4, patches, results, options2, warnings, rebaseCounts, preRebaseCounts, patchesReverted) {
364409
364576
  const conflictResults = results.filter((r5) => r5.status === "conflict");
364410
364577
  const conflictDetails = conflictResults.map((r5) => {
@@ -364478,8 +364645,8 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
364478
364645
  }
364479
364646
  var import_node_crypto22 = require("crypto");
364480
364647
  var import_node_fs33 = require("fs");
364481
- var import_node_path62 = require("path");
364482
- var import_minimatch32 = require_commonjs3();
364648
+ var import_node_path92 = require("path");
364649
+ var import_minimatch42 = require_commonjs3();
364483
364650
  var import_yaml22 = require_dist11();
364484
364651
  var FernignoreMigrator = class {
364485
364652
  git;
@@ -364491,10 +364658,10 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
364491
364658
  this.outputDir = outputDir;
364492
364659
  }
364493
364660
  fernignoreExists() {
364494
- return (0, import_node_fs33.existsSync)((0, import_node_path62.join)(this.outputDir, ".fernignore"));
364661
+ return (0, import_node_fs33.existsSync)((0, import_node_path92.join)(this.outputDir, ".fernignore"));
364495
364662
  }
364496
364663
  readFernignorePatterns() {
364497
- const fernignorePath = (0, import_node_path62.join)(this.outputDir, ".fernignore");
364664
+ const fernignorePath = (0, import_node_path92.join)(this.outputDir, ".fernignore");
364498
364665
  if (!(0, import_node_fs33.existsSync)(fernignorePath)) {
364499
364666
  return [];
364500
364667
  }
@@ -364511,7 +364678,7 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
364511
364678
  const commitsOnly = [];
364512
364679
  const syntheticPatches = [];
364513
364680
  for (const pattern of patterns) {
364514
- const matchingPatch = patches.find((p14) => p14.files.some((f4) => (0, import_minimatch32.minimatch)(f4, pattern) || f4 === pattern));
364681
+ const matchingPatch = patches.find((p14) => p14.files.some((f4) => (0, import_minimatch42.minimatch)(f4, pattern) || f4 === pattern));
364515
364682
  if (matchingPatch) {
364516
364683
  trackedByBoth.push({
364517
364684
  file: pattern,
@@ -364584,7 +364751,7 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
364584
364751
  fernignoreOnly.push(...remainingFernignoreOnly);
364585
364752
  }
364586
364753
  for (const patch5 of patches) {
364587
- const hasUnprotectedFiles = patch5.files.some((f4) => !patterns.some((p14) => (0, import_minimatch32.minimatch)(f4, p14) || f4 === p14));
364754
+ const hasUnprotectedFiles = patch5.files.some((f4) => !patterns.some((p14) => (0, import_minimatch42.minimatch)(f4, p14) || f4 === p14));
364588
364755
  if (hasUnprotectedFiles) {
364589
364756
  commitsOnly.push(patch5);
364590
364757
  }
@@ -364596,14 +364763,14 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
364596
364763
  const results = [];
364597
364764
  for (const pattern of patterns) {
364598
364765
  const matching = allFiles.filter(
364599
- (f4) => (0, import_minimatch32.minimatch)(f4, pattern) || f4 === pattern || f4.startsWith(pattern + "/")
364766
+ (f4) => (0, import_minimatch42.minimatch)(f4, pattern) || f4 === pattern || f4.startsWith(pattern + "/")
364600
364767
  );
364601
364768
  results.push({ pattern, files: matching.length > 0 ? matching : [pattern] });
364602
364769
  }
364603
364770
  return results;
364604
364771
  }
364605
364772
  readFileContent(filePath) {
364606
- const fullPath = (0, import_node_path62.join)(this.outputDir, filePath);
364773
+ const fullPath = (0, import_node_path92.join)(this.outputDir, filePath);
364607
364774
  if (!(0, import_node_fs33.existsSync)(fullPath)) {
364608
364775
  return null;
364609
364776
  }
@@ -364668,7 +364835,7 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
364668
364835
  };
364669
364836
  }
364670
364837
  movePatternsToReplayYml(patterns) {
364671
- const replayYmlPath = (0, import_node_path62.join)(this.outputDir, ".fern", "replay.yml");
364838
+ const replayYmlPath = (0, import_node_path92.join)(this.outputDir, ".fern", "replay.yml");
364672
364839
  let config5 = {};
364673
364840
  if ((0, import_node_fs33.existsSync)(replayYmlPath)) {
364674
364841
  const content5 = (0, import_node_fs33.readFileSync)(replayYmlPath, "utf-8");
@@ -364677,7 +364844,7 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
364677
364844
  const existing = config5.exclude ?? [];
364678
364845
  const merged = [.../* @__PURE__ */ new Set([...existing, ...patterns])];
364679
364846
  config5.exclude = merged;
364680
- const dir = (0, import_node_path62.dirname)(replayYmlPath);
364847
+ const dir = (0, import_node_path92.dirname)(replayYmlPath);
364681
364848
  if (!(0, import_node_fs33.existsSync)(dir)) {
364682
364849
  (0, import_node_fs33.mkdirSync)(dir, { recursive: true });
364683
364850
  }
@@ -364686,7 +364853,7 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
364686
364853
  };
364687
364854
  var import_node_crypto32 = require("crypto");
364688
364855
  var import_node_fs42 = require("fs");
364689
- var import_node_path72 = require("path");
364856
+ var import_node_path102 = require("path");
364690
364857
  init_GitClient();
364691
364858
  async function bootstrap(outputDir, options2) {
364692
364859
  const git = new GitClient(outputDir);
@@ -364853,7 +365020,7 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
364853
365020
  }
364854
365021
  var REPLAY_FERNIGNORE_ENTRIES = [".fern/replay.lock", ".fern/replay.yml", ".gitattributes"];
364855
365022
  function ensureFernignoreEntries(outputDir) {
364856
- const fernignorePath = (0, import_node_path72.join)(outputDir, ".fernignore");
365023
+ const fernignorePath = (0, import_node_path102.join)(outputDir, ".fernignore");
364857
365024
  let content5 = "";
364858
365025
  if ((0, import_node_fs42.existsSync)(fernignorePath)) {
364859
365026
  content5 = (0, import_node_fs42.readFileSync)(fernignorePath, "utf-8");
@@ -364877,7 +365044,7 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
364877
365044
  }
364878
365045
  var GITATTRIBUTES_ENTRIES = [".fern/replay.lock linguist-generated=true"];
364879
365046
  function ensureGitattributesEntries(outputDir) {
364880
- const gitattributesPath = (0, import_node_path72.join)(outputDir, ".gitattributes");
365047
+ const gitattributesPath = (0, import_node_path102.join)(outputDir, ".gitattributes");
364881
365048
  let content5 = "";
364882
365049
  if ((0, import_node_fs42.existsSync)(gitattributesPath)) {
364883
365050
  content5 = (0, import_node_fs42.readFileSync)(gitattributesPath, "utf-8");
@@ -364905,7 +365072,7 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
364905
365072
  const normalized = relevantLines.filter((line) => !line.startsWith("index ")).join("\n").replace(/\n-- \n[\d]+\.[\d]+[^\n]*\s*$/, "");
364906
365073
  return `sha256:${(0, import_node_crypto32.createHash)("sha256").update(normalized).digest("hex")}`;
364907
365074
  }
364908
- var import_minimatch42 = require_commonjs3();
365075
+ var import_minimatch52 = require_commonjs3();
364909
365076
  function parseDiffStat(patchContent) {
364910
365077
  let additions = 0;
364911
365078
  let deletions = 0;
@@ -364935,7 +365102,7 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
364935
365102
  }
364936
365103
  function matchesPatch(patch5, pattern) {
364937
365104
  const fileMatch = patch5.files.some(
364938
- (file4) => file4 === pattern || (0, import_minimatch42.minimatch)(file4, pattern)
365105
+ (file4) => file4 === pattern || (0, import_minimatch52.minimatch)(file4, pattern)
364939
365106
  );
364940
365107
  if (fileMatch) return true;
364941
365108
  return patch5.original_message.toLowerCase().includes(pattern.toLowerCase());
@@ -365069,8 +365236,8 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
365069
365236
  nothingToReset: false
365070
365237
  };
365071
365238
  }
365072
- var import_promises510 = require("fs/promises");
365073
- var import_node_path82 = require("path");
365239
+ var import_promises710 = require("fs/promises");
365240
+ var import_node_path112 = require("path");
365074
365241
  init_GitClient();
365075
365242
  async function resolve15(outputDir, options2) {
365076
365243
  const lockManager = new LockfileManager(outputDir);
@@ -365119,7 +365286,7 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
365119
365286
  const result = await computePerPatchDiffWithFallback(
365120
365287
  patch5,
365121
365288
  (f4) => git.showFile(currentGen, f4),
365122
- (f4) => (0, import_promises510.readFile)((0, import_node_path82.join)(outputDir, f4), "utf-8").catch(() => null),
365289
+ (f4) => (0, import_promises710.readFile)((0, import_node_path112.join)(outputDir, f4), "utf-8").catch(() => null),
365123
365290
  (files) => git.exec(["diff", currentGen, "--", ...files]).catch(() => null)
365124
365291
  );
365125
365292
  if (result === null) {
@@ -365175,7 +365342,7 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
365175
365342
  const theirsSnapshot = {};
365176
365343
  for (const file4 of filesForSnapshot) {
365177
365344
  if (isBinaryFile(file4)) continue;
365178
- const content5 = await (0, import_promises510.readFile)((0, import_node_path82.join)(outputDir, file4), "utf-8").catch(() => null);
365345
+ const content5 = await (0, import_promises710.readFile)((0, import_node_path112.join)(outputDir, file4), "utf-8").catch(() => null);
365179
365346
  if (content5 != null) {
365180
365347
  theirsSnapshot[file4] = content5;
365181
365348
  }
@@ -667965,7 +668132,7 @@ var AccessTokenPosthogManager = class {
667965
668132
  properties: {
667966
668133
  ...event,
667967
668134
  ...event.properties,
667968
- version: "5.27.5",
668135
+ version: "5.27.6",
667969
668136
  usingAccessToken: true,
667970
668137
  ...getRunIdProperties()
667971
668138
  }
@@ -668029,7 +668196,7 @@ var UserPosthogManager = class {
668029
668196
  distinctId: this.userId ?? await this.getPersistedDistinctId(),
668030
668197
  event: "CLI",
668031
668198
  properties: {
668032
- version: "5.27.5",
668199
+ version: "5.27.6",
668033
668200
  ...event,
668034
668201
  ...event.properties,
668035
668202
  usingAccessToken: false,
@@ -859608,7 +859775,7 @@ var LOCAL_STORAGE_FOLDER4 = ".fern-dev";
859608
859775
  var LOGS_FOLDER_NAME = "logs";
859609
859776
  var MAX_LOGS_DIR_SIZE_BYTES = 100 * 1024 * 1024;
859610
859777
  function getCliSource() {
859611
- const version7 = "5.27.5";
859778
+ const version7 = "5.27.6";
859612
859779
  return `cli@${version7}`;
859613
859780
  }
859614
859781
  var DebugLogger = class {
@@ -873608,7 +873775,7 @@ var LegacyDocsPublisher = class {
873608
873775
  previewId,
873609
873776
  disableTemplates: void 0,
873610
873777
  skipUpload,
873611
- cliVersion: "5.27.5",
873778
+ cliVersion: "5.27.6",
873612
873779
  loginCommand: "fern auth login"
873613
873780
  });
873614
873781
  if (taskContext.getResult() === TaskResult.Failure) {
@@ -948013,7 +948180,7 @@ function getAutomationContextFromEnv() {
948013
948180
  config_branch: process.env.FERN_CONFIG_BRANCH,
948014
948181
  config_pr_number: process.env.FERN_CONFIG_PR_NUMBER,
948015
948182
  trigger: process.env.GITHUB_EVENT_NAME,
948016
- cli_version: "5.27.5"
948183
+ cli_version: "5.27.6"
948017
948184
  };
948018
948185
  }
948019
948186
  function isAutomationMode() {
@@ -948839,7 +949006,7 @@ var CliContext = class _CliContext {
948839
949006
  if (false) {
948840
949007
  this.logger.error("CLI_VERSION is not defined");
948841
949008
  }
948842
- return "5.27.5";
949009
+ return "5.27.6";
948843
949010
  }
948844
949011
  getCliName() {
948845
949012
  if (false) {