@fern-api/replay 0.14.1 → 0.15.0

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.
package/dist/cli.cjs CHANGED
@@ -17911,6 +17911,66 @@ var ReplayService = class {
17911
17911
  }
17912
17912
  return snapshot;
17913
17913
  }
17914
+ /**
17915
+ * FER-10203: returns true when a `[fern-autoversion]` commit between
17916
+ * `fromSha` and `toSha` touched any of the patch's `files`. Re-classifies
17917
+ * each grep match via `isGenerationCommit` so a customer commit that
17918
+ * merely quotes the marker in its body doesn't false-positive.
17919
+ */
17920
+ async hasAutoversionContamination(fromSha, toSha, files) {
17921
+ if (files.length === 0) return false;
17922
+ const log = await this.git.exec([
17923
+ "log",
17924
+ `${fromSha}..${toSha}`,
17925
+ "--grep=\\[fern-autoversion\\]",
17926
+ "--extended-regexp",
17927
+ "--format=%H%x09%aN%x09%ae%x09%s",
17928
+ "--",
17929
+ ...files
17930
+ ]).catch(() => "");
17931
+ if (!log.trim()) return false;
17932
+ for (const line of log.trim().split("\n")) {
17933
+ const [sha, authorName, authorEmail, message] = line.split(" ");
17934
+ if (sha == null) continue;
17935
+ if (isGenerationCommit({
17936
+ sha,
17937
+ authorName: authorName ?? "",
17938
+ authorEmail: authorEmail ?? "",
17939
+ message: message ?? ""
17940
+ })) {
17941
+ return true;
17942
+ }
17943
+ }
17944
+ return false;
17945
+ }
17946
+ /**
17947
+ * FER-10203: rebuild patch_content from `theirs_snapshot` (captured at
17948
+ * detection time, predates pipeline content) rather than a HEAD-relative
17949
+ * diff. Returns `null` when snapshot is missing or doesn't cover every
17950
+ * file in `patch.files` — caller leaves the patch unchanged.
17951
+ */
17952
+ async computeSnapshotBasedPatchDiff(patch, currentGen) {
17953
+ if (!patch.theirs_snapshot) return null;
17954
+ if (Object.keys(patch.theirs_snapshot).length === 0) return null;
17955
+ for (const file of patch.files) {
17956
+ if (patch.theirs_snapshot[file] == null) return null;
17957
+ }
17958
+ const fragments = [];
17959
+ for (const file of patch.files) {
17960
+ const theirsContent = patch.theirs_snapshot[file];
17961
+ const oursContent = await this.git.showFile(currentGen, file).catch(() => null);
17962
+ if (oursContent == null) {
17963
+ fragments.push(synthesizeNewFileDiff(file, theirsContent));
17964
+ continue;
17965
+ }
17966
+ if (oursContent === theirsContent) continue;
17967
+ const fileDiff = await unifiedDiff(oursContent, theirsContent, file);
17968
+ if (fileDiff.trim()) {
17969
+ fragments.push(fileDiff);
17970
+ }
17971
+ }
17972
+ return fragments.join("");
17973
+ }
17914
17974
  /**
17915
17975
  * Read THEIRS snapshot from a git tree-ish (commit, branch, or tree).
17916
17976
  * Used when the diff that produced `patch_content` was relative to that
@@ -18176,6 +18236,41 @@ var ReplayService = class {
18176
18236
  }
18177
18237
  if (patch.base_generation === currentGen) {
18178
18238
  try {
18239
+ const contaminated = await this.hasAutoversionContamination(
18240
+ currentGen,
18241
+ "HEAD",
18242
+ patch.files
18243
+ );
18244
+ if (contaminated) {
18245
+ const snapshotDiff = await this.computeSnapshotBasedPatchDiff(
18246
+ patch,
18247
+ currentGen
18248
+ );
18249
+ if (snapshotDiff === null) continue;
18250
+ if (!snapshotDiff.trim()) {
18251
+ if (await allPatchFilesUserOwned(patch)) continue;
18252
+ this.lockManager.removePatch(patch.id);
18253
+ contentRefreshed++;
18254
+ continue;
18255
+ }
18256
+ const snapHasStaleMarkers = snapshotDiff.split("\n").some(
18257
+ (l) => l.startsWith("+<<<<<<< Generated") || l.startsWith("+>>>>>>> Your customization")
18258
+ );
18259
+ if (snapHasStaleMarkers) continue;
18260
+ const isProtectedSnap = patch.files.some(
18261
+ (file) => file === ".fernignore" || fernignorePatterns.some((p) => file === p || minimatch(file, p))
18262
+ );
18263
+ if (isProtectedSnap) continue;
18264
+ const snapHash = this.detector.computeContentHash(snapshotDiff);
18265
+ if (snapHash !== patch.content_hash) {
18266
+ this.lockManager.updatePatch(patch.id, {
18267
+ patch_content: snapshotDiff,
18268
+ content_hash: snapHash
18269
+ });
18270
+ contentRefreshed++;
18271
+ }
18272
+ continue;
18273
+ }
18179
18274
  const ppResult = await computePerPatchDiffWithFallback(
18180
18275
  patch,
18181
18276
  (f) => this.git.showFile(currentGen, f),
@@ -18227,6 +18322,42 @@ var ReplayService = class {
18227
18322
  try {
18228
18323
  const markerFiles = await this.git.exec(["grep", "-l", "<<<<<<< Generated", "HEAD", "--", ...patch.files]).catch(() => "");
18229
18324
  if (markerFiles.trim()) continue;
18325
+ const contaminated = await this.hasAutoversionContamination(
18326
+ currentGen,
18327
+ "HEAD",
18328
+ patch.files
18329
+ );
18330
+ if (contaminated) {
18331
+ const snapshotDiff = await this.computeSnapshotBasedPatchDiff(
18332
+ patch,
18333
+ currentGen
18334
+ );
18335
+ if (snapshotDiff === null) continue;
18336
+ if (!snapshotDiff.trim()) {
18337
+ if (await allPatchFilesUserOwned(patch)) {
18338
+ try {
18339
+ this.lockManager.updatePatch(patch.id, { base_generation: currentGen });
18340
+ } catch {
18341
+ }
18342
+ continue;
18343
+ }
18344
+ this.lockManager.removePatch(patch.id);
18345
+ conflictAbsorbed++;
18346
+ continue;
18347
+ }
18348
+ const snapHasStaleMarkers = snapshotDiff.split("\n").some(
18349
+ (l) => l.startsWith("+<<<<<<< Generated") || l.startsWith("+>>>>>>> Your customization")
18350
+ );
18351
+ if (snapHasStaleMarkers) continue;
18352
+ const snapHash = this.detector.computeContentHash(snapshotDiff);
18353
+ this.lockManager.updatePatch(patch.id, {
18354
+ base_generation: currentGen,
18355
+ patch_content: snapshotDiff,
18356
+ content_hash: snapHash
18357
+ });
18358
+ conflictResolved++;
18359
+ continue;
18360
+ }
18230
18361
  const ppResult = await computePerPatchDiffWithFallback(
18231
18362
  patch,
18232
18363
  (f) => this.git.showFile(currentGen, f),