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

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 +1680 -1575
  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.2/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.2/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,81 +361091,608 @@ 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;
361095
+ var BINARY_EXTENSIONS2 = /* @__PURE__ */ new Set([
361096
+ ".png",
361097
+ ".jpg",
361098
+ ".jpeg",
361099
+ ".gif",
361100
+ ".bmp",
361101
+ ".ico",
361102
+ ".webp",
361103
+ ".svg",
361104
+ ".pdf",
361105
+ ".doc",
361106
+ ".docx",
361107
+ ".xls",
361108
+ ".xlsx",
361109
+ ".ppt",
361110
+ ".pptx",
361111
+ ".zip",
361112
+ ".gz",
361113
+ ".tar",
361114
+ ".bz2",
361115
+ ".7z",
361116
+ ".rar",
361117
+ ".jar",
361118
+ ".war",
361119
+ ".ear",
361120
+ ".class",
361121
+ ".exe",
361122
+ ".dll",
361123
+ ".so",
361124
+ ".dylib",
361125
+ ".o",
361126
+ ".a",
361127
+ ".woff",
361128
+ ".woff2",
361129
+ ".ttf",
361130
+ ".eot",
361131
+ ".otf",
361132
+ ".mp3",
361133
+ ".mp4",
361134
+ ".avi",
361135
+ ".mov",
361136
+ ".wav",
361137
+ ".flac",
361138
+ ".sqlite",
361139
+ ".db",
361140
+ ".pyc",
361141
+ ".pyo",
361142
+ ".DS_Store"
361143
+ ]);
361144
+ function isBinaryFile(filePath) {
361145
+ const ext2 = (0, import_node_path210.extname)(filePath).toLowerCase();
361146
+ return BINARY_EXTENSIONS2.has(ext2);
361103
361147
  }
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;
361148
+ var INFRASTRUCTURE_FILES = /* @__PURE__ */ new Set([".fernignore"]);
361149
+ function parsePatchFileHeaders(patchContent) {
361150
+ const results = [];
361151
+ let currentPath = null;
361152
+ let currentIsCreate = false;
361153
+ let currentIsDelete = false;
361154
+ const flush3 = () => {
361155
+ if (currentPath !== null) {
361156
+ results.push({ path: currentPath, isCreate: currentIsCreate, isDelete: currentIsDelete });
361157
+ }
361158
+ };
361159
+ for (const line of patchContent.split("\n")) {
361160
+ if (line.startsWith("diff --git ")) {
361161
+ flush3();
361162
+ currentPath = null;
361163
+ currentIsCreate = false;
361164
+ currentIsDelete = false;
361165
+ const match3 = line.match(/^diff --git a\/(.+?) b\/(.+?)$/);
361166
+ if (match3) {
361167
+ currentPath = match3[2] ?? null;
361168
+ }
361169
+ } else if (currentPath !== null) {
361170
+ if (line.startsWith("new file mode ")) {
361171
+ currentIsCreate = true;
361172
+ } else if (line.startsWith("deleted file mode ")) {
361173
+ currentIsDelete = true;
361174
+ }
361175
+ }
361176
+ }
361177
+ flush3();
361178
+ return results;
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
+ }
361195
+ var ReplayDetector = class {
361196
+ git;
361197
+ lockManager;
361198
+ sdkOutputDir;
361199
+ warnings = [];
361200
+ constructor(git, lockManager, sdkOutputDir) {
361201
+ this.git = git;
361202
+ this.lockManager = lockManager;
361203
+ this.sdkOutputDir = sdkOutputDir;
361204
+ }
361205
+ async detectNewPatches() {
361206
+ const lock = this.lockManager.read();
361207
+ const lastGen = this.getLastGeneration(lock);
361208
+ if (!lastGen) {
361209
+ return { patches: [], revertedPatchIds: [] };
361210
+ }
361211
+ const exists2 = await this.git.commitExists(lastGen.commit_sha);
361212
+ if (!exists2) {
361213
+ this.warnings.push(
361214
+ `Generation commit ${lastGen.commit_sha.slice(0, 7)} not found in git history. Falling back to alternate detection.`
361215
+ );
361216
+ return this.detectPatchesViaTreeDiff(
361217
+ lastGen,
361218
+ /* commitKnownMissing */
361219
+ true
361220
+ );
361221
+ }
361222
+ const isAncestor = await this.git.isAncestor(lastGen.commit_sha, "HEAD");
361223
+ if (!isAncestor) {
361224
+ return this.detectPatchesViaMergeBase(lastGen, lock);
361225
+ }
361226
+ return this.detectPatchesInRange(lastGen.commit_sha, lastGen, lock);
361227
+ }
361228
+ /**
361229
+ * Non-linear-history detection via `merge-base(prevGen, HEAD)`.
361230
+ *
361231
+ * After a squash-merge of a Fern-bot PR, `lastGen.commit_sha` (the previous
361232
+ * `[fern-generated]`) is orphaned but still in git's object database. The
361233
+ * merge-base is the commit on main from which the bot's branch was created —
361234
+ * a reachable ancestor of HEAD. Walking that range and classifying each
361235
+ * commit via `isGenerationCommit` mirrors the linear path and eliminates
361236
+ * the bug class where pipeline-driven changes (autoversion, replay,
361237
+ * generation) get bundled as customer customizations.
361238
+ *
361239
+ * Falls through to the legacy composite path when no common ancestor exists
361240
+ * (truly disjoint history — orphan branches with no shared root).
361241
+ */
361242
+ async detectPatchesViaMergeBase(lastGen, lock) {
361243
+ let mergeBase = "";
361244
+ try {
361245
+ mergeBase = (await this.git.exec(["merge-base", lastGen.commit_sha, "HEAD"])).trim();
361246
+ } catch {
361247
+ }
361248
+ if (!mergeBase) {
361249
+ this.warnings.push(
361250
+ `No common ancestor between previous generation ${lastGen.commit_sha.slice(0, 7)} and HEAD. Falling back to composite tree-diff detection.`
361251
+ );
361252
+ return this.detectPatchesViaTreeDiff(
361253
+ lastGen,
361254
+ /* commitKnownMissing */
361255
+ false
361256
+ );
361257
+ }
361258
+ return this.detectPatchesInRange(mergeBase, lastGen, lock);
361259
+ }
361260
+ /**
361261
+ * Walk `${rangeStart}..HEAD`, classify each commit, emit one
361262
+ * `StoredPatch` per customer commit. Body shared by the linear path
361263
+ * (rangeStart = lastGen.commit_sha) and the merge-base path.
361264
+ *
361265
+ * Patch `base_generation` is always `lastGen.commit_sha` — the
361266
+ * lockfile-tracked reference the applicator uses to fetch base file
361267
+ * content, regardless of where the walk actually started.
361268
+ */
361269
+ async detectPatchesInRange(rangeStart, lastGen, lock) {
361270
+ const log4 = await this.git.exec([
361271
+ "log",
361272
+ "--no-merges",
361273
+ "--format=%H%x00%an%x00%ae%x00%s",
361274
+ `${rangeStart}..HEAD`,
361275
+ "--",
361276
+ this.sdkOutputDir
361277
+ ]);
361278
+ if (!log4.trim()) {
361279
+ return { patches: [], revertedPatchIds: [] };
361280
+ }
361281
+ const commits = this.parseGitLog(log4);
361282
+ const newPatches = [];
361283
+ const forgottenHashes = new Set(lock.forgotten_hashes ?? []);
361284
+ for (const commit of commits) {
361285
+ if (isGenerationCommit(commit)) {
361286
+ continue;
361287
+ }
361288
+ if (lock.patches.find((p14) => p14.original_commit === commit.sha)) {
361289
+ continue;
361290
+ }
361291
+ const parents = await this.git.getCommitParents(commit.sha);
361292
+ let patchContent;
361293
+ try {
361294
+ patchContent = await this.git.formatPatch(commit.sha);
361295
+ } catch {
361296
+ this.warnings.push(
361297
+ `Could not generate patch for commit ${commit.sha.slice(0, 7)} \u2014 it may be unreachable in a shallow clone. Skipping.`
361298
+ );
361299
+ continue;
361300
+ }
361301
+ let contentHash = this.computeContentHash(patchContent);
361302
+ if (lock.patches.find((p14) => p14.content_hash === contentHash) || forgottenHashes.has(contentHash)) {
361303
+ continue;
361304
+ }
361305
+ const filesOutput = await this.git.exec(["diff-tree", "--no-commit-id", "--name-only", "-r", commit.sha]);
361306
+ const { files, sensitiveFiles } = filterInfrastructureAndSensitiveFiles(filesOutput);
361307
+ if (sensitiveFiles.length > 0) {
361308
+ this.warnings.push(
361309
+ `Sensitive file(s) excluded from patch for commit ${commit.sha.slice(0, 7)}: ${sensitiveFiles.join(", ")}. These files will not be tracked by replay.`
361310
+ );
361311
+ if (files.length > 0) {
361312
+ const parentSha = parents[0];
361313
+ if (parentSha) {
361314
+ try {
361315
+ patchContent = await this.git.exec(["diff", parentSha, commit.sha, "--", ...files]);
361316
+ } catch {
361317
+ continue;
361318
+ }
361319
+ if (!patchContent.trim()) continue;
361320
+ contentHash = this.computeContentHash(patchContent);
361321
+ }
361124
361322
  }
361125
- j7++;
361126
361323
  }
361127
- if (!found) {
361128
- i9++;
361324
+ if (files.length === 0) {
361129
361325
  continue;
361130
361326
  }
361327
+ const theirsSnapshot = await capturePatchSnapshot(this.git, files, commit.sha);
361328
+ newPatches.push({
361329
+ id: `patch-${commit.sha.slice(0, 8)}`,
361330
+ content_hash: contentHash,
361331
+ original_commit: commit.sha,
361332
+ original_message: commit.message,
361333
+ original_author: `${commit.authorName} <${commit.authorEmail}>`,
361334
+ base_generation: lastGen.commit_sha,
361335
+ files,
361336
+ patch_content: patchContent,
361337
+ ...Object.keys(theirsSnapshot).length > 0 ? { theirs_snapshot: theirsSnapshot } : {},
361338
+ ...this.isCreationOnlyPatch(patchContent) ? { user_owned: true } : {}
361339
+ });
361131
361340
  }
361132
- i9++;
361341
+ const survivingPatches = await this.collapseNetZeroFiles(newPatches);
361342
+ survivingPatches.reverse();
361343
+ const revertedPatchIdSet = /* @__PURE__ */ new Set();
361344
+ const revertIndicesToRemove = /* @__PURE__ */ new Set();
361345
+ for (let i9 = 0; i9 < survivingPatches.length; i9++) {
361346
+ const patch5 = survivingPatches[i9];
361347
+ if (!isRevertCommit(patch5.original_message)) continue;
361348
+ let body = "";
361349
+ try {
361350
+ body = await this.git.getCommitBody(patch5.original_commit);
361351
+ } catch {
361352
+ }
361353
+ const revertedSha = parseRevertedSha(body);
361354
+ const revertedMessage = parseRevertedMessage(patch5.original_message);
361355
+ let matchedExisting = false;
361356
+ if (revertedSha) {
361357
+ const existing = lock.patches.find((p14) => p14.original_commit === revertedSha);
361358
+ if (existing) {
361359
+ revertedPatchIdSet.add(existing.id);
361360
+ revertIndicesToRemove.add(i9);
361361
+ matchedExisting = true;
361362
+ }
361363
+ }
361364
+ if (!matchedExisting && revertedMessage) {
361365
+ const existing = lock.patches.find((p14) => p14.original_message === revertedMessage);
361366
+ if (existing) {
361367
+ revertedPatchIdSet.add(existing.id);
361368
+ revertIndicesToRemove.add(i9);
361369
+ matchedExisting = true;
361370
+ }
361371
+ }
361372
+ if (matchedExisting) continue;
361373
+ let matchedNew = false;
361374
+ if (revertedSha) {
361375
+ const idx = survivingPatches.findIndex(
361376
+ (p14, j7) => j7 !== i9 && !revertIndicesToRemove.has(j7) && p14.original_commit === revertedSha
361377
+ );
361378
+ if (idx !== -1) {
361379
+ revertIndicesToRemove.add(i9);
361380
+ revertIndicesToRemove.add(idx);
361381
+ matchedNew = true;
361382
+ }
361383
+ }
361384
+ if (!matchedNew && revertedMessage) {
361385
+ const idx = survivingPatches.findIndex(
361386
+ (p14, j7) => j7 !== i9 && !revertIndicesToRemove.has(j7) && p14.original_message === revertedMessage
361387
+ );
361388
+ if (idx !== -1) {
361389
+ revertIndicesToRemove.add(i9);
361390
+ revertIndicesToRemove.add(idx);
361391
+ }
361392
+ }
361393
+ if (!matchedExisting && !matchedNew) {
361394
+ revertIndicesToRemove.add(i9);
361395
+ }
361396
+ }
361397
+ const filteredPatches = survivingPatches.filter((_7, i9) => !revertIndicesToRemove.has(i9));
361398
+ return { patches: filteredPatches, revertedPatchIds: [...revertedPatchIdSet] };
361133
361399
  }
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;
361400
+ /**
361401
+ * Compute content hash for deduplication.
361402
+ *
361403
+ * Produces a format-agnostic hash so both `git format-patch` output
361404
+ * (email-wrapped) and plain `git diff` output hash to the same value
361405
+ * when the underlying diff hunks are identical.
361406
+ *
361407
+ * Normalization:
361408
+ * 1. Strip email wrapper: everything before the first `diff --git` line
361409
+ * (From:, Subject:, Date:, diffstat, blank separators).
361410
+ * 2. Strip `index` lines (blob SHA pairs change across rebases).
361411
+ * 3. Strip trailing git version marker (`-- \n<version>`).
361412
+ *
361413
+ * This ensures content hashes match across the no-patches and normal-
361414
+ * regeneration flows, which store formatPatch and git-diff formats
361415
+ * respectively.
361416
+ */
361417
+ computeContentHash(patchContent) {
361418
+ const lines = patchContent.split("\n");
361419
+ const diffStart = lines.findIndex((l9) => l9.startsWith("diff --git "));
361420
+ const relevantLines = diffStart > 0 ? lines.slice(diffStart) : lines;
361421
+ const normalized = relevantLines.filter((line) => !line.startsWith("index ")).join("\n").replace(/\n-- \n[\d]+\.[\d]+[^\n]*\s*$/, "");
361422
+ return `sha256:${(0, import_node_crypto7.createHash)("sha256").update(normalized).digest("hex")}`;
361141
361423
  }
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) {
361424
+ /**
361425
+ * Drop patches whose files were transiently created and then deleted
361426
+ * within the current linear detection window, and are absent from
361427
+ * HEAD at the end of the window.
361428
+ *
361429
+ * Rationale: each commit becomes its own patch, so a file that was
361430
+ * briefly added and later removed survives as a create+delete pair whose
361431
+ * cumulative diff is empty. We drop such patches before they reach the
361432
+ * lockfile.
361433
+ *
361434
+ * A file is "transient" when:
361435
+ * 1. some patch in the window sets `new file mode` for it (a creation), AND
361436
+ * 2. some patch in the window sets `deleted file mode` for it (a deletion), AND
361437
+ * 3. it is absent from HEAD's tree.
361438
+ *
361439
+ * The HEAD guard (#3) prevents false positives when a file is deleted
361440
+ * and then recreated with different content — the cumulative diff of
361441
+ * such a pair is non-empty and must be preserved.
361442
+ *
361443
+ * This only drops patches whose `files` are a subset of the transient
361444
+ * set. A partial-transient patch (one that touches a transient file
361445
+ * alongside a persistent one) is left unmodified; surgical stripping of
361446
+ * single files from a multi-file patch would require a follow-up that
361447
+ * regenerates the patch content via `git format-patch -- <files>`. No
361448
+ * current failing test exercises this, and leaving the patch intact
361449
+ * preserves today's behaviour. Revisit if a future adversarial test
361450
+ * demands per-file stripping.
361451
+ */
361452
+ async collapseNetZeroFiles(patches) {
361453
+ if (patches.length === 0) return patches;
361454
+ const stateByFile = /* @__PURE__ */ new Map();
361455
+ for (const patch5 of patches) {
361456
+ for (const header of parsePatchFileHeaders(patch5.patch_content)) {
361457
+ if (!header.isCreate && !header.isDelete) continue;
361458
+ const prior = stateByFile.get(header.path) ?? { created: false, deleted: false };
361459
+ if (header.isCreate) prior.created = true;
361460
+ if (header.isDelete) prior.deleted = true;
361461
+ stateByFile.set(header.path, prior);
361462
+ }
361463
+ }
361464
+ let anyCandidate = false;
361465
+ for (const state of stateByFile.values()) {
361466
+ if (state.created && state.deleted) {
361467
+ anyCandidate = true;
361468
+ break;
361469
+ }
361470
+ }
361471
+ if (!anyCandidate) return patches;
361472
+ const headFiles = await this.listHeadFiles();
361473
+ const transient = /* @__PURE__ */ new Set();
361474
+ for (const [file4, state] of stateByFile) {
361475
+ if (state.created && state.deleted && !headFiles.has(file4)) {
361476
+ transient.add(file4);
361477
+ }
361478
+ }
361479
+ if (transient.size === 0) return patches;
361480
+ return patches.filter((patch5) => !patch5.files.every((f4) => transient.has(f4)));
361481
+ }
361482
+ /**
361483
+ * List every tracked path at HEAD (one `git ls-tree -r` invocation).
361484
+ * Returns an empty Set if HEAD is unborn or the invocation fails — the
361485
+ * caller then treats every candidate as "not in HEAD" and may collapse
361486
+ * more aggressively. On typical SDK repos this is a single sub-10ms call.
361487
+ */
361488
+ async listHeadFiles() {
361489
+ try {
361490
+ const out = await this.git.exec(["ls-tree", "-r", "--name-only", "HEAD"]);
361491
+ return new Set(out.split("\n").map((l9) => l9.trim()).filter(Boolean));
361492
+ } catch {
361493
+ return /* @__PURE__ */ new Set();
361494
+ }
361495
+ }
361496
+ /**
361497
+ * Detect patches via tree diff for non-linear history. Returns a composite patch.
361498
+ * Revert reconciliation is skipped here because tree-diff produces a single composite
361499
+ * patch from the aggregate diff — individual revert commits are not distinguishable.
361500
+ */
361501
+ async detectPatchesViaTreeDiff(lastGen, commitKnownMissing) {
361502
+ const diffBase = await this.resolveDiffBase(lastGen, commitKnownMissing);
361503
+ if (!diffBase) {
361504
+ return this.detectPatchesViaCommitScan();
361505
+ }
361506
+ const lockForGuard = this.lockManager.read();
361507
+ const knownGenerations = new Set(lockForGuard.generations.map((g8) => g8.commit_sha));
361508
+ let resolvedBaseGeneration = commitKnownMissing ? diffBase : lastGen.commit_sha;
361509
+ if (commitKnownMissing && !knownGenerations.has(diffBase)) {
361510
+ const fallback2 = lockForGuard.current_generation ?? lockForGuard.generations[lockForGuard.generations.length - 1]?.commit_sha;
361511
+ if (!fallback2) {
361512
+ this.warnings.push(
361513
+ `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.`
361514
+ );
361515
+ return { patches: [], revertedPatchIds: [] };
361516
+ }
361517
+ resolvedBaseGeneration = fallback2;
361518
+ }
361519
+ const filesOutput = await this.git.exec(["diff", "--name-only", diffBase, "HEAD"]);
361520
+ const { files, sensitiveFiles } = filterInfrastructureAndSensitiveFiles(filesOutput);
361521
+ if (sensitiveFiles.length > 0) {
361522
+ this.warnings.push(
361523
+ `Sensitive file(s) excluded from composite patch: ${sensitiveFiles.join(", ")}. These files will not be tracked by replay.`
361524
+ );
361525
+ }
361526
+ if (files.length === 0) return { patches: [], revertedPatchIds: [] };
361527
+ const diff2 = await this.git.exec(["diff", diffBase, "HEAD", "--", ...files]);
361528
+ if (!diff2.trim()) return { patches: [], revertedPatchIds: [] };
361529
+ const contentHash = this.computeContentHash(diff2);
361530
+ const lock = this.lockManager.read();
361531
+ if (lock.patches.some((p14) => p14.content_hash === contentHash) || (lock.forgotten_hashes ?? []).includes(contentHash)) {
361532
+ return { patches: [], revertedPatchIds: [] };
361533
+ }
361534
+ const headSha = (await this.git.exec(["rev-parse", "HEAD"])).trim();
361535
+ const theirsSnapshot = await capturePatchSnapshot(this.git, files, "HEAD");
361536
+ const compositePatch = {
361537
+ id: `patch-composite-${headSha.slice(0, 8)}`,
361538
+ content_hash: contentHash,
361539
+ original_commit: headSha,
361540
+ original_message: "Customer customizations (composite)",
361541
+ original_author: "composite",
361542
+ // Anchor `base_generation` on a SHA the applicator's
361543
+ // `resolveBaseGeneration` can find — always a member of
361544
+ // `lock.generations`. When `diffBase` is a recorded
361545
+ // generation, use it directly; otherwise the guard above
361546
+ // has already mapped it onto `current_generation`.
361547
+ base_generation: resolvedBaseGeneration,
361548
+ files,
361549
+ patch_content: diff2,
361550
+ ...Object.keys(theirsSnapshot).length > 0 ? { theirs_snapshot: theirsSnapshot } : {},
361551
+ ...this.isCreationOnlyPatch(diff2) ? { user_owned: true } : {}
361552
+ };
361553
+ return { patches: [compositePatch], revertedPatchIds: [] };
361554
+ }
361555
+ /**
361556
+ * Last-resort detection when both generation commit and tree are unreachable.
361557
+ * Scans all commits from HEAD, filters against known lockfile patches, and
361558
+ * skips creation-only commits (squashed history after force push).
361559
+ *
361560
+ * Detected patches use the commit's parent as base_generation so the applicator
361561
+ * can find base file content from the parent's tree (which IS reachable).
361562
+ */
361563
+ async detectPatchesViaCommitScan() {
361564
+ const lock = this.lockManager.read();
361565
+ const knownGenerations = new Set(lock.generations.map((g8) => g8.commit_sha));
361566
+ const fallbackAnchor = lock.current_generation ?? lock.generations[lock.generations.length - 1]?.commit_sha;
361567
+ const log4 = await this.git.exec([
361568
+ "log",
361569
+ "--max-count=200",
361570
+ "--format=%H%x00%an%x00%ae%x00%s",
361571
+ "HEAD",
361572
+ "--",
361573
+ this.sdkOutputDir
361574
+ ]);
361575
+ if (!log4.trim()) {
361576
+ return { patches: [], revertedPatchIds: [] };
361577
+ }
361578
+ const commits = this.parseGitLog(log4);
361579
+ const newPatches = [];
361580
+ const forgottenHashes = new Set(lock.forgotten_hashes ?? []);
361581
+ const existingHashes = new Set(lock.patches.map((p14) => p14.content_hash));
361582
+ const existingCommits = new Set(lock.patches.map((p14) => p14.original_commit));
361583
+ for (const commit of commits) {
361584
+ if (isGenerationCommit(commit)) {
361148
361585
  continue;
361149
361586
  }
361150
- if (i9 > range2.start && i9 < range2.separator) {
361151
- result.push(lines[i9]);
361587
+ const parents = await this.git.getCommitParents(commit.sha);
361588
+ if (parents.length > 1) {
361152
361589
  continue;
361153
361590
  }
361154
- if (i9 === range2.separator) {
361591
+ if (existingCommits.has(commit.sha)) {
361155
361592
  continue;
361156
361593
  }
361157
- if (i9 > range2.separator && i9 < range2.end) {
361594
+ let patchContent;
361595
+ try {
361596
+ patchContent = await this.git.formatPatch(commit.sha);
361597
+ } catch {
361158
361598
  continue;
361159
361599
  }
361160
- if (i9 === range2.end) {
361161
- rangeIdx++;
361600
+ let contentHash = this.computeContentHash(patchContent);
361601
+ if (existingHashes.has(contentHash) || forgottenHashes.has(contentHash)) {
361602
+ continue;
361603
+ }
361604
+ if (this.isCreationOnlyPatch(patchContent)) {
361605
+ continue;
361606
+ }
361607
+ const filesOutput = await this.git.exec(["diff-tree", "--no-commit-id", "--name-only", "-r", commit.sha]);
361608
+ const { files, sensitiveFiles } = filterInfrastructureAndSensitiveFiles(filesOutput);
361609
+ if (sensitiveFiles.length > 0) {
361610
+ this.warnings.push(
361611
+ `Sensitive file(s) excluded from patch for commit ${commit.sha.slice(0, 7)}: ${sensitiveFiles.join(", ")}. These files will not be tracked by replay.`
361612
+ );
361613
+ if (files.length > 0) {
361614
+ if (parents.length === 0) {
361615
+ continue;
361616
+ }
361617
+ try {
361618
+ patchContent = await this.git.exec(["diff", parents[0], commit.sha, "--", ...files]);
361619
+ } catch {
361620
+ continue;
361621
+ }
361622
+ if (!patchContent.trim()) continue;
361623
+ contentHash = this.computeContentHash(patchContent);
361624
+ }
361625
+ }
361626
+ if (files.length === 0) {
361162
361627
  continue;
361163
361628
  }
361629
+ if (parents.length === 0) {
361630
+ continue;
361631
+ }
361632
+ const parentSha = parents[0];
361633
+ let baseGeneration;
361634
+ if (knownGenerations.has(parentSha)) {
361635
+ baseGeneration = parentSha;
361636
+ } else if (fallbackAnchor) {
361637
+ baseGeneration = fallbackAnchor;
361638
+ } else {
361639
+ this.warnings.push(
361640
+ `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.`
361641
+ );
361642
+ continue;
361643
+ }
361644
+ const theirsSnapshot = await capturePatchSnapshot(this.git, files, commit.sha);
361645
+ newPatches.push({
361646
+ id: `patch-${commit.sha.slice(0, 8)}`,
361647
+ content_hash: contentHash,
361648
+ original_commit: commit.sha,
361649
+ original_message: commit.message,
361650
+ original_author: `${commit.authorName} <${commit.authorEmail}>`,
361651
+ base_generation: baseGeneration,
361652
+ files,
361653
+ patch_content: patchContent,
361654
+ ...Object.keys(theirsSnapshot).length > 0 ? { theirs_snapshot: theirsSnapshot } : {}
361655
+ });
361164
361656
  }
361165
- result.push(lines[i9]);
361657
+ newPatches.reverse();
361658
+ return { patches: newPatches, revertedPatchIds: [] };
361166
361659
  }
361167
- return result.join("\n");
361168
- }
361660
+ /**
361661
+ * Check if a format-patch consists entirely of new-file creations.
361662
+ * Used to identify squashed commits after force push, which create all files
361663
+ * from scratch (--- /dev/null) rather than modifying existing files.
361664
+ */
361665
+ isCreationOnlyPatch(patchContent) {
361666
+ const diffOldHeaders = patchContent.split("\n").filter((l9) => l9.startsWith("--- "));
361667
+ if (diffOldHeaders.length === 0) {
361668
+ return false;
361669
+ }
361670
+ return diffOldHeaders.every((l9) => l9 === "--- /dev/null");
361671
+ }
361672
+ /**
361673
+ * Resolve the best available diff base for a generation record.
361674
+ * Prefers commit_sha, falls back to tree_hash for unreachable commits.
361675
+ * When commitKnownMissing is true, skips the redundant commitExists check.
361676
+ */
361677
+ async resolveDiffBase(gen, commitKnownMissing) {
361678
+ if (!commitKnownMissing && await this.git.commitExists(gen.commit_sha)) {
361679
+ return gen.commit_sha;
361680
+ }
361681
+ if (await this.git.treeExists(gen.tree_hash)) {
361682
+ return gen.tree_hash;
361683
+ }
361684
+ return null;
361685
+ }
361686
+ parseGitLog(log4) {
361687
+ return log4.trim().split("\n").map((line) => {
361688
+ const [sha, authorName, authorEmail, message] = line.split("\0");
361689
+ return { sha, authorName, authorEmail, message };
361690
+ });
361691
+ }
361692
+ getLastGeneration(lock) {
361693
+ return lock.generations.find((g8) => g8.commit_sha === lock.current_generation);
361694
+ }
361695
+ };
361169
361696
  var import_node_diff3 = require_diff3();
361170
361697
  function threeWayMerge(base4, ours, theirs) {
361171
361698
  const baseLines = base4.split("\n");
@@ -361303,95 +361830,414 @@ var require_dist14 = __commonJS({
361303
361830
  }
361304
361831
  return result;
361305
361832
  }
361306
- var BINARY_EXTENSIONS2 = /* @__PURE__ */ new Set([
361307
- ".png",
361308
- ".jpg",
361309
- ".jpeg",
361310
- ".gif",
361311
- ".bmp",
361312
- ".ico",
361313
- ".webp",
361314
- ".svg",
361315
- ".pdf",
361316
- ".doc",
361317
- ".docx",
361318
- ".xls",
361319
- ".xlsx",
361320
- ".ppt",
361321
- ".pptx",
361322
- ".zip",
361323
- ".gz",
361324
- ".tar",
361325
- ".bz2",
361326
- ".7z",
361327
- ".rar",
361328
- ".jar",
361329
- ".war",
361330
- ".ear",
361331
- ".class",
361332
- ".exe",
361333
- ".dll",
361334
- ".so",
361335
- ".dylib",
361336
- ".o",
361337
- ".a",
361338
- ".woff",
361339
- ".woff2",
361340
- ".ttf",
361341
- ".eot",
361342
- ".otf",
361343
- ".mp3",
361344
- ".mp4",
361345
- ".avi",
361346
- ".mov",
361347
- ".wav",
361348
- ".flac",
361349
- ".sqlite",
361350
- ".db",
361351
- ".pyc",
361352
- ".pyo",
361353
- ".DS_Store"
361354
- ]);
361833
+ var import_promises310 = require("fs/promises");
361834
+ var import_node_os15 = require("os");
361835
+ var import_node_path52 = require("path");
361836
+ var import_minimatch8 = require_commonjs3();
361837
+ var MergeAccumulator = class {
361838
+ constructor(mode, sharedFiles) {
361839
+ this.mode = mode;
361840
+ this.sharedFiles = sharedFiles;
361841
+ }
361842
+ entries = /* @__PURE__ */ new Map();
361843
+ /** Was this file touched by 2+ patches in the current `applyPatches` invocation? */
361844
+ isShared(path111) {
361845
+ return this.sharedFiles.has(path111);
361846
+ }
361847
+ /** Has any prior patch already recorded a result for this path? */
361848
+ has(path111) {
361849
+ return this.entries.has(path111);
361850
+ }
361851
+ /** Look up the accumulated content + base generation for this path. */
361852
+ lookup(path111) {
361853
+ return this.entries.get(path111);
361854
+ }
361855
+ /**
361856
+ * True if any of the patch's files have a prior accumulator entry — in
361857
+ * which case the fast path (`git apply --3way`) cannot honor prior
361858
+ * patches' contributions and we must route through the slow path.
361859
+ */
361860
+ shouldSkipFastPath(patch5) {
361861
+ return patch5.files.some((f4) => this.entries.has(f4));
361862
+ }
361863
+ /** Record a successful (clean) merge result. */
361864
+ recordMerge(path111, content5, baseGeneration) {
361865
+ this.entries.set(path111, { content: content5, baseGeneration });
361866
+ }
361867
+ /**
361868
+ * Record a conflicted merge result. Populates only in `"resolve"` mode
361869
+ * — replay mode keeps the accumulator clean of marker-laden content
361870
+ * since markers will be stripped from disk after the apply loop.
361871
+ */
361872
+ recordConflict(path111, content5, baseGeneration) {
361873
+ if (this.mode === "resolve") {
361874
+ this.entries.set(path111, { content: content5, baseGeneration });
361875
+ }
361876
+ }
361877
+ /**
361878
+ * Defensive copy for test introspection. Used by invariant I2 to assert
361879
+ * that after every applied patch, the accumulator has an entry for each
361880
+ * file the patch touched (see `__tests__/invariants/i2-accumulator-correctness.ts`).
361881
+ *
361882
+ * @internal
361883
+ */
361884
+ snapshot() {
361885
+ const copy = /* @__PURE__ */ new Map();
361886
+ for (const [path111, entry] of this.entries) {
361887
+ copy.set(path111, { content: entry.content, baseGeneration: entry.baseGeneration });
361888
+ }
361889
+ return copy;
361890
+ }
361891
+ };
361892
+ var import_promises195 = require("fs/promises");
361893
+ var import_node_path310 = require("path");
361894
+ async function resolveInputs(args) {
361895
+ const {
361896
+ patch: patch5,
361897
+ filePath,
361898
+ git,
361899
+ lockManager,
361900
+ outputDir,
361901
+ isTreeReachable,
361902
+ resolveBaseGeneration,
361903
+ resolveFilePath,
361904
+ extractRenameSource,
361905
+ extractFileDiff: extractFileDiff2
361906
+ } = args;
361907
+ const baseGen = await resolveBaseGeneration(patch5.base_generation);
361908
+ if (!baseGen) {
361909
+ return {
361910
+ resolvedPath: filePath,
361911
+ metadata: {
361912
+ patchId: patch5.id,
361913
+ patchMessage: patch5.original_message,
361914
+ baseGeneration: patch5.base_generation,
361915
+ currentGeneration: lockManager.read().current_generation
361916
+ },
361917
+ base: null,
361918
+ ours: null,
361919
+ oursPath: (0, import_node_path310.join)(outputDir, filePath),
361920
+ renameSourcePath: void 0,
361921
+ ghostTheirs: null,
361922
+ earlyExit: { status: "skipped", reason: "base-generation-not-found" }
361923
+ };
361924
+ }
361925
+ const lock = lockManager.read();
361926
+ const currentGen = lock.generations.find((g8) => g8.commit_sha === lock.current_generation);
361927
+ const currentTreeHash = currentGen?.tree_hash ?? baseGen.tree_hash;
361928
+ const resolvedPath = await resolveFilePath(filePath, baseGen.tree_hash, currentTreeHash);
361929
+ const metadata = {
361930
+ patchId: patch5.id,
361931
+ patchMessage: patch5.original_message,
361932
+ baseGeneration: patch5.base_generation,
361933
+ currentGeneration: lock.current_generation
361934
+ };
361935
+ let base4 = await git.showFile(baseGen.tree_hash, filePath);
361936
+ let renameSourcePath;
361937
+ if (!base4) {
361938
+ const renameSource = extractRenameSource(patch5.patch_content, filePath);
361939
+ if (renameSource) {
361940
+ base4 = await git.showFile(baseGen.tree_hash, renameSource);
361941
+ renameSourcePath = renameSource;
361942
+ }
361943
+ }
361944
+ const oursPath = (0, import_node_path310.join)(outputDir, resolvedPath);
361945
+ const ours = await (0, import_promises195.readFile)(oursPath, "utf-8").catch(() => null);
361946
+ let ghostTheirs = null;
361947
+ if (!base4 && ours && !renameSourcePath) {
361948
+ const treeReachable = await isTreeReachable(baseGen.tree_hash);
361949
+ if (!treeReachable) {
361950
+ const fileDiff = extractFileDiff2(patch5.patch_content, filePath);
361951
+ if (fileDiff) {
361952
+ const { reconstructFromGhostPatch: reconstructFromGhostPatch2 } = await Promise.resolve().then(() => (init_HybridReconstruction(), HybridReconstruction_exports));
361953
+ const result = reconstructFromGhostPatch2(fileDiff, ours);
361954
+ if (result) {
361955
+ base4 = result.base;
361956
+ ghostTheirs = result.theirs;
361957
+ }
361958
+ }
361959
+ }
361960
+ }
361961
+ return {
361962
+ resolvedPath,
361963
+ metadata,
361964
+ base: base4,
361965
+ ours,
361966
+ oursPath,
361967
+ renameSourcePath,
361968
+ ghostTheirs
361969
+ };
361970
+ }
361971
+ var CONFLICT_OPENER = "<<<<<<< Generated";
361972
+ var CONFLICT_SEPARATOR = "=======";
361973
+ var CONFLICT_CLOSER = ">>>>>>> Your customization";
361974
+ function trimCR(line) {
361975
+ return line.endsWith("\r") ? line.slice(0, -1) : line;
361976
+ }
361977
+ function findConflictRanges(lines) {
361978
+ const ranges = [];
361979
+ let i9 = 0;
361980
+ while (i9 < lines.length) {
361981
+ if (trimCR(lines[i9]) === CONFLICT_OPENER) {
361982
+ let separatorIdx = -1;
361983
+ let j7 = i9 + 1;
361984
+ let found = false;
361985
+ while (j7 < lines.length) {
361986
+ const trimmed2 = trimCR(lines[j7]);
361987
+ if (trimmed2 === CONFLICT_OPENER) {
361988
+ break;
361989
+ }
361990
+ if (separatorIdx === -1 && trimmed2 === CONFLICT_SEPARATOR) {
361991
+ separatorIdx = j7;
361992
+ } else if (separatorIdx !== -1 && trimmed2 === CONFLICT_CLOSER) {
361993
+ ranges.push({ start: i9, separator: separatorIdx, end: j7 });
361994
+ i9 = j7;
361995
+ found = true;
361996
+ break;
361997
+ }
361998
+ j7++;
361999
+ }
362000
+ if (!found) {
362001
+ i9++;
362002
+ continue;
362003
+ }
362004
+ }
362005
+ i9++;
362006
+ }
362007
+ return ranges;
362008
+ }
362009
+ function stripConflictMarkers(content5) {
362010
+ const lines = content5.split(/\r?\n/);
362011
+ const ranges = findConflictRanges(lines);
362012
+ if (ranges.length === 0) {
362013
+ return content5;
362014
+ }
362015
+ const result = [];
362016
+ let rangeIdx = 0;
362017
+ for (let i9 = 0; i9 < lines.length; i9++) {
362018
+ if (rangeIdx < ranges.length) {
362019
+ const range2 = ranges[rangeIdx];
362020
+ if (i9 === range2.start) {
362021
+ continue;
362022
+ }
362023
+ if (i9 > range2.start && i9 < range2.separator) {
362024
+ result.push(lines[i9]);
362025
+ continue;
362026
+ }
362027
+ if (i9 === range2.separator) {
362028
+ continue;
362029
+ }
362030
+ if (i9 > range2.separator && i9 < range2.end) {
362031
+ continue;
362032
+ }
362033
+ if (i9 === range2.end) {
362034
+ rangeIdx++;
362035
+ continue;
362036
+ }
362037
+ }
362038
+ result.push(lines[i9]);
362039
+ }
362040
+ return result.join("\n");
362041
+ }
362042
+ function hasConflictMarkerStrings(content5) {
362043
+ if (content5 == null) return false;
362044
+ return content5.includes(CONFLICT_OPENER) || content5.includes(CONFLICT_CLOSER);
362045
+ }
362046
+ async function buildMergePlan(args) {
362047
+ const {
362048
+ inputs,
362049
+ patch: patch5,
362050
+ filePath,
362051
+ accumulator,
362052
+ tempGit,
362053
+ tempDir: tempDir2,
362054
+ applyPatchToContent: applyPatchToContent2,
362055
+ isPatchAlreadyApplied
362056
+ } = args;
362057
+ const { base: base4, ours, resolvedPath, renameSourcePath, ghostTheirs } = inputs;
362058
+ let theirs = ghostTheirs;
362059
+ const ghostReconstructed = ghostTheirs != null;
362060
+ const snapshotForFile = patch5.theirs_snapshot?.[resolvedPath] ?? patch5.theirs_snapshot?.[filePath];
362061
+ const fileIsShared = accumulator.isShared(resolvedPath) || accumulator.isShared(filePath);
362062
+ const useSnapshotAsPrimary = !fileIsShared && snapshotForFile != null;
362063
+ let primarySource = ghostReconstructed ? "ghost" : "reconstructed";
362064
+ if (!ghostReconstructed) {
362065
+ if (useSnapshotAsPrimary) {
362066
+ theirs = snapshotForFile;
362067
+ primarySource = "snapshot";
362068
+ } else {
362069
+ theirs = await applyPatchToContent2(
362070
+ base4,
362071
+ patch5.patch_content,
362072
+ filePath,
362073
+ tempGit,
362074
+ tempDir2,
362075
+ renameSourcePath
362076
+ );
362077
+ const reconstructionHasMarkers = hasConflictMarkerStrings(theirs);
362078
+ const baseHadMarkers = hasConflictMarkerStrings(base4);
362079
+ if (reconstructionHasMarkers && !baseHadMarkers && snapshotForFile != null) {
362080
+ theirs = snapshotForFile;
362081
+ }
362082
+ }
362083
+ }
362084
+ const accumulatorEntry = accumulator.lookup(resolvedPath);
362085
+ if (theirs) {
362086
+ const theirsHasMarkers = hasConflictMarkerStrings(theirs);
362087
+ const baseHasMarkers = hasConflictMarkerStrings(base4);
362088
+ if (theirsHasMarkers && !baseHasMarkers) {
362089
+ if (accumulatorEntry) {
362090
+ theirs = null;
362091
+ } else {
362092
+ return { kind: "skipped", reason: "stale-conflict-markers" };
362093
+ }
362094
+ }
362095
+ }
362096
+ let useAccumulatorAsMergeBase = false;
362097
+ if (accumulatorEntry && (theirs == null || base4 == null)) {
362098
+ theirs = await applyPatchToContent2(
362099
+ accumulatorEntry.content,
362100
+ patch5.patch_content,
362101
+ filePath,
362102
+ tempGit,
362103
+ tempDir2
362104
+ );
362105
+ if (theirs != null) {
362106
+ useAccumulatorAsMergeBase = true;
362107
+ } else if (await isPatchAlreadyApplied(
362108
+ patch5.patch_content,
362109
+ filePath,
362110
+ accumulatorEntry.content,
362111
+ tempGit,
362112
+ tempDir2
362113
+ )) {
362114
+ theirs = accumulatorEntry.content;
362115
+ useAccumulatorAsMergeBase = true;
362116
+ }
362117
+ }
362118
+ if (theirs) {
362119
+ const theirsHasMarkers = hasConflictMarkerStrings(theirs);
362120
+ const accBaseHasMarkers = accumulatorEntry != null && hasConflictMarkerStrings(accumulatorEntry.content);
362121
+ if (theirsHasMarkers && !accBaseHasMarkers && !hasConflictMarkerStrings(base4)) {
362122
+ return { kind: "skipped", reason: "stale-conflict-markers" };
362123
+ }
362124
+ }
362125
+ let effective_theirs = theirs;
362126
+ let conflictReasonHint;
362127
+ if (theirs != null && base4 != null && !useAccumulatorAsMergeBase) {
362128
+ if (accumulatorEntry && accumulatorEntry.baseGeneration === patch5.base_generation) {
362129
+ try {
362130
+ const preMerged = threeWayMerge(base4, accumulatorEntry.content, theirs);
362131
+ if (!preMerged.hasConflicts) {
362132
+ effective_theirs = preMerged.content;
362133
+ } else {
362134
+ effective_theirs = theirs;
362135
+ }
362136
+ } catch {
362137
+ effective_theirs = theirs;
362138
+ }
362139
+ } else if (accumulatorEntry) {
362140
+ conflictReasonHint = "base-generation-mismatch";
362141
+ }
362142
+ }
362143
+ if (base4 == null && ours == null && effective_theirs != null) {
362144
+ return { kind: "new-file-only-user", theirs: effective_theirs };
362145
+ }
362146
+ if (base4 == null && ours != null && effective_theirs != null && !useAccumulatorAsMergeBase) {
362147
+ return { kind: "new-file-both", theirs: effective_theirs };
362148
+ }
362149
+ if (effective_theirs == null) {
362150
+ return { kind: "skipped", reason: "missing-content" };
362151
+ }
362152
+ if (base4 == null && !useAccumulatorAsMergeBase || ours == null) {
362153
+ return { kind: "skipped", reason: "missing-content" };
362154
+ }
362155
+ if (useAccumulatorAsMergeBase) {
362156
+ const mergeBase = accumulatorEntry?.content ?? base4;
362157
+ if (mergeBase == null) {
362158
+ return { kind: "skipped", reason: "missing-content" };
362159
+ }
362160
+ return {
362161
+ kind: "accumulator-base",
362162
+ theirs: effective_theirs,
362163
+ mergeBase,
362164
+ conflictReasonHint
362165
+ };
362166
+ }
362167
+ return {
362168
+ kind: primarySource,
362169
+ base: base4,
362170
+ theirs: effective_theirs,
362171
+ mergeBase: base4,
362172
+ conflictReasonHint
362173
+ };
362174
+ }
362175
+ var import_promises210 = require("fs/promises");
362176
+ var import_node_path43 = require("path");
362177
+ async function runMergeAndRecord(args) {
362178
+ const { plan, inputs, patch: patch5, accumulator } = args;
362179
+ const { resolvedPath, metadata, oursPath, ours } = inputs;
362180
+ if (plan.kind === "skipped") {
362181
+ return { file: resolvedPath, status: "skipped", reason: plan.reason };
362182
+ }
362183
+ if (plan.kind === "new-file-only-user") {
362184
+ accumulator.recordMerge(resolvedPath, plan.theirs, patch5.base_generation);
362185
+ await (0, import_promises210.mkdir)((0, import_node_path43.dirname)(oursPath), { recursive: true });
362186
+ await (0, import_promises210.writeFile)(oursPath, plan.theirs);
362187
+ return { file: resolvedPath, status: "merged", reason: "new-file" };
362188
+ }
362189
+ if (plan.kind === "new-file-both") {
362190
+ const merged2 = threeWayMerge("", ours, plan.theirs);
362191
+ await (0, import_promises210.mkdir)((0, import_node_path43.dirname)(oursPath), { recursive: true });
362192
+ await (0, import_promises210.writeFile)(oursPath, merged2.content);
362193
+ if (merged2.hasConflicts) {
362194
+ return {
362195
+ file: resolvedPath,
362196
+ status: "conflict",
362197
+ conflicts: merged2.conflicts,
362198
+ conflictReason: "new-file-both",
362199
+ conflictMetadata: metadata
362200
+ };
362201
+ }
362202
+ accumulator.recordMerge(resolvedPath, merged2.content, patch5.base_generation);
362203
+ return { file: resolvedPath, status: "merged" };
362204
+ }
362205
+ const merged = threeWayMerge(plan.mergeBase, ours, plan.theirs);
362206
+ await (0, import_promises210.mkdir)((0, import_node_path43.dirname)(oursPath), { recursive: true });
362207
+ await (0, import_promises210.writeFile)(oursPath, merged.content);
362208
+ if (merged.hasConflicts) {
362209
+ accumulator.recordConflict(resolvedPath, plan.theirs, patch5.base_generation);
362210
+ return {
362211
+ file: resolvedPath,
362212
+ status: "conflict",
362213
+ conflicts: merged.conflicts,
362214
+ conflictReason: plan.conflictReasonHint ?? "same-line-edit",
362215
+ conflictMetadata: metadata
362216
+ };
362217
+ }
362218
+ accumulator.recordMerge(resolvedPath, plan.theirs, patch5.base_generation);
362219
+ return {
362220
+ file: resolvedPath,
362221
+ status: "merged",
362222
+ ...merged.droppedContextLines && merged.droppedContextLines.length > 0 ? { droppedContextLines: merged.droppedContextLines } : {}
362223
+ };
362224
+ }
361355
362225
  var ReplayApplicator = class {
361356
362226
  git;
361357
362227
  lockManager;
361358
362228
  outputDir;
361359
362229
  renameCache = /* @__PURE__ */ new Map();
361360
362230
  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.
362231
+ /**
362232
+ * Inter-patch THEIRS coordination. Created fresh in each `applyPatches`
362233
+ * invocation with the run's apply mode + precomputed shared-file set.
362234
+ * See `src/accumulator/MergeAccumulator.ts` for the full contract.
361386
362235
  *
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();
362236
+ * Initialized to an empty default in the constructor so type narrowing
362237
+ * works for callers that read it before any `applyPatches` invocation
362238
+ * (e.g., the `getAccumulatorSnapshot` test accessor).
362239
+ */
362240
+ accumulator = new MergeAccumulator("replay", /* @__PURE__ */ new Set());
361395
362241
  constructor(git, lockManager, outputDir) {
361396
362242
  this.git = git;
361397
362243
  this.lockManager = lockManager;
@@ -361418,23 +362264,15 @@ var require_dist14 = __commonJS({
361418
362264
  return void 0;
361419
362265
  }
361420
362266
  }
361421
- /** Reset inter-patch accumulator for a new cycle. */
361422
- resetAccumulator() {
361423
- this.fileTheirsAccumulator.clear();
361424
- }
361425
362267
  /**
361426
362268
  * @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.
362269
+ * Returns a defensive copy of the inter-patch accumulator. Used by
362270
+ * invariant I2 to assert that after every applied patch, the
362271
+ * accumulator has an entry for each file the patch touched, and the
362272
+ * entry's content matches on-disk.
361431
362273
  */
361432
362274
  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;
362275
+ return this.accumulator.snapshot();
361438
362276
  }
361439
362277
  /**
361440
362278
  * Apply all patches, returning results for each.
@@ -361455,17 +362293,16 @@ var require_dist14 = __commonJS({
361455
362293
  */
361456
362294
  async applyPatches(patches, opts) {
361457
362295
  const applyMode = opts?.applyMode ?? "replay";
361458
- this.currentApplyMode = applyMode;
361459
- this.resetAccumulator();
361460
362296
  const fileFreq = /* @__PURE__ */ new Map();
361461
362297
  for (const p14 of patches) {
361462
362298
  for (const f4 of p14.files) {
361463
362299
  fileFreq.set(f4, (fileFreq.get(f4) ?? 0) + 1);
361464
362300
  }
361465
362301
  }
361466
- this.sharedFiles = new Set(
362302
+ const sharedFiles = new Set(
361467
362303
  Array.from(fileFreq.entries()).filter(([, n19]) => n19 > 1).map(([f4]) => f4)
361468
362304
  );
362305
+ this.accumulator = new MergeAccumulator(applyMode, sharedFiles);
361469
362306
  const results = [];
361470
362307
  for (let i9 = 0; i9 < patches.length; i9++) {
361471
362308
  const patch5 = patches[i9];
@@ -361496,11 +362333,11 @@ var require_dist14 = __commonJS({
361496
362333
  if (fileResult.status !== "conflict") continue;
361497
362334
  const originalPath = resolvedToOriginal.get(fileResult.file) ?? fileResult.file;
361498
362335
  if (laterFiles.has(fileResult.file) || laterFiles.has(originalPath)) {
361499
- const filePath = (0, import_node_path210.join)(this.outputDir, fileResult.file);
362336
+ const filePath = (0, import_node_path52.join)(this.outputDir, fileResult.file);
361500
362337
  try {
361501
- const content5 = await (0, import_promises195.readFile)(filePath, "utf-8");
362338
+ const content5 = await (0, import_promises310.readFile)(filePath, "utf-8");
361502
362339
  const stripped = stripConflictMarkers(content5);
361503
- await (0, import_promises195.writeFile)(filePath, stripped);
362340
+ await (0, import_promises310.writeFile)(filePath, stripped);
361504
362341
  } catch {
361505
362342
  }
361506
362343
  }
@@ -361520,7 +362357,7 @@ var require_dist14 = __commonJS({
361520
362357
  async populateAccumulatorForPatch(patch5, baseGen, currentTreeHash) {
361521
362358
  const fileResults = [];
361522
362359
  if (!baseGen) return fileResults;
361523
- const tempDir2 = await (0, import_promises195.mkdtemp)((0, import_node_path210.join)((0, import_node_os15.tmpdir)(), "replay-acc-"));
362360
+ const tempDir2 = await (0, import_promises310.mkdtemp)((0, import_node_path52.join)((0, import_node_os15.tmpdir)(), "replay-acc-"));
361524
362361
  const { GitClient: GitClient2 } = await Promise.resolve().then(() => (init_GitClient(), GitClient_exports));
361525
362362
  const tempGit = new GitClient2(tempDir2);
361526
362363
  await tempGit.exec(["init"]);
@@ -361532,15 +362369,12 @@ var require_dist14 = __commonJS({
361532
362369
  const resolvedPath = await this.resolveFilePath(filePath, baseGen.tree_hash, currentTreeHash);
361533
362370
  const base4 = await this.git.showFile(baseGen.tree_hash, filePath);
361534
362371
  const snapshotForFile = patch5.theirs_snapshot?.[resolvedPath] ?? patch5.theirs_snapshot?.[filePath];
361535
- const fileIsShared = this.sharedFiles.has(resolvedPath) || this.sharedFiles.has(filePath);
362372
+ const fileIsShared = this.accumulator.isShared(resolvedPath) || this.accumulator.isShared(filePath);
361536
362373
  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);
362374
+ const finalOnDisk = await (0, import_promises310.readFile)((0, import_node_path52.join)(this.outputDir, resolvedPath), "utf-8").catch(() => null);
361538
362375
  const effectiveTheirs = theirs ?? finalOnDisk;
361539
362376
  if (effectiveTheirs != null) {
361540
- this.fileTheirsAccumulator.set(resolvedPath, {
361541
- content: effectiveTheirs,
361542
- baseGeneration: patch5.base_generation
361543
- });
362377
+ this.accumulator.recordMerge(resolvedPath, effectiveTheirs, patch5.base_generation);
361544
362378
  }
361545
362379
  if (base4 != null && effectiveTheirs != null && finalOnDisk != null) {
361546
362380
  const dropped = computeDroppedContextLinesForFile(base4, effectiveTheirs, finalOnDisk);
@@ -361554,7 +362388,7 @@ var require_dist14 = __commonJS({
361554
362388
  }
361555
362389
  }
361556
362390
  } finally {
361557
- await (0, import_promises195.rm)(tempDir2, { recursive: true }).catch(() => {
362391
+ await (0, import_promises310.rm)(tempDir2, { recursive: true }).catch(() => {
361558
362392
  });
361559
362393
  }
361560
362394
  return fileResults;
@@ -361568,7 +362402,7 @@ var require_dist14 = __commonJS({
361568
362402
  patch5.files.map(async (f4) => {
361569
362403
  if (!baseGen) return false;
361570
362404
  const resolved = await this.resolveFilePath(f4, baseGen.tree_hash, currentTreeHash);
361571
- return this.fileTheirsAccumulator.has(resolved);
362405
+ return this.accumulator.has(resolved);
361572
362406
  })
361573
362407
  ).then((results) => results.some(Boolean));
361574
362408
  const resolvedFiles = {};
@@ -361583,8 +362417,8 @@ var require_dist14 = __commonJS({
361583
362417
  if (!needsAccumulation && !hasGeneratorRename) {
361584
362418
  const snapshots = /* @__PURE__ */ new Map();
361585
362419
  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));
362420
+ const fullPath = (0, import_node_path52.join)(this.outputDir, filePath);
362421
+ snapshots.set(filePath, await (0, import_promises310.readFile)(fullPath, "utf-8").catch(() => null));
361588
362422
  }
361589
362423
  try {
361590
362424
  await this.git.execWithInput(["apply", "--3way"], patch5.patch_content);
@@ -361598,11 +362432,11 @@ var require_dist14 = __commonJS({
361598
362432
  };
361599
362433
  } catch {
361600
362434
  for (const [filePath, content5] of snapshots) {
361601
- const fullPath = (0, import_node_path210.join)(this.outputDir, filePath);
362435
+ const fullPath = (0, import_node_path52.join)(this.outputDir, filePath);
361602
362436
  if (content5 != null) {
361603
- await (0, import_promises195.writeFile)(fullPath, content5);
362437
+ await (0, import_promises310.writeFile)(fullPath, content5);
361604
362438
  } else {
361605
- await (0, import_promises195.unlink)(fullPath).catch(() => {
362439
+ await (0, import_promises310.unlink)(fullPath).catch(() => {
361606
362440
  });
361607
362441
  }
361608
362442
  }
@@ -361613,7 +362447,7 @@ var require_dist14 = __commonJS({
361613
362447
  async applyWithThreeWayMerge(patch5) {
361614
362448
  const fileResults = [];
361615
362449
  const resolvedFiles = {};
361616
- const tempDir2 = await (0, import_promises195.mkdtemp)((0, import_node_path210.join)((0, import_node_os15.tmpdir)(), "replay-"));
362450
+ const tempDir2 = await (0, import_promises310.mkdtemp)((0, import_node_path52.join)((0, import_node_os15.tmpdir)(), "replay-"));
361617
362451
  const { GitClient: GitClient2 } = await Promise.resolve().then(() => (init_GitClient(), GitClient_exports));
361618
362452
  const tempGit = new GitClient2(tempDir2);
361619
362453
  await tempGit.exec(["init"]);
@@ -361636,7 +362470,7 @@ var require_dist14 = __commonJS({
361636
362470
  fileResults.push(result);
361637
362471
  }
361638
362472
  } finally {
361639
- await (0, import_promises195.rm)(tempDir2, { recursive: true }).catch(() => {
362473
+ await (0, import_promises310.rm)(tempDir2, { recursive: true }).catch(() => {
361640
362474
  });
361641
362475
  }
361642
362476
  const conflictFiles = fileResults.filter((r5) => r5.status === "conflict");
@@ -361653,214 +362487,37 @@ var require_dist14 = __commonJS({
361653
362487
  }
361654
362488
  async mergeFile(patch5, filePath, tempGit, tempDir2) {
361655
362489
  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
- };
362490
+ const inputs = await resolveInputs({
362491
+ patch: patch5,
362492
+ filePath,
362493
+ git: this.git,
362494
+ lockManager: this.lockManager,
362495
+ outputDir: this.outputDir,
362496
+ isTreeReachable: (h5) => this.isTreeReachable(h5),
362497
+ resolveBaseGeneration: (s9) => this.resolveBaseGeneration(s9),
362498
+ resolveFilePath: (f4, b8, c6) => this.resolveFilePath(f4, b8, c6),
362499
+ extractRenameSource: (p14, f4) => this.extractRenameSource(p14, f4),
362500
+ extractFileDiff: (p14, f4) => this.extractFileDiff(p14, f4)
362501
+ });
362502
+ if (inputs.earlyExit) {
362503
+ return { file: filePath, ...inputs.earlyExit };
362504
+ }
362505
+ const plan = await buildMergePlan({
362506
+ inputs,
362507
+ patch: patch5,
362508
+ filePath,
362509
+ accumulator: this.accumulator,
362510
+ tempGit,
362511
+ tempDir: tempDir2,
362512
+ applyPatchToContent: (b8, p14, f4, g8, d5, s9) => this.applyPatchToContent(b8, p14, f4, g8, d5, s9),
362513
+ isPatchAlreadyApplied: (p14, f4, c6, g8, d5) => this.isPatchAlreadyApplied(p14, f4, c6, g8, d5)
362514
+ });
362515
+ return await runMergeAndRecord({
362516
+ plan,
362517
+ inputs,
362518
+ patch: patch5,
362519
+ accumulator: this.accumulator
362520
+ });
361864
362521
  } catch (error50) {
361865
362522
  return {
361866
362523
  file: filePath,
@@ -361918,9 +362575,9 @@ var require_dist14 = __commonJS({
361918
362575
  if (!fileDiff) return null;
361919
362576
  try {
361920
362577
  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);
362578
+ const tempSourcePath = (0, import_node_path52.join)(tempDir2, sourceFilePath);
362579
+ await (0, import_promises310.mkdir)((0, import_node_path52.dirname)(tempSourcePath), { recursive: true });
362580
+ await (0, import_promises310.writeFile)(tempSourcePath, base4);
361924
362581
  await tempGit.exec(["add", sourceFilePath]);
361925
362582
  await tempGit.exec([
361926
362583
  "commit",
@@ -361929,16 +362586,16 @@ var require_dist14 = __commonJS({
361929
362586
  "--allow-empty"
361930
362587
  ]);
361931
362588
  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");
362589
+ const tempTargetPath = (0, import_node_path52.join)(tempDir2, filePath);
362590
+ return await (0, import_promises310.readFile)(tempTargetPath, "utf-8");
361934
362591
  }
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);
362592
+ const tempFilePath = (0, import_node_path52.join)(tempDir2, filePath);
362593
+ await (0, import_promises310.mkdir)((0, import_node_path52.dirname)(tempFilePath), { recursive: true });
362594
+ await (0, import_promises310.writeFile)(tempFilePath, base4);
361938
362595
  await tempGit.exec(["add", filePath]);
361939
362596
  await tempGit.exec(["commit", "-m", `base for ${filePath}`, "--allow-empty"]);
361940
362597
  await tempGit.execWithInput(["apply", "--allow-empty"], fileDiff);
361941
- return await (0, import_promises195.readFile)(tempFilePath, "utf-8");
362598
+ return await (0, import_promises310.readFile)(tempFilePath, "utf-8");
361942
362599
  } catch {
361943
362600
  return null;
361944
362601
  }
@@ -361954,9 +362611,9 @@ var require_dist14 = __commonJS({
361954
362611
  const fileDiff = this.extractFileDiff(patchContent, filePath);
361955
362612
  if (!fileDiff) return false;
361956
362613
  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);
362614
+ const tempFilePath = (0, import_node_path52.join)(tempDir2, filePath);
362615
+ await (0, import_promises310.mkdir)((0, import_node_path52.dirname)(tempFilePath), { recursive: true });
362616
+ await (0, import_promises310.writeFile)(tempFilePath, content5);
361960
362617
  await tempGit.exec(["add", filePath]);
361961
362618
  await tempGit.exec(["commit", "-m", `check already-applied ${filePath}`, "--allow-empty"]);
361962
362619
  await tempGit.execWithInput(["apply", "--reverse", "--check", "--allow-empty"], fileDiff);
@@ -362042,10 +362699,6 @@ var require_dist14 = __commonJS({
362042
362699
  return addedLines.join("\n") + (noTrailingNewline ? "" : "\n");
362043
362700
  }
362044
362701
  };
362045
- function isBinaryFile(filePath) {
362046
- const ext2 = (0, import_node_path210.extname)(filePath).toLowerCase();
362047
- return BINARY_EXTENSIONS2.has(ext2);
362048
- }
362049
362702
  function computeDroppedContextLinesForFile(base4, theirs, finalContent) {
362050
362703
  const baseLines = base4.split("\n");
362051
362704
  const theirsLines = theirs.split("\n");
@@ -362074,600 +362727,6 @@ var require_dist14 = __commonJS({
362074
362727
  const match3 = diffLine.match(/^diff --git a\/.+ b\/(.+)$/);
362075
362728
  return match3 !== null && match3[1] === filePath;
362076
362729
  }
362077
- var INFRASTRUCTURE_FILES = /* @__PURE__ */ new Set([".fernignore"]);
362078
- function parsePatchFileHeaders(patchContent) {
362079
- const results = [];
362080
- let currentPath = null;
362081
- let currentIsCreate = false;
362082
- let currentIsDelete = false;
362083
- const flush3 = () => {
362084
- if (currentPath !== null) {
362085
- results.push({ path: currentPath, isCreate: currentIsCreate, isDelete: currentIsDelete });
362086
- }
362087
- };
362088
- for (const line of patchContent.split("\n")) {
362089
- if (line.startsWith("diff --git ")) {
362090
- flush3();
362091
- currentPath = null;
362092
- currentIsCreate = false;
362093
- currentIsDelete = false;
362094
- const match3 = line.match(/^diff --git a\/(.+?) b\/(.+?)$/);
362095
- if (match3) {
362096
- currentPath = match3[2] ?? null;
362097
- }
362098
- } else if (currentPath !== null) {
362099
- if (line.startsWith("new file mode ")) {
362100
- currentIsCreate = true;
362101
- } else if (line.startsWith("deleted file mode ")) {
362102
- currentIsDelete = true;
362103
- }
362104
- }
362105
- }
362106
- flush3();
362107
- return results;
362108
- }
362109
- var ReplayDetector = class {
362110
- git;
362111
- lockManager;
362112
- sdkOutputDir;
362113
- warnings = [];
362114
- constructor(git, lockManager, sdkOutputDir) {
362115
- this.git = git;
362116
- this.lockManager = lockManager;
362117
- this.sdkOutputDir = sdkOutputDir;
362118
- }
362119
- async detectNewPatches() {
362120
- const lock = this.lockManager.read();
362121
- const lastGen = this.getLastGeneration(lock);
362122
- if (!lastGen) {
362123
- return { patches: [], revertedPatchIds: [] };
362124
- }
362125
- const exists2 = await this.git.commitExists(lastGen.commit_sha);
362126
- if (!exists2) {
362127
- this.warnings.push(
362128
- `Generation commit ${lastGen.commit_sha.slice(0, 7)} not found in git history. Falling back to alternate detection.`
362129
- );
362130
- return this.detectPatchesViaTreeDiff(
362131
- lastGen,
362132
- /* commitKnownMissing */
362133
- true
362134
- );
362135
- }
362136
- const isAncestor = await this.git.isAncestor(lastGen.commit_sha, "HEAD");
362137
- if (!isAncestor) {
362138
- return this.detectPatchesViaMergeBase(lastGen, lock);
362139
- }
362140
- return this.detectPatchesInRange(lastGen.commit_sha, lastGen, lock);
362141
- }
362142
- /**
362143
- * FER-10201 — Non-linear-history detection via `merge-base(prevGen, HEAD)`.
362144
- *
362145
- * After a squash-merge of a Fern-bot PR, `lastGen.commit_sha` (the previous
362146
- * `[fern-generated]`) is orphaned but still in git's object database. The
362147
- * merge-base is the commit on main from which the bot's branch was created —
362148
- * a reachable ancestor of HEAD. Walking that range and classifying each
362149
- * commit via `isGenerationCommit` mirrors the linear path and eliminates
362150
- * the bug class where pipeline-driven changes (autoversion, replay,
362151
- * generation) get bundled as customer customizations.
362152
- *
362153
- * Falls through to the legacy composite path when no common ancestor exists
362154
- * (truly disjoint history — orphan branches with no shared root).
362155
- */
362156
- async detectPatchesViaMergeBase(lastGen, lock) {
362157
- let mergeBase = "";
362158
- try {
362159
- mergeBase = (await this.git.exec(["merge-base", lastGen.commit_sha, "HEAD"])).trim();
362160
- } catch {
362161
- }
362162
- if (!mergeBase) {
362163
- this.warnings.push(
362164
- `No common ancestor between previous generation ${lastGen.commit_sha.slice(0, 7)} and HEAD. Falling back to composite tree-diff detection.`
362165
- );
362166
- return this.detectPatchesViaTreeDiff(
362167
- lastGen,
362168
- /* commitKnownMissing */
362169
- false
362170
- );
362171
- }
362172
- return this.detectPatchesInRange(mergeBase, lastGen, lock);
362173
- }
362174
- /**
362175
- * FER-10201 — Walk `${rangeStart}..HEAD`, classify each commit, emit one
362176
- * `StoredPatch` per customer commit. Body shared by the linear path
362177
- * (rangeStart = lastGen.commit_sha) and the merge-base path.
362178
- *
362179
- * Patch `base_generation` is always `lastGen.commit_sha` — the
362180
- * lockfile-tracked reference the applicator uses to fetch base file
362181
- * content, regardless of where the walk actually started.
362182
- */
362183
- async detectPatchesInRange(rangeStart, lastGen, lock) {
362184
- const log4 = await this.git.exec([
362185
- "log",
362186
- "--first-parent",
362187
- "--format=%H%x00%an%x00%ae%x00%s",
362188
- `${rangeStart}..HEAD`,
362189
- "--",
362190
- this.sdkOutputDir
362191
- ]);
362192
- if (!log4.trim()) {
362193
- return { patches: [], revertedPatchIds: [] };
362194
- }
362195
- const commits = this.parseGitLog(log4);
362196
- const newPatches = [];
362197
- const forgottenHashes = new Set(lock.forgotten_hashes ?? []);
362198
- for (const commit of commits) {
362199
- if (isGenerationCommit(commit)) {
362200
- continue;
362201
- }
362202
- if (lock.patches.find((p14) => p14.original_commit === commit.sha)) {
362203
- continue;
362204
- }
362205
- const parents = await this.git.getCommitParents(commit.sha);
362206
- if (parents.length > 1) {
362207
- const compositePatch = await this.createMergeCompositePatch({
362208
- mergeCommit: commit,
362209
- firstParent: parents[0],
362210
- lastGen,
362211
- lock
362212
- });
362213
- if (compositePatch) {
362214
- newPatches.push(compositePatch);
362215
- }
362216
- continue;
362217
- }
362218
- let patchContent;
362219
- try {
362220
- patchContent = await this.git.formatPatch(commit.sha);
362221
- } catch {
362222
- this.warnings.push(
362223
- `Could not generate patch for commit ${commit.sha.slice(0, 7)} \u2014 it may be unreachable in a shallow clone. Skipping.`
362224
- );
362225
- continue;
362226
- }
362227
- let contentHash = this.computeContentHash(patchContent);
362228
- if (lock.patches.find((p14) => p14.content_hash === contentHash) || forgottenHashes.has(contentHash)) {
362229
- continue;
362230
- }
362231
- 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));
362235
- if (sensitiveFiles.length > 0) {
362236
- this.warnings.push(
362237
- `Sensitive file(s) excluded from patch for commit ${commit.sha.slice(0, 7)}: ${sensitiveFiles.join(", ")}. These files will not be tracked by replay.`
362238
- );
362239
- if (files.length > 0) {
362240
- const parentSha = parents[0];
362241
- if (parentSha) {
362242
- try {
362243
- patchContent = await this.git.exec(["diff", parentSha, commit.sha, "--", ...files]);
362244
- } catch {
362245
- continue;
362246
- }
362247
- if (!patchContent.trim()) continue;
362248
- contentHash = this.computeContentHash(patchContent);
362249
- }
362250
- }
362251
- }
362252
- if (files.length === 0) {
362253
- continue;
362254
- }
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
- }
362261
- newPatches.push({
362262
- id: `patch-${commit.sha.slice(0, 8)}`,
362263
- content_hash: contentHash,
362264
- original_commit: commit.sha,
362265
- original_message: commit.message,
362266
- original_author: `${commit.authorName} <${commit.authorEmail}>`,
362267
- base_generation: lastGen.commit_sha,
362268
- files,
362269
- patch_content: patchContent,
362270
- ...Object.keys(theirsSnapshot).length > 0 ? { theirs_snapshot: theirsSnapshot } : {},
362271
- ...this.isCreationOnlyPatch(patchContent) ? { user_owned: true } : {}
362272
- });
362273
- }
362274
- const survivingPatches = await this.collapseNetZeroFiles(newPatches);
362275
- survivingPatches.reverse();
362276
- const revertedPatchIdSet = /* @__PURE__ */ new Set();
362277
- const revertIndicesToRemove = /* @__PURE__ */ new Set();
362278
- for (let i9 = 0; i9 < survivingPatches.length; i9++) {
362279
- const patch5 = survivingPatches[i9];
362280
- if (!isRevertCommit(patch5.original_message)) continue;
362281
- let body = "";
362282
- try {
362283
- body = await this.git.getCommitBody(patch5.original_commit);
362284
- } catch {
362285
- }
362286
- const revertedSha = parseRevertedSha(body);
362287
- const revertedMessage = parseRevertedMessage(patch5.original_message);
362288
- let matchedExisting = false;
362289
- if (revertedSha) {
362290
- const existing = lock.patches.find((p14) => p14.original_commit === revertedSha);
362291
- if (existing) {
362292
- revertedPatchIdSet.add(existing.id);
362293
- revertIndicesToRemove.add(i9);
362294
- matchedExisting = true;
362295
- }
362296
- }
362297
- if (!matchedExisting && revertedMessage) {
362298
- const existing = lock.patches.find((p14) => p14.original_message === revertedMessage);
362299
- if (existing) {
362300
- revertedPatchIdSet.add(existing.id);
362301
- revertIndicesToRemove.add(i9);
362302
- matchedExisting = true;
362303
- }
362304
- }
362305
- if (matchedExisting) continue;
362306
- let matchedNew = false;
362307
- if (revertedSha) {
362308
- const idx = survivingPatches.findIndex(
362309
- (p14, j7) => j7 !== i9 && !revertIndicesToRemove.has(j7) && p14.original_commit === revertedSha
362310
- );
362311
- if (idx !== -1) {
362312
- revertIndicesToRemove.add(i9);
362313
- revertIndicesToRemove.add(idx);
362314
- matchedNew = true;
362315
- }
362316
- }
362317
- if (!matchedNew && revertedMessage) {
362318
- const idx = survivingPatches.findIndex(
362319
- (p14, j7) => j7 !== i9 && !revertIndicesToRemove.has(j7) && p14.original_message === revertedMessage
362320
- );
362321
- if (idx !== -1) {
362322
- revertIndicesToRemove.add(i9);
362323
- revertIndicesToRemove.add(idx);
362324
- }
362325
- }
362326
- if (!matchedExisting && !matchedNew) {
362327
- revertIndicesToRemove.add(i9);
362328
- }
362329
- }
362330
- const filteredPatches = survivingPatches.filter((_7, i9) => !revertIndicesToRemove.has(i9));
362331
- return { patches: filteredPatches, revertedPatchIds: [...revertedPatchIdSet] };
362332
- }
362333
- /**
362334
- * Compute content hash for deduplication.
362335
- *
362336
- * Produces a format-agnostic hash so both `git format-patch` output
362337
- * (email-wrapped) and plain `git diff` output hash to the same value
362338
- * when the underlying diff hunks are identical.
362339
- *
362340
- * Normalization:
362341
- * 1. Strip email wrapper: everything before the first `diff --git` line
362342
- * (From:, Subject:, Date:, diffstat, blank separators).
362343
- * 2. Strip `index` lines (blob SHA pairs change across rebases).
362344
- * 3. Strip trailing git version marker (`-- \n<version>`).
362345
- *
362346
- * This ensures content hashes match across the no-patches and normal-
362347
- * regeneration flows, which store formatPatch and git-diff formats
362348
- * respectively (FER-9850, D5-D9).
362349
- */
362350
- computeContentHash(patchContent) {
362351
- const lines = patchContent.split("\n");
362352
- const diffStart = lines.findIndex((l9) => l9.startsWith("diff --git "));
362353
- const relevantLines = diffStart > 0 ? lines.slice(diffStart) : lines;
362354
- const normalized = relevantLines.filter((line) => !line.startsWith("index ")).join("\n").replace(/\n-- \n[\d]+\.[\d]+[^\n]*\s*$/, "");
362355
- return `sha256:${(0, import_node_crypto7.createHash)("sha256").update(normalized).digest("hex")}`;
362356
- }
362357
- /**
362358
- * FER-9805 — Drop patches whose files were transiently created and then
362359
- * deleted within the current linear detection window, and are absent from
362360
- * HEAD at the end of the window.
362361
- *
362362
- * Rationale: on a merge commit, createMergeCompositePatch already diffs
362363
- * firstParent..mergeCommit so net-zero files never enter the composite. On
362364
- * linear history each commit becomes its own patch, so a file that was
362365
- * briefly added and later removed survives as a create+delete pair whose
362366
- * cumulative diff is empty. We drop such patches before they reach the
362367
- * lockfile.
362368
- *
362369
- * A file is "transient" when:
362370
- * 1. some patch in the window sets `new file mode` for it (a creation), AND
362371
- * 2. some patch in the window sets `deleted file mode` for it (a deletion), AND
362372
- * 3. it is absent from HEAD's tree.
362373
- *
362374
- * The HEAD guard (#3) prevents false positives when a file is deleted
362375
- * and then recreated with different content — the cumulative diff of
362376
- * such a pair is non-empty and must be preserved.
362377
- *
362378
- * This only drops patches whose `files` are a subset of the transient
362379
- * set. A partial-transient patch (one that touches a transient file
362380
- * alongside a persistent one) is left unmodified; surgical stripping of
362381
- * single files from a multi-file patch would require a follow-up that
362382
- * regenerates the patch content via `git format-patch -- <files>`. No
362383
- * 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.
362386
- */
362387
- async collapseNetZeroFiles(patches) {
362388
- if (patches.length === 0) return patches;
362389
- const stateByFile = /* @__PURE__ */ new Map();
362390
- for (const patch5 of patches) {
362391
- for (const header of parsePatchFileHeaders(patch5.patch_content)) {
362392
- if (!header.isCreate && !header.isDelete) continue;
362393
- const prior = stateByFile.get(header.path) ?? { created: false, deleted: false };
362394
- if (header.isCreate) prior.created = true;
362395
- if (header.isDelete) prior.deleted = true;
362396
- stateByFile.set(header.path, prior);
362397
- }
362398
- }
362399
- let anyCandidate = false;
362400
- for (const state of stateByFile.values()) {
362401
- if (state.created && state.deleted) {
362402
- anyCandidate = true;
362403
- break;
362404
- }
362405
- }
362406
- if (!anyCandidate) return patches;
362407
- const headFiles = await this.listHeadFiles();
362408
- const transient = /* @__PURE__ */ new Set();
362409
- for (const [file4, state] of stateByFile) {
362410
- if (state.created && state.deleted && !headFiles.has(file4)) {
362411
- transient.add(file4);
362412
- }
362413
- }
362414
- if (transient.size === 0) return patches;
362415
- return patches.filter((patch5) => !patch5.files.every((f4) => transient.has(f4)));
362416
- }
362417
- /**
362418
- * List every tracked path at HEAD (one `git ls-tree -r` invocation).
362419
- * Returns an empty Set if HEAD is unborn or the invocation fails — the
362420
- * caller then treats every candidate as "not in HEAD" and may collapse
362421
- * more aggressively. On typical SDK repos this is a single sub-10ms call.
362422
- */
362423
- async listHeadFiles() {
362424
- try {
362425
- const out = await this.git.exec(["ls-tree", "-r", "--name-only", "HEAD"]);
362426
- return new Set(out.split("\n").map((l9) => l9.trim()).filter(Boolean));
362427
- } catch {
362428
- return /* @__PURE__ */ new Set();
362429
- }
362430
- }
362431
- /**
362432
- * Create a single composite patch for a merge commit by diffing the merge
362433
- * commit against its first parent. This captures the net change of the
362434
- * entire merged branch as one patch, eliminating accumulator chaining and
362435
- * zero-net-change ghost conflicts.
362436
- */
362437
- async createMergeCompositePatch(input3) {
362438
- const { mergeCommit, firstParent, lastGen, lock } = input3;
362439
- const forgottenHashes = new Set(lock.forgotten_hashes ?? []);
362440
- const filesOutput = await this.git.exec([
362441
- "diff",
362442
- "--name-only",
362443
- firstParent,
362444
- mergeCommit.sha
362445
- ]);
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) {
362450
- 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.`
362452
- );
362453
- }
362454
- if (files.length === 0) return null;
362455
- const diff2 = await this.git.exec([
362456
- "diff",
362457
- firstParent,
362458
- mergeCommit.sha,
362459
- "--",
362460
- ...files
362461
- ]);
362462
- if (!diff2.trim()) return null;
362463
- const contentHash = this.computeContentHash(diff2);
362464
- if (lock.patches.some((p14) => p14.content_hash === contentHash) || forgottenHashes.has(contentHash)) {
362465
- return null;
362466
- }
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
- }
362473
- return {
362474
- id: `patch-merge-${mergeCommit.sha.slice(0, 8)}`,
362475
- content_hash: contentHash,
362476
- original_commit: mergeCommit.sha,
362477
- original_message: mergeCommit.message,
362478
- original_author: `${mergeCommit.authorName} <${mergeCommit.authorEmail}>`,
362479
- base_generation: lastGen.commit_sha,
362480
- files,
362481
- patch_content: diff2,
362482
- ...Object.keys(theirsSnapshot).length > 0 ? { theirs_snapshot: theirsSnapshot } : {},
362483
- ...this.isCreationOnlyPatch(diff2) ? { user_owned: true } : {}
362484
- };
362485
- }
362486
- /**
362487
- * Detect patches via tree diff for non-linear history. Returns a composite patch.
362488
- * Revert reconciliation is skipped here because tree-diff produces a single composite
362489
- * patch from the aggregate diff — individual revert commits are not distinguishable.
362490
- */
362491
- async detectPatchesViaTreeDiff(lastGen, commitKnownMissing) {
362492
- const diffBase = await this.resolveDiffBase(lastGen, commitKnownMissing);
362493
- if (!diffBase) {
362494
- return this.detectPatchesViaCommitScan();
362495
- }
362496
- 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) {
362501
- this.warnings.push(
362502
- `Sensitive file(s) excluded from composite patch: ${sensitiveTreeFiles.join(", ")}. These files will not be tracked by replay.`
362503
- );
362504
- }
362505
- if (files.length === 0) return { patches: [], revertedPatchIds: [] };
362506
- const diff2 = await this.git.exec(["diff", diffBase, "HEAD", "--", ...files]);
362507
- if (!diff2.trim()) return { patches: [], revertedPatchIds: [] };
362508
- const contentHash = this.computeContentHash(diff2);
362509
- const lock = this.lockManager.read();
362510
- if (lock.patches.some((p14) => p14.content_hash === contentHash) || (lock.forgotten_hashes ?? []).includes(contentHash)) {
362511
- return { patches: [], revertedPatchIds: [] };
362512
- }
362513
- 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
- }
362520
- const compositePatch = {
362521
- id: `patch-composite-${headSha.slice(0, 8)}`,
362522
- content_hash: contentHash,
362523
- original_commit: headSha,
362524
- original_message: "Customer customizations (composite)",
362525
- 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,
362529
- files,
362530
- patch_content: diff2,
362531
- ...Object.keys(theirsSnapshot).length > 0 ? { theirs_snapshot: theirsSnapshot } : {},
362532
- ...this.isCreationOnlyPatch(diff2) ? { user_owned: true } : {}
362533
- };
362534
- return { patches: [compositePatch], revertedPatchIds: [] };
362535
- }
362536
- /**
362537
- * Last-resort detection when both generation commit and tree are unreachable.
362538
- * Scans all commits from HEAD, filters against known lockfile patches, and
362539
- * skips creation-only commits (squashed history after force push).
362540
- *
362541
- * Detected patches use the commit's parent as base_generation so the applicator
362542
- * can find base file content from the parent's tree (which IS reachable).
362543
- */
362544
- async detectPatchesViaCommitScan() {
362545
- const lock = this.lockManager.read();
362546
- const log4 = await this.git.exec([
362547
- "log",
362548
- "--max-count=200",
362549
- "--format=%H%x00%an%x00%ae%x00%s",
362550
- "HEAD",
362551
- "--",
362552
- this.sdkOutputDir
362553
- ]);
362554
- if (!log4.trim()) {
362555
- return { patches: [], revertedPatchIds: [] };
362556
- }
362557
- const commits = this.parseGitLog(log4);
362558
- const newPatches = [];
362559
- const forgottenHashes = new Set(lock.forgotten_hashes ?? []);
362560
- const existingHashes = new Set(lock.patches.map((p14) => p14.content_hash));
362561
- const existingCommits = new Set(lock.patches.map((p14) => p14.original_commit));
362562
- for (const commit of commits) {
362563
- if (isGenerationCommit(commit)) {
362564
- continue;
362565
- }
362566
- const parents = await this.git.getCommitParents(commit.sha);
362567
- if (parents.length > 1) {
362568
- continue;
362569
- }
362570
- if (existingCommits.has(commit.sha)) {
362571
- continue;
362572
- }
362573
- let patchContent;
362574
- try {
362575
- patchContent = await this.git.formatPatch(commit.sha);
362576
- } catch {
362577
- continue;
362578
- }
362579
- let contentHash = this.computeContentHash(patchContent);
362580
- if (existingHashes.has(contentHash) || forgottenHashes.has(contentHash)) {
362581
- continue;
362582
- }
362583
- if (this.isCreationOnlyPatch(patchContent)) {
362584
- continue;
362585
- }
362586
- 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) {
362591
- 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.`
362593
- );
362594
- if (files.length > 0) {
362595
- if (parents.length === 0) {
362596
- continue;
362597
- }
362598
- try {
362599
- patchContent = await this.git.exec(["diff", parents[0], commit.sha, "--", ...files]);
362600
- } catch {
362601
- continue;
362602
- }
362603
- if (!patchContent.trim()) continue;
362604
- contentHash = this.computeContentHash(patchContent);
362605
- }
362606
- }
362607
- if (files.length === 0) {
362608
- continue;
362609
- }
362610
- if (parents.length === 0) {
362611
- continue;
362612
- }
362613
- 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;
362619
- }
362620
- newPatches.push({
362621
- id: `patch-${commit.sha.slice(0, 8)}`,
362622
- content_hash: contentHash,
362623
- original_commit: commit.sha,
362624
- original_message: commit.message,
362625
- original_author: `${commit.authorName} <${commit.authorEmail}>`,
362626
- base_generation: parentSha,
362627
- files,
362628
- patch_content: patchContent,
362629
- ...Object.keys(theirsSnapshot).length > 0 ? { theirs_snapshot: theirsSnapshot } : {}
362630
- });
362631
- }
362632
- newPatches.reverse();
362633
- return { patches: newPatches, revertedPatchIds: [] };
362634
- }
362635
- /**
362636
- * Check if a format-patch consists entirely of new-file creations.
362637
- * Used to identify squashed commits after force push, which create all files
362638
- * from scratch (--- /dev/null) rather than modifying existing files.
362639
- */
362640
- isCreationOnlyPatch(patchContent) {
362641
- const diffOldHeaders = patchContent.split("\n").filter((l9) => l9.startsWith("--- "));
362642
- if (diffOldHeaders.length === 0) {
362643
- return false;
362644
- }
362645
- return diffOldHeaders.every((l9) => l9 === "--- /dev/null");
362646
- }
362647
- /**
362648
- * Resolve the best available diff base for a generation record.
362649
- * Prefers commit_sha, falls back to tree_hash for unreachable commits.
362650
- * When commitKnownMissing is true, skips the redundant commitExists check.
362651
- */
362652
- async resolveDiffBase(gen, commitKnownMissing) {
362653
- if (!commitKnownMissing && await this.git.commitExists(gen.commit_sha)) {
362654
- return gen.commit_sha;
362655
- }
362656
- if (await this.git.treeExists(gen.tree_hash)) {
362657
- return gen.tree_hash;
362658
- }
362659
- return null;
362660
- }
362661
- parseGitLog(log4) {
362662
- return log4.trim().split("\n").map((line) => {
362663
- const [sha, authorName, authorEmail, message] = line.split("\0");
362664
- return { sha, authorName, authorEmail, message };
362665
- });
362666
- }
362667
- getLastGeneration(lock) {
362668
- return lock.generations.find((g8) => g8.commit_sha === lock.current_generation);
362669
- }
362670
- };
362671
362730
  var ReplayCommitter = class {
362672
362731
  git;
362673
362732
  outputDir;
@@ -362770,13 +362829,13 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
362770
362829
  }
362771
362830
  };
362772
362831
  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();
362832
+ var import_promises610 = require("fs/promises");
362833
+ var import_node_path82 = require("path");
362834
+ var import_minimatch32 = require_commonjs3();
362776
362835
  init_GitClient();
362777
- var import_promises210 = require("fs/promises");
362836
+ var import_promises410 = require("fs/promises");
362778
362837
  var import_node_os22 = require("os");
362779
- var import_node_path310 = require("path");
362838
+ var import_node_path62 = require("path");
362780
362839
  var import_node_child_process8 = require("child_process");
362781
362840
  init_HybridReconstruction();
362782
362841
  function normalizeHunkBody(hunk) {
@@ -362983,17 +363042,17 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
362983
363042
  }
362984
363043
  async function unifiedDiff(beforeContent, afterContent, filePath) {
362985
363044
  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-"));
363045
+ const tmp18 = await (0, import_promises410.mkdtemp)((0, import_node_path62.join)((0, import_node_os22.tmpdir)(), "fern-replay-pp-"));
362987
363046
  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");
363047
+ const beforePath = (0, import_node_path62.join)(tmp18, "before");
363048
+ const afterPath = (0, import_node_path62.join)(tmp18, "after");
363049
+ await (0, import_promises410.writeFile)(beforePath, beforeContent, "utf-8");
363050
+ await (0, import_promises410.writeFile)(afterPath, afterContent, "utf-8");
362992
363051
  const raw = await runGitDiffNoIndex(beforePath, afterPath);
362993
363052
  if (!raw.trim()) return "";
362994
363053
  return rewriteDiffHeaders(raw, filePath);
362995
363054
  } finally {
362996
- await (0, import_promises210.rm)(tmp18, { recursive: true, force: true });
363055
+ await (0, import_promises410.rm)(tmp18, { recursive: true, force: true });
362997
363056
  }
362998
363057
  }
362999
363058
  function runGitDiffNoIndex(beforePath, afterPath) {
@@ -363064,27 +363123,513 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
363064
363123
  const trailer = hasTrailingNewline ? "" : "\n\";
363065
363124
  return header + "\n" + body + trailer + "\n";
363066
363125
  }
363126
+ var import_minimatch22 = require_commonjs3();
363127
+ var FileOwnership = class {
363128
+ constructor(git) {
363129
+ this.git = git;
363130
+ }
363131
+ /**
363132
+ * Canonical resolution. Returns true if `file` is user-owned per:
363133
+ *
363134
+ * 1. `patch.user_owned === true` — authoritative, persisted flag.
363135
+ * 2. `file === ".fernignore"` — always user-owned.
363136
+ * 3. File matches a `.fernignore` pattern (via minimatch).
363137
+ * 4. Patch content shows `--- /dev/null` for this file (legacy-safe
363138
+ * signal for patches predating `user_owned` or whose flag was lost).
363139
+ * 5. Tree-based check against `patch.base_generation` when reachable
363140
+ * AND distinct from `currentGenSha`. Unreachable base → conservative
363141
+ * user-owned (silent-absorption safety net), with a one-time warning
363142
+ * per base SHA.
363143
+ * 6. Final fallback: check `currentGenSha` when no usable base exists
363144
+ * (legacy one-gen lockfile).
363145
+ */
363146
+ async resolveCanonical(ctx) {
363147
+ const { file: file4, originalFileName, patch: patch5, currentGenSha, fernignorePatterns, warnedGens, warnings } = ctx;
363148
+ if (patch5.user_owned) return true;
363149
+ if (file4 === ".fernignore") return true;
363150
+ if (fernignorePatterns.some((p14) => file4 === p14 || (0, import_minimatch22.minimatch)(file4, p14))) return true;
363151
+ if (fileIsCreationInPatchContent(patch5.patch_content, originalFileName ?? file4)) {
363152
+ return true;
363153
+ }
363154
+ const base4 = patch5.base_generation;
363155
+ if (base4 && base4 !== currentGenSha) {
363156
+ const reachable = await this.git.commitExists(base4) || await this.git.treeExists(base4);
363157
+ if (!reachable) {
363158
+ if (!warnedGens.has(base4)) {
363159
+ warnedGens.add(base4);
363160
+ warnings.push(
363161
+ `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.`
363162
+ );
363163
+ }
363164
+ return true;
363165
+ }
363166
+ return await this.git.showFile(base4, originalFileName ?? file4) === null;
363167
+ }
363168
+ return await this.git.showFile(currentGenSha, originalFileName ?? file4) === null;
363169
+ }
363170
+ };
363171
+ function fileIsCreationInPatchContent(patchContent, file4) {
363172
+ const lines = patchContent.split("\n");
363173
+ let inFileSection = false;
363174
+ for (const line of lines) {
363175
+ if (line.startsWith("diff --git ")) {
363176
+ inFileSection = line.includes(` b/${file4}`) || line.endsWith(` b/${file4}`);
363177
+ continue;
363178
+ }
363179
+ if (inFileSection && line.startsWith("--- ")) {
363180
+ return line === "--- /dev/null";
363181
+ }
363182
+ }
363183
+ return false;
363184
+ }
363185
+ var FirstGenerationFlow = class {
363186
+ constructor(service) {
363187
+ this.service = service;
363188
+ }
363189
+ async prepare(options2) {
363190
+ if (options2?.dryRun) {
363191
+ const headSha = await this.service.git.exec(["rev-parse", "HEAD"]).then((s9) => s9.trim()).catch(() => "");
363192
+ return {
363193
+ flow: "first-generation",
363194
+ previousGenerationSha: null,
363195
+ currentGenerationSha: headSha,
363196
+ baseBranchHead: options2.baseBranchHead ?? null,
363197
+ _prepared: {
363198
+ kind: "terminal",
363199
+ flow: "first-generation",
363200
+ preparedReport: firstGenerationReport()
363201
+ }
363202
+ };
363203
+ }
363204
+ const commitOpts = options2 ? {
363205
+ cliVersion: options2.cliVersion ?? "unknown",
363206
+ generatorVersions: options2.generatorVersions ?? {},
363207
+ baseBranchHead: options2.baseBranchHead
363208
+ } : void 0;
363209
+ await this.service.committer.commitGeneration("Initial SDK generation", commitOpts);
363210
+ const genRecord = await this.service.committer.createGenerationRecord(commitOpts);
363211
+ this.service.lockManager.initializeInMemory(genRecord);
363212
+ return {
363213
+ flow: "first-generation",
363214
+ previousGenerationSha: null,
363215
+ currentGenerationSha: genRecord.commit_sha,
363216
+ baseBranchHead: options2?.baseBranchHead ?? null,
363217
+ _prepared: {
363218
+ kind: "active",
363219
+ flow: "first-generation",
363220
+ patchesToApply: [],
363221
+ newPatchIds: /* @__PURE__ */ new Set(),
363222
+ detectorWarnings: [],
363223
+ revertedCount: 0,
363224
+ genSha: genRecord.commit_sha,
363225
+ optionsSnapshot: options2
363226
+ }
363227
+ };
363228
+ }
363229
+ async apply(_prep) {
363230
+ this.service.lockManager.save();
363231
+ return firstGenerationReport();
363232
+ }
363233
+ };
363234
+ function firstGenerationReport() {
363235
+ return {
363236
+ flow: "first-generation",
363237
+ patchesDetected: 0,
363238
+ patchesApplied: 0,
363239
+ patchesWithConflicts: 0,
363240
+ patchesSkipped: 0,
363241
+ conflicts: []
363242
+ };
363243
+ }
363244
+ var SkipApplicationFlow = class {
363245
+ constructor(service) {
363246
+ this.service = service;
363247
+ }
363248
+ async prepare(options2) {
363249
+ const commitOpts = options2 ? {
363250
+ cliVersion: options2.cliVersion ?? "unknown",
363251
+ generatorVersions: options2.generatorVersions ?? {},
363252
+ baseBranchHead: options2.baseBranchHead
363253
+ } : void 0;
363254
+ await this.service.committer.commitGeneration("Update SDK (replay skipped)", commitOpts);
363255
+ await this.service.cleanupStaleConflictMarkers();
363256
+ const genRecord = await this.service.committer.createGenerationRecord(commitOpts);
363257
+ let previousGenerationSha = null;
363258
+ try {
363259
+ const lock = this.service.lockManager.read();
363260
+ previousGenerationSha = lock.current_generation ?? null;
363261
+ this.service.lockManager.addGeneration(genRecord);
363262
+ } catch (error50) {
363263
+ if (error50 instanceof LockfileNotFoundError) {
363264
+ this.service.lockManager.initializeInMemory(genRecord);
363265
+ } else {
363266
+ throw error50;
363267
+ }
363268
+ }
363269
+ this.service.lockManager.setReplaySkippedAt((/* @__PURE__ */ new Date()).toISOString());
363270
+ return {
363271
+ flow: "skip-application",
363272
+ previousGenerationSha,
363273
+ currentGenerationSha: genRecord.commit_sha,
363274
+ baseBranchHead: options2?.baseBranchHead ?? null,
363275
+ _prepared: {
363276
+ kind: "active",
363277
+ flow: "skip-application",
363278
+ patchesToApply: [],
363279
+ newPatchIds: /* @__PURE__ */ new Set(),
363280
+ detectorWarnings: [],
363281
+ revertedCount: 0,
363282
+ genSha: genRecord.commit_sha,
363283
+ optionsSnapshot: options2
363284
+ }
363285
+ };
363286
+ }
363287
+ async apply(_prep, options2) {
363288
+ this.service.lockManager.save();
363289
+ const credentialWarnings = [...this.service.lockManager.warnings];
363290
+ if (!options2?.stageOnly) {
363291
+ await this.service.committer.commitReplay(0);
363292
+ } else {
363293
+ await this.service.committer.stageAll();
363294
+ }
363295
+ return {
363296
+ flow: "skip-application",
363297
+ patchesDetected: 0,
363298
+ patchesApplied: 0,
363299
+ patchesWithConflicts: 0,
363300
+ patchesSkipped: 0,
363301
+ conflicts: [],
363302
+ warnings: credentialWarnings.length > 0 ? credentialWarnings : void 0
363303
+ };
363304
+ }
363305
+ };
363306
+ var NoPatchesFlow = class {
363307
+ constructor(service) {
363308
+ this.service = service;
363309
+ }
363310
+ async prepare(options2) {
363311
+ const preLock = this.service.lockManager.read();
363312
+ const previousGenerationSha = preLock.current_generation ?? null;
363313
+ let { patches: newPatches, revertedPatchIds } = await this.service.detector.detectNewPatches();
363314
+ const detectorWarnings = [...this.service.detector.warnings];
363315
+ if (options2?.dryRun) {
363316
+ const dryRunWarnings = [...detectorWarnings, ...this.service.warnings];
363317
+ const preparedReport = {
363318
+ flow: "no-patches",
363319
+ patchesDetected: newPatches.length,
363320
+ patchesApplied: 0,
363321
+ patchesWithConflicts: 0,
363322
+ patchesSkipped: 0,
363323
+ patchesReverted: revertedPatchIds.length,
363324
+ conflicts: [],
363325
+ wouldApply: newPatches,
363326
+ warnings: dryRunWarnings.length > 0 ? dryRunWarnings : void 0
363327
+ };
363328
+ const headSha = await this.service.git.exec(["rev-parse", "HEAD"]).then((s9) => s9.trim()).catch(() => "");
363329
+ return {
363330
+ flow: "no-patches",
363331
+ previousGenerationSha,
363332
+ currentGenerationSha: headSha,
363333
+ baseBranchHead: options2.baseBranchHead ?? null,
363334
+ _prepared: {
363335
+ kind: "terminal",
363336
+ flow: "no-patches",
363337
+ preparedReport
363338
+ }
363339
+ };
363340
+ }
363341
+ {
363342
+ const preCurrentGen = preLock.current_generation;
363343
+ const uniqueFiles = [...new Set(newPatches.flatMap((p14) => p14.files))];
363344
+ const absorbedFiles = /* @__PURE__ */ new Set();
363345
+ for (const file4 of uniqueFiles) {
363346
+ const diff2 = await this.service.git.exec(["diff", preCurrentGen, "HEAD", "--", file4]).catch(() => null);
363347
+ if (diff2 !== null && !diff2.trim()) {
363348
+ absorbedFiles.add(file4);
363349
+ }
363350
+ }
363351
+ if (absorbedFiles.size > 0) {
363352
+ newPatches = newPatches.filter(
363353
+ (p14) => p14.files.length === 0 || !p14.files.every((f4) => absorbedFiles.has(f4))
363354
+ );
363355
+ }
363356
+ }
363357
+ for (const id2 of revertedPatchIds) {
363358
+ try {
363359
+ this.service.lockManager.removePatch(id2);
363360
+ } catch {
363361
+ }
363362
+ }
363363
+ const commitOpts = options2 ? {
363364
+ cliVersion: options2.cliVersion ?? "unknown",
363365
+ generatorVersions: options2.generatorVersions ?? {},
363366
+ baseBranchHead: options2.baseBranchHead
363367
+ } : void 0;
363368
+ await this.service.committer.commitGeneration("Update SDK", commitOpts);
363369
+ await this.service.cleanupStaleConflictMarkers();
363370
+ const genRecord = await this.service.committer.createGenerationRecord(commitOpts);
363371
+ this.service.lockManager.addGeneration(genRecord);
363372
+ return {
363373
+ flow: "no-patches",
363374
+ previousGenerationSha,
363375
+ currentGenerationSha: genRecord.commit_sha,
363376
+ baseBranchHead: options2?.baseBranchHead ?? null,
363377
+ _prepared: {
363378
+ kind: "active",
363379
+ flow: "no-patches",
363380
+ patchesToApply: newPatches,
363381
+ newPatchIds: new Set(newPatches.map((p14) => p14.id)),
363382
+ detectorWarnings,
363383
+ revertedCount: revertedPatchIds.length,
363384
+ genSha: genRecord.commit_sha,
363385
+ optionsSnapshot: options2
363386
+ }
363387
+ };
363388
+ }
363389
+ async apply(prep, options2) {
363390
+ assertActive(prep);
363391
+ const newPatches = prep._prepared.patchesToApply;
363392
+ const detectorWarnings = prep._prepared.detectorWarnings;
363393
+ const genSha = prep._prepared.genSha;
363394
+ let results = [];
363395
+ if (newPatches.length > 0) {
363396
+ results = await this.service.applicator.applyPatches(newPatches);
363397
+ this.service._lastApplyResults = results;
363398
+ this.service.recordDroppedContextLineWarnings(results);
363399
+ this.service.revertConflictingFiles(results);
363400
+ for (const result of results) {
363401
+ if (result.status === "conflict") {
363402
+ result.patch.status = "unresolved";
363403
+ }
363404
+ }
363405
+ }
363406
+ const rebaseCounts = await this.service.rebasePatches(results, genSha);
363407
+ for (const patch5 of newPatches) {
363408
+ if (!rebaseCounts.absorbedPatchIds.has(patch5.id)) {
363409
+ this.service.lockManager.addPatch(patch5);
363410
+ }
363411
+ }
363412
+ this.service.lockManager.save();
363413
+ this.service.warnings.push(...this.service.lockManager.warnings);
363414
+ if (newPatches.length > 0) {
363415
+ if (!options2?.stageOnly) {
363416
+ const appliedCount = results.filter((r5) => r5.status === "applied").length;
363417
+ const buckets = computeCommitBuckets(results, rebaseCounts.absorbedPatchIds, newPatches);
363418
+ await this.service.committer.commitReplay(appliedCount, newPatches, void 0, { buckets });
363419
+ } else {
363420
+ await this.service.committer.stageAll();
363421
+ }
363422
+ }
363423
+ const warnings = [...detectorWarnings, ...this.service.warnings];
363424
+ return this.service.buildReport(
363425
+ "no-patches",
363426
+ newPatches,
363427
+ results,
363428
+ prep._prepared.optionsSnapshot,
363429
+ warnings,
363430
+ rebaseCounts,
363431
+ void 0,
363432
+ prep._prepared.revertedCount
363433
+ );
363434
+ }
363435
+ };
363436
+ var NormalRegenerationFlow = class {
363437
+ constructor(service) {
363438
+ this.service = service;
363439
+ }
363440
+ async prepare(options2) {
363441
+ const preLock = this.service.lockManager.read();
363442
+ const previousGenerationSha = preLock.current_generation ?? null;
363443
+ if (options2?.dryRun) {
363444
+ const existingPatches2 = this.service.lockManager.getPatches();
363445
+ const { patches: newPatches2, revertedPatchIds: dryRunReverted } = await this.service.detector.detectNewPatches();
363446
+ const warnings = [...this.service.detector.warnings, ...this.service.warnings];
363447
+ const allPatches2 = [...existingPatches2, ...newPatches2];
363448
+ const preparedReport = {
363449
+ flow: "normal-regeneration",
363450
+ patchesDetected: allPatches2.length,
363451
+ patchesApplied: 0,
363452
+ patchesWithConflicts: 0,
363453
+ patchesSkipped: 0,
363454
+ patchesReverted: dryRunReverted.length,
363455
+ conflicts: [],
363456
+ wouldApply: allPatches2,
363457
+ warnings: warnings.length > 0 ? warnings : void 0
363458
+ };
363459
+ const headSha = await this.service.git.exec(["rev-parse", "HEAD"]).then((s9) => s9.trim()).catch(() => "");
363460
+ return {
363461
+ flow: "normal-regeneration",
363462
+ previousGenerationSha,
363463
+ currentGenerationSha: headSha,
363464
+ baseBranchHead: options2.baseBranchHead ?? null,
363465
+ _prepared: {
363466
+ kind: "terminal",
363467
+ flow: "normal-regeneration",
363468
+ preparedReport
363469
+ }
363470
+ };
363471
+ }
363472
+ let existingPatches = this.service.lockManager.getPatches();
363473
+ const preRebasePatchIds = new Set(existingPatches.map((p14) => p14.id));
363474
+ const preRebaseCounts = await this.service.preGenerationRebase(existingPatches);
363475
+ const postRebasePatchIds = new Set(this.service.lockManager.getPatches().map((p14) => p14.id));
363476
+ const removedByPreRebase = existingPatches.filter(
363477
+ (p14) => preRebasePatchIds.has(p14.id) && !postRebasePatchIds.has(p14.id)
363478
+ );
363479
+ existingPatches = this.service.lockManager.getPatches();
363480
+ const seenHashes = /* @__PURE__ */ new Map();
363481
+ for (const p14 of existingPatches) {
363482
+ const priorPatchId = seenHashes.get(p14.content_hash);
363483
+ if (priorPatchId !== void 0) {
363484
+ this.service.lockManager.removePatch(p14.id);
363485
+ this.service.warnings.push(
363486
+ `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.`
363487
+ );
363488
+ } else {
363489
+ seenHashes.set(p14.content_hash, p14.id);
363490
+ }
363491
+ }
363492
+ existingPatches = this.service.lockManager.getPatches();
363493
+ let { patches: newPatches, revertedPatchIds } = await this.service.detector.detectNewPatches();
363494
+ const detectorWarnings = [...this.service.detector.warnings];
363495
+ if (removedByPreRebase.length > 0) {
363496
+ const removedOriginalCommits = new Set(removedByPreRebase.map((p14) => p14.original_commit));
363497
+ const removedOriginalMessages = new Set(removedByPreRebase.map((p14) => p14.original_message));
363498
+ newPatches = newPatches.filter((p14) => {
363499
+ if (removedOriginalCommits.has(p14.original_commit)) return false;
363500
+ if (isRevertCommit(p14.original_message)) {
363501
+ const revertedMsg = parseRevertedMessage(p14.original_message);
363502
+ if (revertedMsg && removedOriginalMessages.has(revertedMsg)) return false;
363503
+ }
363504
+ return true;
363505
+ });
363506
+ }
363507
+ for (const id2 of revertedPatchIds) {
363508
+ try {
363509
+ this.service.lockManager.removePatch(id2);
363510
+ } catch {
363511
+ }
363512
+ }
363513
+ const revertedSet = new Set(revertedPatchIds);
363514
+ existingPatches = existingPatches.filter((p14) => !revertedSet.has(p14.id));
363515
+ const allPatches = [...existingPatches, ...newPatches];
363516
+ const commitOpts = options2 ? {
363517
+ cliVersion: options2.cliVersion ?? "unknown",
363518
+ generatorVersions: options2.generatorVersions ?? {},
363519
+ baseBranchHead: options2.baseBranchHead
363520
+ } : void 0;
363521
+ await this.service.committer.commitGeneration("Update SDK", commitOpts);
363522
+ await this.service.cleanupStaleConflictMarkers();
363523
+ const genRecord = await this.service.committer.createGenerationRecord(commitOpts);
363524
+ this.service.lockManager.addGeneration(genRecord);
363525
+ return {
363526
+ flow: "normal-regeneration",
363527
+ previousGenerationSha,
363528
+ currentGenerationSha: genRecord.commit_sha,
363529
+ baseBranchHead: options2?.baseBranchHead ?? null,
363530
+ _prepared: {
363531
+ kind: "active",
363532
+ flow: "normal-regeneration",
363533
+ patchesToApply: allPatches,
363534
+ newPatchIds: new Set(newPatches.map((p14) => p14.id)),
363535
+ preRebaseCounts,
363536
+ detectorWarnings,
363537
+ revertedCount: revertedPatchIds.length,
363538
+ genSha: genRecord.commit_sha,
363539
+ optionsSnapshot: options2
363540
+ }
363541
+ };
363542
+ }
363543
+ async apply(prep, options2) {
363544
+ assertActive(prep);
363545
+ const allPatches = prep._prepared.patchesToApply;
363546
+ const newPatchIds = prep._prepared.newPatchIds;
363547
+ const detectorWarnings = prep._prepared.detectorWarnings;
363548
+ const genSha = prep._prepared.genSha;
363549
+ const results = await this.service.applicator.applyPatches(allPatches);
363550
+ this.service._lastApplyResults = results;
363551
+ this.service.recordDroppedContextLineWarnings(results);
363552
+ this.service.revertConflictingFiles(results);
363553
+ for (const result of results) {
363554
+ if (result.status === "conflict") {
363555
+ result.patch.status = "unresolved";
363556
+ }
363557
+ }
363558
+ const rebaseCounts = await this.service.rebasePatches(results, genSha);
363559
+ const newPatches = allPatches.filter((p14) => newPatchIds.has(p14.id));
363560
+ for (const patch5 of newPatches) {
363561
+ if (!rebaseCounts.absorbedPatchIds.has(patch5.id)) {
363562
+ this.service.lockManager.addPatch(patch5);
363563
+ }
363564
+ }
363565
+ for (const result of results) {
363566
+ if (result.status === "conflict") {
363567
+ try {
363568
+ this.service.lockManager.markPatchUnresolved(result.patch.id);
363569
+ } catch {
363570
+ }
363571
+ }
363572
+ }
363573
+ this.service.lockManager.save();
363574
+ this.service.warnings.push(...this.service.lockManager.warnings);
363575
+ if (options2?.stageOnly) {
363576
+ await this.service.committer.stageAll();
363577
+ } else {
363578
+ const appliedCount = results.filter((r5) => r5.status === "applied").length;
363579
+ const buckets = computeCommitBuckets(results, rebaseCounts.absorbedPatchIds, allPatches);
363580
+ await this.service.committer.commitReplay(appliedCount, allPatches, void 0, { buckets });
363581
+ }
363582
+ const warnings = [...detectorWarnings, ...this.service.warnings];
363583
+ return this.service.buildReport(
363584
+ "normal-regeneration",
363585
+ allPatches,
363586
+ results,
363587
+ prep._prepared.optionsSnapshot,
363588
+ warnings,
363589
+ rebaseCounts,
363590
+ prep._prepared.preRebaseCounts,
363591
+ prep._prepared.revertedCount
363592
+ );
363593
+ }
363594
+ };
363595
+ function assertActive(prep) {
363596
+ if (prep._prepared.kind !== "active") {
363597
+ throw new Error(
363598
+ `Flow.apply() called with terminal preparation (kind=${prep._prepared.kind}); terminal cases must be handled by applyPreparedReplay's dispatcher`
363599
+ );
363600
+ }
363601
+ }
363067
363602
  var ReplayService = class {
363603
+ /** @internal Used by Flow implementations in `src/flows/`. */
363068
363604
  git;
363605
+ /** @internal Used by Flow implementations in `src/flows/`. */
363069
363606
  detector;
363607
+ /** @internal Used by Flow implementations in `src/flows/`. */
363070
363608
  applicator;
363609
+ /** @internal Used by Flow implementations in `src/flows/`. */
363071
363610
  committer;
363611
+ /** @internal Used by Flow implementations in `src/flows/`. */
363072
363612
  lockManager;
363613
+ /** @internal Used by Flow implementations in `src/flows/`. */
363614
+ fileOwnership;
363615
+ /** @internal Used by Flow implementations in `src/flows/`. */
363073
363616
  outputDir;
363074
363617
  /** @internal Test-only. Captures the last ReplayResult[] returned from applicator.applyPatches(). */
363075
363618
  _lastApplyResults = [];
363076
363619
  /**
363077
363620
  * Service-level warnings accumulated during a single runReplay() call.
363078
363621
  * 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()`.
363622
+ * Populated by `FileOwnership.resolveCanonical` when a patch's base generation tree
363623
+ * is unreachable (shallow clone / aggressive `git gc --prune`) and we fall back to a
363624
+ * conservative heuristic. Cleared at the top of `runReplay()`.
363625
+ *
363626
+ * @internal Used by Flow implementations in `src/flows/`.
363082
363627
  */
363083
363628
  warnings = [];
363084
363629
  /**
363085
363630
  * @internal Test-only accessor.
363086
363631
  * 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).
363632
+ * to inspect the inter-patch accumulator after runReplay() completes (invariant I2).
363088
363633
  */
363089
363634
  get applicatorRef() {
363090
363635
  return this.applicator;
@@ -363092,8 +363637,8 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
363092
363637
  /**
363093
363638
  * @internal Test-only accessor.
363094
363639
  * 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.
363640
+ * this to filter applied-vs-conflicted-vs-absorbed patches for invariant
363641
+ * assertions. Returns an empty array if no patches have been applied yet.
363097
363642
  */
363098
363643
  getLastResults() {
363099
363644
  return this._lastApplyResults;
@@ -363106,6 +363651,7 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
363106
363651
  this.detector = new ReplayDetector(git, this.lockManager, outputDir);
363107
363652
  this.applicator = new ReplayApplicator(git, this.lockManager, outputDir);
363108
363653
  this.committer = new ReplayCommitter(git, outputDir);
363654
+ this.fileOwnership = new FileOwnership(git);
363109
363655
  }
363110
363656
  /**
363111
363657
  * Phase 1 of the two-phase replay flow. Runs preGenerationRebase (when applicable),
@@ -363122,18 +363668,7 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
363122
363668
  async prepareReplay(options2) {
363123
363669
  this.warnings = [];
363124
363670
  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
- }
363671
+ return this.selectFlow(options2).prepare(options2);
363137
363672
  }
363138
363673
  /**
363139
363674
  * Phase 2 of the two-phase replay flow. Applies patches, rebases them against
@@ -363145,18 +363680,33 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
363145
363680
  * its own post-commit logic), this returns the precomputed report.
363146
363681
  */
363147
363682
  async applyPreparedReplay(prep, options2) {
363148
- if (prep._prepared.terminal) {
363683
+ if (prep._prepared.kind === "terminal") {
363149
363684
  return prep._prepared.preparedReport;
363150
363685
  }
363151
- switch (prep._prepared.flow) {
363686
+ return this.flowForKind(prep._prepared.flow).apply(prep, options2);
363687
+ }
363688
+ /**
363689
+ * Construct the flow for the current run. `selectFlow` is called from
363690
+ * `prepareReplay` and decides based on options + lockfile state.
363691
+ */
363692
+ selectFlow(options2) {
363693
+ if (options2?.skipApplication) return new SkipApplicationFlow(this);
363694
+ return this.flowForKind(this.determineFlow());
363695
+ }
363696
+ /**
363697
+ * Construct a flow given its kind. Used by `applyPreparedReplay` to
363698
+ * route to the same kind that was selected in phase 1.
363699
+ */
363700
+ flowForKind(kind) {
363701
+ switch (kind) {
363152
363702
  case "first-generation":
363153
- return this._applyFirstGeneration(prep);
363154
- case "skip-application":
363155
- return this._applySkipApplication(prep, options2);
363703
+ return new FirstGenerationFlow(this);
363156
363704
  case "no-patches":
363157
- return this._applyNoPatchesRegeneration(prep, options2);
363705
+ return new NoPatchesFlow(this);
363158
363706
  case "normal-regeneration":
363159
- return this._applyNormalRegeneration(prep, options2);
363707
+ return new NormalRegenerationFlow(this);
363708
+ case "skip-application":
363709
+ return new SkipApplicationFlow(this);
363160
363710
  }
363161
363711
  }
363162
363712
  /**
@@ -363240,423 +363790,12 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
363240
363790
  throw error50;
363241
363791
  }
363242
363792
  }
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
363793
  /**
363657
363794
  * Rebase cleanly applied patches so they are relative to the current generation.
363658
363795
  * This prevents recurring conflicts on subsequent regenerations.
363659
363796
  * Returns the number of patches rebased.
363797
+ *
363798
+ * @internal Used by Flow implementations in `src/flows/`.
363660
363799
  */
363661
363800
  async rebasePatches(results, currentGenSha) {
363662
363801
  let absorbed = 0;
@@ -363666,6 +363805,7 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
363666
363805
  const seenContentHashes = /* @__PURE__ */ new Map();
363667
363806
  const absorbedPatchIds = /* @__PURE__ */ new Set();
363668
363807
  const fernignorePatterns = this.readFernignorePatterns();
363808
+ const warnedGens = /* @__PURE__ */ new Set();
363669
363809
  for (const result of results) {
363670
363810
  if (result.resolvedFiles && Object.keys(result.resolvedFiles).length > 0) {
363671
363811
  const patch5 = result.patch;
@@ -363710,21 +363850,20 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
363710
363850
  }
363711
363851
  }
363712
363852
  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
- })
363853
+ patch5.files.map(
363854
+ (file4) => this.fileOwnership.resolveCanonical({
363855
+ file: file4,
363856
+ originalFileName: originalByResolved[file4],
363857
+ patch: patch5,
363858
+ currentGenSha,
363859
+ fernignorePatterns,
363860
+ warnedGens,
363861
+ warnings: this.warnings
363862
+ })
363863
+ )
363725
363864
  );
363726
363865
  const hasProtectedFiles = patch5.files.some(
363727
- (file4) => file4 === ".fernignore" || fernignorePatterns.some((p14) => file4 === p14 || (0, import_minimatch22.minimatch)(file4, p14))
363866
+ (file4) => file4 === ".fernignore" || fernignorePatterns.some((p14) => file4 === p14 || (0, import_minimatch32.minimatch)(file4, p14))
363728
363867
  );
363729
363868
  const allUserOwned = isUserOwnedPerFile.every(Boolean);
363730
363869
  if (allUserOwned && patch5.files.length > 0) {
@@ -363848,7 +363987,7 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
363848
363987
  for (const file4 of files) {
363849
363988
  if (isBinaryFile(file4)) continue;
363850
363989
  try {
363851
- const content5 = await (0, import_promises410.readFile)((0, import_node_path52.join)(this.outputDir, file4), "utf-8");
363990
+ const content5 = await (0, import_promises610.readFile)((0, import_node_path82.join)(this.outputDir, file4), "utf-8");
363852
363991
  snapshot[file4] = content5;
363853
363992
  } catch {
363854
363993
  }
@@ -363856,10 +363995,16 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
363856
363995
  return snapshot;
363857
363996
  }
363858
363997
  /**
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.
363998
+ * Detects autoversion contamination — returns true when a
363999
+ * `[fern-autoversion]` commit between `fromSha` and `toSha` touched any
364000
+ * of the patch's `files`. Re-classifies each grep match via
364001
+ * `isGenerationCommit` so a customer commit that merely quotes the
364002
+ * marker in its body doesn't false-positive.
364003
+ *
364004
+ * Used by preGenerationRebase to skip the customer-content rebuild for
364005
+ * patches whose files were last touched by an autoversion step — a
364006
+ * naive `git diff currentGen HEAD` there would capture the
364007
+ * placeholder→semver swap as customer customization.
363863
364008
  */
363864
364009
  async hasAutoversionContamination(fromSha, toSha, files) {
363865
364010
  if (files.length === 0) return false;
@@ -363888,10 +364033,15 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
363888
364033
  return false;
363889
364034
  }
363890
364035
  /**
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.
364036
+ * Rebuild patch_content from `theirs_snapshot` (captured at detection
364037
+ * time, predates pipeline content) rather than a HEAD-relative diff.
364038
+ * Returns `null` when snapshot is missing or doesn't cover every file
364039
+ * in `patch.files` — caller leaves the patch unchanged.
364040
+ *
364041
+ * Used as the autoversion-contamination escape hatch: when the
364042
+ * HEAD-relative diff would capture pipeline content (autoversion's
364043
+ * placeholder swap) as customer customization, the snapshot path
364044
+ * preserves the customer's actual intent.
363895
364045
  */
363896
364046
  async computeSnapshotBasedPatchDiff(patch5, currentGen) {
363897
364047
  if (!patch5.theirs_snapshot) return null;
@@ -363939,7 +364089,7 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
363939
364089
  * **Skips patches sharing files with other patches.** HEAD's content
363940
364090
  * for a file shared by N patches is cumulative across all of them, so
363941
364091
  * a HEAD-fallback would falsely encode other patches' contributions
363942
- * into one patch's snapshot — breaking FER-9525 cross-patch isolation
364092
+ * into one patch's snapshot — breaking cross-patch isolation
363943
364093
  * if the snapshot fallback later fires. Per-patch snapshots only
363944
364094
  * make sense when the patch owns its file. Multi-patch-same-file
363945
364095
  * legacy lockfiles keep using line-anchored reconstruction (the
@@ -364001,72 +364151,6 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
364001
364151
  );
364002
364152
  }
364003
364153
  }
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
364154
  /**
364071
364155
  * For conflict patches with mixed results (some files merged, some conflicted),
364072
364156
  * check if the cleanly merged files were absorbed by the generator (empty diff).
@@ -364085,7 +364169,15 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
364085
364169
  const warnedGens = /* @__PURE__ */ new Set();
364086
364170
  const generatorCleanFiles = [];
364087
364171
  for (const file4 of cleanFiles) {
364088
- if (await this.isFileUserOwned(file4, patch5, currentGenSha, fernignorePatterns, warnedGens)) {
364172
+ const userOwned = await this.fileOwnership.resolveCanonical({
364173
+ file: file4,
364174
+ patch: patch5,
364175
+ currentGenSha,
364176
+ fernignorePatterns,
364177
+ warnedGens,
364178
+ warnings: this.warnings
364179
+ });
364180
+ if (userOwned) {
364089
364181
  continue;
364090
364182
  }
364091
364183
  generatorCleanFiles.push(file4);
@@ -364114,6 +364206,7 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
364114
364206
  * Pre-generation rebase: update patches using the customer's current state.
364115
364207
  * Called BEFORE commitGeneration() while HEAD has customer code.
364116
364208
  */
364209
+ /** @internal Used by Flow implementations in `src/flows/`. */
364117
364210
  async preGenerationRebase(patches) {
364118
364211
  const lock = this.lockManager.read();
364119
364212
  const currentGen = lock.current_generation;
@@ -364152,7 +364245,15 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
364152
364245
  if (patch5.user_owned) return true;
364153
364246
  if (patch5.files.length === 0) return false;
364154
364247
  for (const file4 of patch5.files) {
364155
- if (!await this.isFileUserOwned(file4, patch5, currentGen, fernignorePatterns, warnedGens)) {
364248
+ const userOwned = await this.fileOwnership.resolveCanonical({
364249
+ file: file4,
364250
+ patch: patch5,
364251
+ currentGenSha: currentGen,
364252
+ fernignorePatterns,
364253
+ warnedGens,
364254
+ warnings: this.warnings
364255
+ });
364256
+ if (!userOwned) {
364156
364257
  return false;
364157
364258
  }
364158
364259
  }
@@ -364202,7 +364303,7 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
364202
364303
  );
364203
364304
  if (snapHasStaleMarkers) continue;
364204
364305
  const isProtectedSnap = patch5.files.some(
364205
- (file4) => file4 === ".fernignore" || fernignorePatterns.some((p14) => file4 === p14 || (0, import_minimatch22.minimatch)(file4, p14))
364306
+ (file4) => file4 === ".fernignore" || fernignorePatterns.some((p14) => file4 === p14 || (0, import_minimatch32.minimatch)(file4, p14))
364206
364307
  );
364207
364308
  if (isProtectedSnap) continue;
364208
364309
  const snapHash = this.detector.computeContentHash(snapshotDiff);
@@ -364237,7 +364338,7 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
364237
364338
  continue;
364238
364339
  }
364239
364340
  const isProtected = patch5.files.some(
364240
- (file4) => file4 === ".fernignore" || fernignorePatterns.some((p14) => file4 === p14 || (0, import_minimatch22.minimatch)(file4, p14))
364341
+ (file4) => file4 === ".fernignore" || fernignorePatterns.some((p14) => file4 === p14 || (0, import_minimatch32.minimatch)(file4, p14))
364241
364342
  );
364242
364343
  if (isProtected) {
364243
364344
  continue;
@@ -364344,6 +364445,7 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
364344
364445
  * relied on them. This is the "surface or preserve, never silently drop"
364345
364446
  * contract for legitimate-deletion cases.
364346
364447
  */
364448
+ /** @internal Used by Flow implementations in `src/flows/`. */
364347
364449
  recordDroppedContextLineWarnings(results) {
364348
364450
  const PREVIEW_COUNT = 5;
364349
364451
  for (const result of results) {
@@ -364366,12 +364468,13 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
364366
364468
  * After applyPatches(), strip conflict markers from conflicting files
364367
364469
  * so only clean content is committed. Keeps the Generated (OURS) side.
364368
364470
  */
364471
+ /** @internal Used by Flow implementations in `src/flows/`. */
364369
364472
  revertConflictingFiles(results) {
364370
364473
  for (const result of results) {
364371
364474
  if (result.status !== "conflict" || !result.fileResults) continue;
364372
364475
  for (const fileResult of result.fileResults) {
364373
364476
  if (fileResult.status !== "conflict") continue;
364374
- const filePath = (0, import_node_path52.join)(this.outputDir, fileResult.file);
364477
+ const filePath = (0, import_node_path82.join)(this.outputDir, fileResult.file);
364375
364478
  try {
364376
364479
  const content5 = (0, import_node_fs210.readFileSync)(filePath, "utf-8");
364377
364480
  const stripped = stripConflictMarkers(content5);
@@ -364387,13 +364490,14 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
364387
364490
  * Restores files to their clean generated state from HEAD.
364388
364491
  * Skips .fernignore-protected files to prevent overwriting user content.
364389
364492
  */
364493
+ /** @internal Used by Flow implementations in `src/flows/`. */
364390
364494
  async cleanupStaleConflictMarkers() {
364391
364495
  const markerFiles = await this.git.exec(["grep", "-l", "<<<<<<< Generated", "--", "."]).catch(() => "");
364392
364496
  const files = markerFiles.trim().split("\n").filter(Boolean);
364393
364497
  if (files.length === 0) return;
364394
364498
  const fernignorePatterns = this.readFernignorePatterns();
364395
364499
  for (const file4 of files) {
364396
- if (fernignorePatterns.some((pattern) => (0, import_minimatch22.minimatch)(file4, pattern))) continue;
364500
+ if (fernignorePatterns.some((pattern) => (0, import_minimatch32.minimatch)(file4, pattern))) continue;
364397
364501
  try {
364398
364502
  await this.git.exec(["checkout", "HEAD", "--", file4]);
364399
364503
  } catch {
@@ -364401,10 +364505,11 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
364401
364505
  }
364402
364506
  }
364403
364507
  readFernignorePatterns() {
364404
- const fernignorePath = (0, import_node_path52.join)(this.outputDir, ".fernignore");
364508
+ const fernignorePath = (0, import_node_path82.join)(this.outputDir, ".fernignore");
364405
364509
  if (!(0, import_node_fs210.existsSync)(fernignorePath)) return [];
364406
364510
  return (0, import_node_fs210.readFileSync)(fernignorePath, "utf-8").split("\n").map((line) => line.trim()).filter((line) => line && !line.startsWith("#"));
364407
364511
  }
364512
+ /** @internal Used by Flow implementations in `src/flows/`. */
364408
364513
  buildReport(flow4, patches, results, options2, warnings, rebaseCounts, preRebaseCounts, patchesReverted) {
364409
364514
  const conflictResults = results.filter((r5) => r5.status === "conflict");
364410
364515
  const conflictDetails = conflictResults.map((r5) => {
@@ -364478,8 +364583,8 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
364478
364583
  }
364479
364584
  var import_node_crypto22 = require("crypto");
364480
364585
  var import_node_fs33 = require("fs");
364481
- var import_node_path62 = require("path");
364482
- var import_minimatch32 = require_commonjs3();
364586
+ var import_node_path92 = require("path");
364587
+ var import_minimatch42 = require_commonjs3();
364483
364588
  var import_yaml22 = require_dist11();
364484
364589
  var FernignoreMigrator = class {
364485
364590
  git;
@@ -364491,10 +364596,10 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
364491
364596
  this.outputDir = outputDir;
364492
364597
  }
364493
364598
  fernignoreExists() {
364494
- return (0, import_node_fs33.existsSync)((0, import_node_path62.join)(this.outputDir, ".fernignore"));
364599
+ return (0, import_node_fs33.existsSync)((0, import_node_path92.join)(this.outputDir, ".fernignore"));
364495
364600
  }
364496
364601
  readFernignorePatterns() {
364497
- const fernignorePath = (0, import_node_path62.join)(this.outputDir, ".fernignore");
364602
+ const fernignorePath = (0, import_node_path92.join)(this.outputDir, ".fernignore");
364498
364603
  if (!(0, import_node_fs33.existsSync)(fernignorePath)) {
364499
364604
  return [];
364500
364605
  }
@@ -364511,7 +364616,7 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
364511
364616
  const commitsOnly = [];
364512
364617
  const syntheticPatches = [];
364513
364618
  for (const pattern of patterns) {
364514
- const matchingPatch = patches.find((p14) => p14.files.some((f4) => (0, import_minimatch32.minimatch)(f4, pattern) || f4 === pattern));
364619
+ const matchingPatch = patches.find((p14) => p14.files.some((f4) => (0, import_minimatch42.minimatch)(f4, pattern) || f4 === pattern));
364515
364620
  if (matchingPatch) {
364516
364621
  trackedByBoth.push({
364517
364622
  file: pattern,
@@ -364584,7 +364689,7 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
364584
364689
  fernignoreOnly.push(...remainingFernignoreOnly);
364585
364690
  }
364586
364691
  for (const patch5 of patches) {
364587
- const hasUnprotectedFiles = patch5.files.some((f4) => !patterns.some((p14) => (0, import_minimatch32.minimatch)(f4, p14) || f4 === p14));
364692
+ const hasUnprotectedFiles = patch5.files.some((f4) => !patterns.some((p14) => (0, import_minimatch42.minimatch)(f4, p14) || f4 === p14));
364588
364693
  if (hasUnprotectedFiles) {
364589
364694
  commitsOnly.push(patch5);
364590
364695
  }
@@ -364596,14 +364701,14 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
364596
364701
  const results = [];
364597
364702
  for (const pattern of patterns) {
364598
364703
  const matching = allFiles.filter(
364599
- (f4) => (0, import_minimatch32.minimatch)(f4, pattern) || f4 === pattern || f4.startsWith(pattern + "/")
364704
+ (f4) => (0, import_minimatch42.minimatch)(f4, pattern) || f4 === pattern || f4.startsWith(pattern + "/")
364600
364705
  );
364601
364706
  results.push({ pattern, files: matching.length > 0 ? matching : [pattern] });
364602
364707
  }
364603
364708
  return results;
364604
364709
  }
364605
364710
  readFileContent(filePath) {
364606
- const fullPath = (0, import_node_path62.join)(this.outputDir, filePath);
364711
+ const fullPath = (0, import_node_path92.join)(this.outputDir, filePath);
364607
364712
  if (!(0, import_node_fs33.existsSync)(fullPath)) {
364608
364713
  return null;
364609
364714
  }
@@ -364668,7 +364773,7 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
364668
364773
  };
364669
364774
  }
364670
364775
  movePatternsToReplayYml(patterns) {
364671
- const replayYmlPath = (0, import_node_path62.join)(this.outputDir, ".fern", "replay.yml");
364776
+ const replayYmlPath = (0, import_node_path92.join)(this.outputDir, ".fern", "replay.yml");
364672
364777
  let config5 = {};
364673
364778
  if ((0, import_node_fs33.existsSync)(replayYmlPath)) {
364674
364779
  const content5 = (0, import_node_fs33.readFileSync)(replayYmlPath, "utf-8");
@@ -364677,7 +364782,7 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
364677
364782
  const existing = config5.exclude ?? [];
364678
364783
  const merged = [.../* @__PURE__ */ new Set([...existing, ...patterns])];
364679
364784
  config5.exclude = merged;
364680
- const dir = (0, import_node_path62.dirname)(replayYmlPath);
364785
+ const dir = (0, import_node_path92.dirname)(replayYmlPath);
364681
364786
  if (!(0, import_node_fs33.existsSync)(dir)) {
364682
364787
  (0, import_node_fs33.mkdirSync)(dir, { recursive: true });
364683
364788
  }
@@ -364686,7 +364791,7 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
364686
364791
  };
364687
364792
  var import_node_crypto32 = require("crypto");
364688
364793
  var import_node_fs42 = require("fs");
364689
- var import_node_path72 = require("path");
364794
+ var import_node_path102 = require("path");
364690
364795
  init_GitClient();
364691
364796
  async function bootstrap(outputDir, options2) {
364692
364797
  const git = new GitClient(outputDir);
@@ -364853,7 +364958,7 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
364853
364958
  }
364854
364959
  var REPLAY_FERNIGNORE_ENTRIES = [".fern/replay.lock", ".fern/replay.yml", ".gitattributes"];
364855
364960
  function ensureFernignoreEntries(outputDir) {
364856
- const fernignorePath = (0, import_node_path72.join)(outputDir, ".fernignore");
364961
+ const fernignorePath = (0, import_node_path102.join)(outputDir, ".fernignore");
364857
364962
  let content5 = "";
364858
364963
  if ((0, import_node_fs42.existsSync)(fernignorePath)) {
364859
364964
  content5 = (0, import_node_fs42.readFileSync)(fernignorePath, "utf-8");
@@ -364877,7 +364982,7 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
364877
364982
  }
364878
364983
  var GITATTRIBUTES_ENTRIES = [".fern/replay.lock linguist-generated=true"];
364879
364984
  function ensureGitattributesEntries(outputDir) {
364880
- const gitattributesPath = (0, import_node_path72.join)(outputDir, ".gitattributes");
364985
+ const gitattributesPath = (0, import_node_path102.join)(outputDir, ".gitattributes");
364881
364986
  let content5 = "";
364882
364987
  if ((0, import_node_fs42.existsSync)(gitattributesPath)) {
364883
364988
  content5 = (0, import_node_fs42.readFileSync)(gitattributesPath, "utf-8");
@@ -364905,7 +365010,7 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
364905
365010
  const normalized = relevantLines.filter((line) => !line.startsWith("index ")).join("\n").replace(/\n-- \n[\d]+\.[\d]+[^\n]*\s*$/, "");
364906
365011
  return `sha256:${(0, import_node_crypto32.createHash)("sha256").update(normalized).digest("hex")}`;
364907
365012
  }
364908
- var import_minimatch42 = require_commonjs3();
365013
+ var import_minimatch52 = require_commonjs3();
364909
365014
  function parseDiffStat(patchContent) {
364910
365015
  let additions = 0;
364911
365016
  let deletions = 0;
@@ -364935,7 +365040,7 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
364935
365040
  }
364936
365041
  function matchesPatch(patch5, pattern) {
364937
365042
  const fileMatch = patch5.files.some(
364938
- (file4) => file4 === pattern || (0, import_minimatch42.minimatch)(file4, pattern)
365043
+ (file4) => file4 === pattern || (0, import_minimatch52.minimatch)(file4, pattern)
364939
365044
  );
364940
365045
  if (fileMatch) return true;
364941
365046
  return patch5.original_message.toLowerCase().includes(pattern.toLowerCase());
@@ -365069,8 +365174,8 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
365069
365174
  nothingToReset: false
365070
365175
  };
365071
365176
  }
365072
- var import_promises510 = require("fs/promises");
365073
- var import_node_path82 = require("path");
365177
+ var import_promises710 = require("fs/promises");
365178
+ var import_node_path112 = require("path");
365074
365179
  init_GitClient();
365075
365180
  async function resolve15(outputDir, options2) {
365076
365181
  const lockManager = new LockfileManager(outputDir);
@@ -365119,7 +365224,7 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
365119
365224
  const result = await computePerPatchDiffWithFallback(
365120
365225
  patch5,
365121
365226
  (f4) => git.showFile(currentGen, f4),
365122
- (f4) => (0, import_promises510.readFile)((0, import_node_path82.join)(outputDir, f4), "utf-8").catch(() => null),
365227
+ (f4) => (0, import_promises710.readFile)((0, import_node_path112.join)(outputDir, f4), "utf-8").catch(() => null),
365123
365228
  (files) => git.exec(["diff", currentGen, "--", ...files]).catch(() => null)
365124
365229
  );
365125
365230
  if (result === null) {
@@ -365175,7 +365280,7 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
365175
365280
  const theirsSnapshot = {};
365176
365281
  for (const file4 of filesForSnapshot) {
365177
365282
  if (isBinaryFile(file4)) continue;
365178
- const content5 = await (0, import_promises510.readFile)((0, import_node_path82.join)(outputDir, file4), "utf-8").catch(() => null);
365283
+ const content5 = await (0, import_promises710.readFile)((0, import_node_path112.join)(outputDir, file4), "utf-8").catch(() => null);
365179
365284
  if (content5 != null) {
365180
365285
  theirsSnapshot[file4] = content5;
365181
365286
  }
@@ -667965,7 +668070,7 @@ var AccessTokenPosthogManager = class {
667965
668070
  properties: {
667966
668071
  ...event,
667967
668072
  ...event.properties,
667968
- version: "5.27.5",
668073
+ version: "5.27.7",
667969
668074
  usingAccessToken: true,
667970
668075
  ...getRunIdProperties()
667971
668076
  }
@@ -668029,7 +668134,7 @@ var UserPosthogManager = class {
668029
668134
  distinctId: this.userId ?? await this.getPersistedDistinctId(),
668030
668135
  event: "CLI",
668031
668136
  properties: {
668032
- version: "5.27.5",
668137
+ version: "5.27.7",
668033
668138
  ...event,
668034
668139
  ...event.properties,
668035
668140
  usingAccessToken: false,
@@ -859608,7 +859713,7 @@ var LOCAL_STORAGE_FOLDER4 = ".fern-dev";
859608
859713
  var LOGS_FOLDER_NAME = "logs";
859609
859714
  var MAX_LOGS_DIR_SIZE_BYTES = 100 * 1024 * 1024;
859610
859715
  function getCliSource() {
859611
- const version7 = "5.27.5";
859716
+ const version7 = "5.27.7";
859612
859717
  return `cli@${version7}`;
859613
859718
  }
859614
859719
  var DebugLogger = class {
@@ -873608,7 +873713,7 @@ var LegacyDocsPublisher = class {
873608
873713
  previewId,
873609
873714
  disableTemplates: void 0,
873610
873715
  skipUpload,
873611
- cliVersion: "5.27.5",
873716
+ cliVersion: "5.27.7",
873612
873717
  loginCommand: "fern auth login"
873613
873718
  });
873614
873719
  if (taskContext.getResult() === TaskResult.Failure) {
@@ -948013,7 +948118,7 @@ function getAutomationContextFromEnv() {
948013
948118
  config_branch: process.env.FERN_CONFIG_BRANCH,
948014
948119
  config_pr_number: process.env.FERN_CONFIG_PR_NUMBER,
948015
948120
  trigger: process.env.GITHUB_EVENT_NAME,
948016
- cli_version: "5.27.5"
948121
+ cli_version: "5.27.7"
948017
948122
  };
948018
948123
  }
948019
948124
  function isAutomationMode() {
@@ -948839,7 +948944,7 @@ var CliContext = class _CliContext {
948839
948944
  if (false) {
948840
948945
  this.logger.error("CLI_VERSION is not defined");
948841
948946
  }
948842
- return "5.27.5";
948947
+ return "5.27.7";
948843
948948
  }
948844
948949
  getCliName() {
948845
948950
  if (false) {