@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.d.cts CHANGED
@@ -734,6 +734,20 @@ declare class ReplayService {
734
734
  * never be used during apply anyway).
735
735
  */
736
736
  private readSnapshotFromDisk;
737
+ /**
738
+ * FER-10203: returns true when a `[fern-autoversion]` commit between
739
+ * `fromSha` and `toSha` touched any of the patch's `files`. Re-classifies
740
+ * each grep match via `isGenerationCommit` so a customer commit that
741
+ * merely quotes the marker in its body doesn't false-positive.
742
+ */
743
+ private hasAutoversionContamination;
744
+ /**
745
+ * FER-10203: rebuild patch_content from `theirs_snapshot` (captured at
746
+ * detection time, predates pipeline content) rather than a HEAD-relative
747
+ * diff. Returns `null` when snapshot is missing or doesn't cover every
748
+ * file in `patch.files` — caller leaves the patch unchanged.
749
+ */
750
+ private computeSnapshotBasedPatchDiff;
737
751
  /**
738
752
  * Read THEIRS snapshot from a git tree-ish (commit, branch, or tree).
739
753
  * Used when the diff that produced `patch_content` was relative to that
package/dist/index.d.ts CHANGED
@@ -734,6 +734,20 @@ declare class ReplayService {
734
734
  * never be used during apply anyway).
735
735
  */
736
736
  private readSnapshotFromDisk;
