@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/index.cjs CHANGED
@@ -3551,6 +3551,66 @@ var ReplayService = class {
3551
3551
  }
3552
3552
  return snapshot;
3553
3553
  }
3554
+ /**
3555
+ * FER-10203: returns true when a `[fern-autoversion]` commit between
3556
+ * `fromSha` and `toSha` touched any of the patch's `files`. Re-classifies
3557
+ * each grep match via `isGenerationCommit` so a customer commit that
3558
+ * merely quotes the marker in its body doesn't false-positive.
3559
+ */
3560
+ async hasAutoversionContamination(fromSha, toSha, files) {
3561
+ if (files.length === 0) return false;
3562
+ const log = await this.git.exec([
3563
+ "log",
3564
+ `${fromSha}..${toSha}`,
3565
+ "--grep=\\[fern-autoversion\\]",
3566
+ "--extended-regexp",
3567
+ "--format=%H%x09%aN%x09%ae%x09%s",
3568
+ "--",
3569
+ ...files
3570
+ ]).catch(() => "");
3571
+ if (!log.trim()) return false;
3572
+ for (const line of log.trim().split("\n")) {
3573
+ const [sha, authorName, authorEmail, message] = line.split(" ");
3574
+ if (sha == null) continue;
3575
+ if (isGenerationCommit({
3576
+ sha,
3577
+ authorName: authorName ?? "",
3578
+ authorEmail: authorEmail ?? "",
3579
+ message: message ?? ""
3580
+ })) {
3581
+ return true;
3582
+ }
3583
+ }
3584
+ return false;
3585
+ }
3586
+ /**
3587
+ * FER-10203: rebuild patch_content from `theirs_snapshot` (captured at
3588
+ * detection time, predates pipeline content) rather than a HEAD-relative
3589
+ * diff. Returns `null` when snapshot is missing or doesn't cover every
3590
+ * file in `patch.files` — caller leaves the patch unchanged.
3591
+ */
3592
+ async computeSnapshotBasedPatchDiff(patch, currentGen) {
3593
+ if (!patch.theirs_snapshot) return null;
3594
+ if (Object.keys(patch.theirs_snapshot).length === 0) return null;
3595
+ for (const file of patch.files) {
3596
+ if (patch.theirs_snapshot[file] == null) return null;
3597
+ }
3598
+ const fragments = [];
3599
+ for (const file of patch.files) {
3600
+ const theirsContent = patch.theirs_snapshot[file];
3601
+ const oursContent = await this.git.showFile(currentGen, file).catch(() => null);
3602
+ if (oursContent == null) {
3603
+ fragments.push(synthesizeNewFileDiff(file, theirsContent));
3604
+ continue;
3605
+ }
3606
+ if (oursContent === theirsContent) continue;
3607
+ const fileDiff = await unifiedDiff(oursContent, theirsContent, file);
3608
+ if (fileDiff.trim()) {
3609
+ fragments.push(fileDiff);
3610
+ }
3611
+ }
3612
+ return fragments.join("");
3613
+ }
3554
3614
  /**
3555
3615
  * Read THEIRS snapshot from a git tree-ish (commit, branch, or tree).
3556
3616
  * Used when the diff that produced `patch_content` was relative to that
@@ -3816,6 +3876,41 @@ var ReplayService = class {
3816
3876
  }
3817
3877
  if (patch.base_generation === currentGen) {
3818
3878
  try {
3879
+ const contaminated = await this.hasAutoversionContamination(
3880
+ currentGen,
3881
+ "HEAD",
3882
+ patch.files
3883
+ );
3884
+ if (contaminated) {
3885
+ const snapshotDiff = await this.computeSnapshotBasedPatchDiff(
3886
+ patch,
3887
+ currentGen
3888
+ );
3889
+ if (snapshotDiff === null) continue;
3890
+ if (!snapshotDiff.trim()) {
3891
+ if (await allPatchFilesUserOwned(patch)) continue;
3892
+ this.lockManager.removePatch(patch.id);
3893
+ contentRefreshed++;
3894
+ continue;
3895
+ }
3896
+ const snapHasStaleMarkers = snapshotDiff.split("\n").some(
3897
+ (l) => l.startsWith("+<<<<<<< Generated") || l.startsWith("+>>>>>>> Your customization")
3898
+ );
3899
+ if (snapHasStaleMarkers) continue;
3900
+ const isProtectedSnap = patch.files.some(
3901
+ (file) => file === ".fernignore" || fernignorePatterns.some((p) => file === p || (0, import_minimatch2.minimatch)(file, p))
3902
+ );
3903
+ if (isProtectedSnap) continue;
3904
+ const snapHash = this.detector.computeContentHash(snapshotDiff);
3905
+ if (snapHash !== patch.content_hash) {
3906
+ this.lockManager.updatePatch(patch.id, {
3907
+ patch_content: snapshotDiff,
3908
+ content_hash: snapHash
3909
+ });
3910
+ contentRefreshed++;
3911
+ }
3912
+ continue;
3913
+ }
3819
3914
  const ppResult = await computePerPatchDiffWithFallback(
3820
3915
  patch,
3821
3916
  (f) => this.git.showFile(currentGen, f),
@@ -3867,6 +3962,42 @@ var ReplayService = class {
3867
3962
  try {
3868
3963
  const markerFiles = await this.git.exec(["grep", "-l", "<<<<<<< Generated", "HEAD", "--", ...patch.files]).catch(() => "");
3869
3964
  if (markerFiles.trim()) continue;
3965
+ const contaminated = await this.hasAutoversionContamination(
3966
+ currentGen,
3967
+ "HEAD",
3968
+ patch.files
3969
+ );
3970
+ if (contaminated) {
3971
+ const snapshotDiff = await this.computeSnapshotBasedPatchDiff(
3972
+ patch,
3973
+ currentGen
3974
+ );
3975
+ if (snapshotDiff === null) continue;
3976
+ if (!snapshotDiff.trim()) {
3977
+ if (await allPatchFilesUserOwned(patch)) {
3978
+ try {
3979
+ this.lockManager.updatePatch(patch.id, { base_generation: currentGen });
3980
+ } catch {
3981
+ }
3982
+ continue;
3983
+ }
3984
+ this.lockManager.removePatch(patch.id);
3985
+ conflictAbsorbed++;
3986
+ continue;
3987
+ }
3988
+ const snapHasStaleMarkers = snapshotDiff.split("\n").some(
3989
+ (l) => l.startsWith("+<<<<<<< Generated") || l.startsWith("+>>>>>>> Your customization")
3990
+ );
3991
+ if (snapHasStaleMarkers) continue;
3992
+ const snapHash = this.detector.computeContentHash(snapshotDiff);
3993
+ this.lockManager.updatePatch(patch.id, {
3994
+ base_generation: currentGen,
3995
+ patch_content: snapshotDiff,
3996
+ content_hash: snapHash
3997
+ });
3998
+ conflictResolved++;
3999
+ continue;
4000
+ }
3870
4001
  const ppResult = await computePerPatchDiffWithFallback(
3871
4002
  patch,
3872
4003
  (f) => this.git.showFile(currentGen, f),