@fern-api/replay 0.19.0 → 0.19.1

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
@@ -970,6 +970,21 @@ declare class ReplayService {
970
970
  * never be used during apply anyway).
971
971
  */
972
972
  private readSnapshotFromDisk;
973
+ /**
974
+ * Autoversion-aware replacement for the naive `git diff currentGen -- files`
975
+ * + on-disk snapshot used when rebasing a cleanly-applied patch.
976
+ *
977
+ * The generation tree (BASE) still carries the placeholder on version lines
978
+ * (autoversion runs after `[fern-generated]`), but the working tree holds
979
+ * autoversion's resolved semver. A raw diff/snapshot would fold the
980
+ * placeholder->semver swap into the customer's patch_content/theirs_snapshot,
981
+ * pinning a stale version. This re-derives both against a version-neutralized
982
+ * THEIRS so the pipeline-owned version line never enters the patch.
983
+ *
984
+ * Returns null when no file is under autoversion (fixed `--version`, i.e. no
985
+ * placeholder in the generation tree) — callers keep their existing behavior.
986
+ */
987
+ private computeAutoversionNeutralizedRebase;
973
988
  /**
974
989
  * Detects autoversion contamination — returns true when a
975
990
  * `[fern-autoversion]` commit between `fromSha` and `toSha` touched any
package/dist/index.d.ts CHANGED
@@ -970,6 +970,21 @@ declare class ReplayService {
970
970
  * never be used during apply anyway).
971
971
  */
972
972
  private readSnapshotFromDisk;