737
+ /**
738
+ * FER-10203: returns true when a `[fern-autoversion]` commit between
739
+ * `fromSha` and `toSha` touched any of the patch's `files`. Re-classifies
740
+ * each grep match via `isGenerationCommit` so a customer commit that
741
+ * merely quotes the marker in its body doesn't false-positive.
742
+ */
743
+ private hasAutoversionContamination;
744
+ /**
745
+ * FER-10203: rebuild patch_content from `theirs_snapshot` (captured at
746
+ * detection time, predates pipeline content) rather than a HEAD-relative
747
+ * diff. Returns `null` when snapshot is missing or doesn't cover every
748
+ * file in `patch.files` — caller leaves the patch unchanged.
749
+ */
750
+ private computeSnapshotBasedPatchDiff;
737
751
  /**
738
752
  * Read THEIRS snapshot from a git tree-ish (commit, branch, or tree).
739
753
  * Used when the diff that produced `patch_content` was relative to that
package/dist/index.js CHANGED
@@ -3496,6 +3496,66 @@ var ReplayService = class {
3496
3496
  }
3497
3497
  return snapshot;
3498
3498
  }
3499
+ /**
3500
+ * FER-10203: returns true when a `[fern-autoversion]` commit between
3501
+ * `fromSha` and `toSha` touched any of the patch's `files`. Re-classifies
3502
+ * each grep match via `isGenerationCommit` so a customer commit that
3503
+ * merely quotes the marker in its body doesn't false-positive.
3504
+ */
3505
+ async hasAutoversionContamination(fromSha, toSha, files) {
3506
+ if (files.length === 0) return false;
3507
+ const log = await this.git.exec([
3508
+ "log",
3509
+ `${fromSha}..${toSha}`,
3510
+ "--grep=\\[fern-autoversion\\]",
3511
+ "--extended-regexp",
3512
+ "--format=%H%x09%aN%x09%ae%x09%s",
3513
+ "--",
3514
+ ...files
3515
+ ]).catch(() => "");
3516
+ if (!log.trim()) return false;
3517
+ for (const line of log.trim().split("\n")) {
3518
+ const [sha, authorName, authorEmail, message] = line.split(" ");
3519
+ if (sha == null) continue;
3520
+ if (isGenerationCommit({
3521
+ sha,
3522
+ authorName: authorName ?? "",
3523
+ authorEmail: authorEmail ?? "",
3524
+ message: message ?? ""
3525
+ })) {
3526
+ return true;
3527
+ }
3528
+ }
3529
+ return false;
3530
+ }
3531
+ /**
3532
+ * FER-10203: rebuild patch_content from `theirs_snapshot` (captured at
3533
+ * detection time, predates pipeline content) rather than a HEAD-relative
3534
+ * diff. Returns `null` when snapshot is missing or doesn't cover every
3535
+ * file in `patch.files` — caller leaves the patch unchanged.
3536
+ */
3537
+ async computeSnapshotBasedPatchDiff(patch, currentGen) {
3538
+ if (!patch.theirs_snapshot) return null;
3539
+ if (Object.keys(patch.theirs_snapshot).length === 0) return null;
3540
+ for (const file of patch.files) {
3541
+ if (patch.theirs_snapshot[file] == null) return null;
3542
+ }
3543
+ const fragments = [];
3544
+ for (const file of patch.files) {
3545
+ const theirsContent = patch.theirs_snapshot[file];
3546
+ const oursContent = await this.git.showFile(currentGen, file).catch(() => null);
3547
+ if (oursContent == null) {
3548
+ fragments.push(synthesizeNewFileDiff(file, theirsContent));
3549
+ continue;
3550
+ }
3551
+ if (oursContent === theirsContent) continue;
3552
+ const fileDiff = await unifiedDiff(oursContent, theirsContent, file);
3553
+ if (fileDiff.trim()) {
3554
+ fragments.push(fileDiff);
3555
+ }
3556
+ }
3557
+ return fragments.join("");
3558
+ }
3499
3559
  /**
3500
3560
  * Read THEIRS snapshot from a git tree-ish (commit, branch, or tree).
3501
3561
  * Used when the diff that produced `patch_content` was relative to that
@@ -3761,6 +3821,41 @@ var ReplayService = class {
3761
3821
  }
3762
3822
  if (patch.base_generation === currentGen) {
3763
3823
  try {
3824
+ const contaminated = await this.hasAutoversionContamination(
3825
+ currentGen,
3826
+ "HEAD",
3827
+ patch.files
3828
+ );
3829
+ if (contaminated) {
3830
+ const snapshotDiff = await this.computeSnapshotBasedPatchDiff(
3831
+ patch,
3832
+ currentGen
3833
+ );
3834
+ if (snapshotDiff === null) continue;
3835
+ if (!snapshotDiff.trim()) {
3836
+ if (await allPatchFilesUserOwned(patch)) continue;
3837
+ this.lockManager.removePatch(patch.id);
3838
+ contentRefreshed++;
3839
+ continue;
3840
+ }
3841
+ const snapHasStaleMarkers = snapshotDiff.split("\n").some(
3842
+ (l) => l.startsWith("+<<<<<<< Generated") || l.startsWith("+>>>>>>> Your customization")
3843
+ );
3844
+ if (snapHasStaleMarkers) continue;
3845
+ const isProtectedSnap = patch.files.some(
3846
+ (file) => file === ".fernignore" || fernignorePatterns.some((p) => file === p || minimatch2(file, p))
3847
+ );
3848
+ if (isProtectedSnap) continue;
3849
+ const snapHash = this.detector.computeContentHash(snapshotDiff);
3850
+ if (snapHash !== patch.content_hash) {
3851
+ this.lockManager.updatePatch(patch.id, {
3852
+ patch_content: snapshotDiff,
3853
+ content_hash: snapHash
3854
+ });
3855
+ contentRefreshed++;
3856
+ }
3857
+ continue;
3858
+ }
3764
3859
  const ppResult = await computePerPatchDiffWithFallback(
3765
3860
  patch,
3766
3861
  (f) => this.git.showFile(currentGen, f),
@@ -3812,6 +3907,42 @@ var ReplayService = class {
3812
3907
  try {
3813
3908
  const markerFiles = await this.git.exec(["grep", "-l", "<<<<<<< Generated", "HEAD", "--", ...patch.files]).catch(() => "");
3814
3909
  if (markerFiles.trim()) continue;
3910
+ const contaminated = await this.hasAutoversionContamination(
3911
+ currentGen,
3912
+ "HEAD",
3913
+ patch.files
3914
+ );
3915
+ if (contaminated) {
3916
+ const snapshotDiff = await this.computeSnapshotBasedPatchDiff(
3917
+ patch,
3918
+ currentGen
3919
+ );
3920
+ if (snapshotDiff === null) continue;
3921
+ if (!snapshotDiff.trim()) {
3922
+ if (await allPatchFilesUserOwned(patch)) {
3923
+ try {
3924
+ this.lockManager.updatePatch(patch.id, { base_generation: currentGen });
3925
+ } catch {
3926
+ }
3927
+ continue;
3928
+ }
3929
+ this.lockManager.removePatch(patch.id);
3930
+ conflictAbsorbed++;
3931
+ continue;
3932
+ }
3933
+ const snapHasStaleMarkers = snapshotDiff.split("\n").some(
3934
+ (l) => l.startsWith("+<<<<<<< Generated") || l.startsWith("+>>>>>>> Your customization")
3935
+ );
3936
+ if (snapHasStaleMarkers) continue;
3937
+ const snapHash = this.detector.computeContentHash(snapshotDiff);
3938
+ this.lockManager.updatePatch(patch.id, {
3939
+ base_generation: currentGen,
3940
+ patch_content: snapshotDiff,
3941
+ content_hash: snapHash
3942
+ });
3943
+ conflictResolved++;
3944
+ continue;
3945
+ }
3815
3946
  const ppResult = await computePerPatchDiffWithFallback(
3816
3947
  patch,
3817
3948
  (f) => this.git.showFile(currentGen, f),