973
+ /**
974
+ * Autoversion-aware replacement for the naive `git diff currentGen -- files`
975
+ * + on-disk snapshot used when rebasing a cleanly-applied patch.
976
+ *
977
+ * The generation tree (BASE) still carries the placeholder on version lines
978
+ * (autoversion runs after `[fern-generated]`), but the working tree holds
979
+ * autoversion's resolved semver. A raw diff/snapshot would fold the
980
+ * placeholder->semver swap into the customer's patch_content/theirs_snapshot,
981
+ * pinning a stale version. This re-derives both against a version-neutralized
982
+ * THEIRS so the pipeline-owned version line never enters the patch.
983
+ *
984
+ * Returns null when no file is under autoversion (fixed `--version`, i.e. no
985
+ * placeholder in the generation tree) — callers keep their existing behavior.
986
+ */
987
+ private computeAutoversionNeutralizedRebase;
973
988
  /**
974
989
  * Detects autoversion contamination — returns true when a
975
990
  * `[fern-autoversion]` commit between `fromSha` and `toSha` touched any
package/dist/index.js CHANGED
@@ -2027,6 +2027,51 @@ async function buildMergePlan(args) {
2027
2027
  // src/applicator/runMergeAndRecord.ts
2028
2028
  import { mkdir, writeFile } from "fs/promises";
2029
2029
  import { dirname as dirname2 } from "path";
2030
+
2031
+ // src/autoversion.ts
2032
+ var AUTOVERSION_PLACEHOLDERS = [
2033
+ "v0.0.0-fern-placeholder",
2034
+ // Go -- a superstring of the default; check first
2035
+ "0.0.0-fern-placeholder",
2036
+ // TS/Java/C#/Ruby/PHP/Rust/default
2037
+ "0.0.0.dev0"
2038
+ // Python (PEP 440 -- Poetry rejects hyphens)
2039
+ ];
2040
+ var VERSION_TOKEN = /(?:v)?\d+\.\d+\.\d+[0-9A-Za-z.+-]*/g;
2041
+ var VERSION_MASK = "@@FERN_VERSION@@";
2042
+ function maskVersions(line) {
2043
+ return line.replace(VERSION_TOKEN, VERSION_MASK);
2044
+ }
2045
+ function containsPlaceholder(content) {
2046
+ if (!content) return false;
2047
+ return AUTOVERSION_PLACEHOLDERS.some((p) => content.includes(p));
2048
+ }
2049
+ function remapManagedLines(genBase, target, theirs) {
2050
+ if (!containsPlaceholder(genBase)) return theirs;
2051
+ const managedMasks = /* @__PURE__ */ new Set();
2052
+ for (const line of genBase.split("\n")) {
2053
+ if (AUTOVERSION_PLACEHOLDERS.some((p) => line.includes(p))) managedMasks.add(maskVersions(line));
2054
+ }
2055
+ if (managedMasks.size === 0) return theirs;
2056
+ const targetByMask = /* @__PURE__ */ new Map();
2057
+ for (const line of target.split("\n")) {
2058
+ const mask = maskVersions(line);
2059
+ if (managedMasks.has(mask) && !targetByMask.has(mask)) targetByMask.set(mask, line);
2060
+ }
2061
+ return theirs.split("\n").map((line) => {
2062
+ const mask = maskVersions(line);
2063
+ const replacement = managedMasks.has(mask) ? targetByMask.get(mask) : void 0;
2064
+ return replacement !== void 0 && replacement !== line ? replacement : line;
2065
+ }).join("\n");
2066
+ }
2067
+ function neutralizeAutoversionTheirs(genBase, theirs) {
2068
+ return remapManagedLines(genBase, genBase, theirs);
2069
+ }
2070
+ function alignAutoversionToOurs(genBase, ours, theirs) {
2071
+ return remapManagedLines(genBase, ours, theirs);
2072
+ }
2073
+
2074
+ // src/applicator/runMergeAndRecord.ts
2030
2075
  async function runMergeAndRecord(args) {
2031
2076
  const { plan, inputs, patch, accumulator } = args;
2032
2077
  const { resolvedPath, metadata, oursPath, ours } = inputs;
@@ -2057,11 +2102,13 @@ async function runMergeAndRecord(args) {
2057
2102
  accumulator.recordMerge(resolvedPath, merged2.content, patch.base_generation);
2058
2103
  return { file: resolvedPath, status: "merged", ...unreachableFlag };
2059
2104
  }
2060
- const merged = threeWayMerge(plan.mergeBase, ours, plan.theirs);
2105
+ const mergeBase = inputs.base != null ? alignAutoversionToOurs(inputs.base, ours, plan.mergeBase) : plan.mergeBase;
2106
+ const theirs = inputs.base != null ? alignAutoversionToOurs(inputs.base, ours, plan.theirs) : plan.theirs;
2107
+ const merged = threeWayMerge(mergeBase, ours, theirs);
2061
2108
  await mkdir(dirname2(oursPath), { recursive: true });
2062
2109
  await writeFile(oursPath, merged.content);
2063
2110
  if (merged.hasConflicts) {
2064
- accumulator.recordConflict(resolvedPath, plan.theirs, patch.base_generation);
2111
+ accumulator.recordConflict(resolvedPath, theirs, patch.base_generation);
2065
2112
  return {
2066
2113
  file: resolvedPath,
2067
2114
  status: "conflict",
@@ -2070,7 +2117,7 @@ async function runMergeAndRecord(args) {
2070
2117
  conflictMetadata: metadata
2071
2118
  };
2072
2119
  }
2073
- accumulator.recordMerge(resolvedPath, plan.theirs, patch.base_generation);
2120
+ accumulator.recordMerge(resolvedPath, theirs, patch.base_generation);
2074
2121
  return {
2075
2122
  file: resolvedPath,
2076
2123
  status: "merged",
@@ -3856,7 +3903,9 @@ var ReplayService = class {
3856
3903
  if (baseChanged) repointed++;
3857
3904
  continue;
3858
3905
  }
3859
- const diff = await this.git.exec(["diff", currentGenSha, "--", ...patch.files]).catch(() => null);
3906
+ const rawDiff = await this.git.exec(["diff", currentGenSha, "--", ...patch.files]).catch(() => null);
3907
+ const neutralizedRebase = rawDiff != null && containsPlaceholder(rawDiff) ? await this.computeAutoversionNeutralizedRebase(patch.files, currentGenSha) : null;
3908
+ const diff = neutralizedRebase ? neutralizedRebase.diff : rawDiff;
3860
3909
  if (diff === null) continue;
3861
3910
  if (!diff.trim()) {
3862
3911
  if (hasProtectedFiles) {
@@ -3912,7 +3961,7 @@ var ReplayService = class {
3912
3961
  patch.base_generation = currentGenSha;
3913
3962
  patch.patch_content = diff;
3914
3963
  patch.content_hash = newContentHash;
3915
- const snapshot = await this.readSnapshotFromDisk(patch.files);
3964
+ const snapshot = neutralizedRebase ? neutralizedRebase.snapshot : await this.readSnapshotFromDisk(patch.files);
3916
3965
  const snapshotIsNonEmpty = Object.keys(snapshot).length > 0;
3917
3966
  if (snapshotIsNonEmpty) {
3918
3967
  patch.theirs_snapshot = snapshot;
@@ -3952,6 +4001,47 @@ var ReplayService = class {
3952
4001
  }
3953
4002
  return snapshot;
3954
4003
  }
4004
+ /**
4005
+ * Autoversion-aware replacement for the naive `git diff currentGen -- files`
4006
+ * + on-disk snapshot used when rebasing a cleanly-applied patch.
4007
+ *
4008
+ * The generation tree (BASE) still carries the placeholder on version lines
4009
+ * (autoversion runs after `[fern-generated]`), but the working tree holds
4010
+ * autoversion's resolved semver. A raw diff/snapshot would fold the
4011
+ * placeholder->semver swap into the customer's patch_content/theirs_snapshot,
4012
+ * pinning a stale version. This re-derives both against a version-neutralized
4013
+ * THEIRS so the pipeline-owned version line never enters the patch.
4014
+ *
4015
+ * Returns null when no file is under autoversion (fixed `--version`, i.e. no
4016
+ * placeholder in the generation tree) — callers keep their existing behavior.
4017
+ */
4018
+ async computeAutoversionNeutralizedRebase(files, currentGenSha) {
4019
+ const genByFile = /* @__PURE__ */ new Map();
4020
+ let anyPlaceholder = false;
4021
+ for (const file of files) {
4022
+ const gen = await this.git.showFile(currentGenSha, file).catch(() => null);
4023
+ genByFile.set(file, gen);
4024
+ if (containsPlaceholder(gen)) anyPlaceholder = true;
4025
+ }
4026
+ if (!anyPlaceholder) return null;
4027
+ const snapshot = {};
4028
+ const fragments = [];
4029
+ for (const file of files) {
4030
+ if (isBinaryFile(file)) continue;
4031
+ const disk = await readFileAsync(join7(this.outputDir, file), "utf-8").catch(() => null);
4032
+ if (disk == null) continue;
4033
+ const gen = genByFile.get(file) ?? null;
4034
+ const neutralized = gen != null ? neutralizeAutoversionTheirs(gen, disk) : disk;
4035
+ snapshot[file] = neutralized;
4036
+ if (gen == null) {
4037
+ fragments.push(synthesizeNewFileDiff(file, neutralized));
4038
+ } else if (gen !== neutralized) {
4039
+ const fileDiff = await unifiedDiff(gen, neutralized, file);
4040
+ if (fileDiff.trim()) fragments.push(fileDiff);
4041
+ }
4042
+ }
4043
+ return { diff: fragments.join(""), snapshot };
4044
+ }
3955
4045
  /**
3956
4046
  * Detects autoversion contamination — returns true when a
3957
4047
  * `[fern-autoversion]` commit between `fromSha` and `toSha` touched any