@fern-api/replay 0.16.2 → 0.17.4

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
@@ -963,6 +963,22 @@ var ReplayDetector = class {
963
963
  return commit.sha;
964
964
  }
965
965
  }
966
+ let fullLog;
967
+ try {
968
+ fullLog = await this.git.exec([
969
+ "log",
970
+ "--no-merges",
971
+ "--format=%H%x00%an%x00%ae%x00%s",
972
+ "HEAD"
973
+ ]);
974
+ } catch {
975
+ return null;
976
+ }
977
+ for (const commit of this.parseGitLog(fullLog)) {
978
+ if (isGenerationBoundary(commit)) {
979
+ return commit.sha;
980
+ }
981
+ }
966
982
  return null;
967
983
  }
968
984
  async detectNewPatches() {
@@ -974,6 +990,11 @@ var ReplayDetector = class {
974
990
  }
975
991
  const lastGen = this.getLastGeneration(lock);
976
992
  if (!lastGen) {
993
+ if (lock.current_generation) {
994
+ this.warnings.push(
995
+ `No generation boundary found in git history and the lockfile's recorded generation (${lock.current_generation.slice(0, 7)}) matches no entry in its generation records. Customer customizations may differ from the last known generation and could be lost on the next regeneration. Run \`fern-replay bootstrap --force\` to re-initialize tracking.`
996
+ );
997
+ }
977
998
  return { patches: [], revertedPatchIds: [] };
978
999
  }
979
1000
  const exists = await this.git.commitExists(lastGen.commit_sha);
@@ -1728,16 +1749,20 @@ async function resolveInputs(args) {
1728
1749
  const oursPath = (0, import_node_path3.join)(outputDir, resolvedPath);
1729
1750
  const ours = await (0, import_promises.readFile)(oursPath, "utf-8").catch(() => null);
1730
1751
  let ghostTheirs = null;
1731
- if (!base && ours && !renameSourcePath) {
1752
+ let baseTreeUnreachable = false;
1753
+ if (!base && !renameSourcePath) {
1732
1754
  const treeReachable = await isTreeReachable(baseGen.tree_hash);
1733
1755
  if (!treeReachable) {
1734
- const fileDiff = extractFileDiff2(patch.patch_content, filePath);
1735
- if (fileDiff) {
1736
- const { reconstructFromGhostPatch: reconstructFromGhostPatch2 } = await Promise.resolve().then(() => (init_HybridReconstruction(), HybridReconstruction_exports));
1737
- const result = reconstructFromGhostPatch2(fileDiff, ours);
1738
- if (result) {
1739
- base = result.base;
1740
- ghostTheirs = result.theirs;
1756
+ baseTreeUnreachable = true;
1757
+ if (ours) {
1758
+ const fileDiff = extractFileDiff2(patch.patch_content, filePath);
1759
+ if (fileDiff) {
1760
+ const { reconstructFromGhostPatch: reconstructFromGhostPatch2 } = await Promise.resolve().then(() => (init_HybridReconstruction(), HybridReconstruction_exports));
1761
+ const result = reconstructFromGhostPatch2(fileDiff, ours);
1762
+ if (result) {
1763
+ base = result.base;
1764
+ ghostTheirs = result.theirs;
1765
+ }
1741
1766
  }
1742
1767
  }
1743
1768
  }
@@ -1749,7 +1774,11 @@ async function resolveInputs(args) {
1749
1774
  ours,
1750
1775
  oursPath,
1751
1776
  renameSourcePath,
1752
- ghostTheirs
1777
+ ghostTheirs,
1778
+ // Only meaningful when ghost reconstruction did NOT recover a base; if it
1779
+ // did, `base` is non-null and the merge proceeds normally. We still set
1780
+ // the flag truthfully (the tree IS unreachable) so Phase 3 can decide.
1781
+ baseTreeUnreachable
1753
1782
  };
1754
1783
  }
1755
1784
 
@@ -1932,7 +1961,11 @@ async function buildMergePlan(args) {
1932
1961
  return { kind: "new-file-only-user", theirs: effective_theirs };
1933
1962
  }
1934
1963
  if (base == null && ours != null && effective_theirs != null && !useAccumulatorAsMergeBase) {
1935
- return { kind: "new-file-both", theirs: effective_theirs };
1964
+ return {
1965
+ kind: "new-file-both",
1966
+ theirs: effective_theirs,
1967
+ ...inputs.baseTreeUnreachable ? { baseTreeUnreachable: true } : {}
1968
+ };
1936
1969
  }
1937
1970
  if (effective_theirs == null) {
1938
1971
  return { kind: "skipped", reason: "missing-content" };
@@ -1980,17 +2013,19 @@ async function runMergeAndRecord(args) {
1980
2013
  const merged2 = threeWayMerge("", ours, plan.theirs);
1981
2014
  await (0, import_promises2.mkdir)((0, import_node_path4.dirname)(oursPath), { recursive: true });
1982
2015
  await (0, import_promises2.writeFile)(oursPath, merged2.content);
2016
+ const unreachableFlag = plan.baseTreeUnreachable ? { baseTreeUnreachable: true } : {};
1983
2017
  if (merged2.hasConflicts) {
1984
2018
  return {
1985
2019
  file: resolvedPath,
1986
2020
  status: "conflict",
1987
2021
  conflicts: merged2.conflicts,
1988
2022
  conflictReason: "new-file-both",
1989
- conflictMetadata: metadata
2023
+ conflictMetadata: metadata,
2024
+ ...unreachableFlag
1990
2025
  };
1991
2026
  }
1992
2027
  accumulator.recordMerge(resolvedPath, merged2.content, patch.base_generation);
1993
- return { file: resolvedPath, status: "merged" };
2028
+ return { file: resolvedPath, status: "merged", ...unreachableFlag };
1994
2029
  }
1995
2030
  const merged = threeWayMerge(plan.mergeBase, ours, plan.theirs);
1996
2031
  await (0, import_promises2.mkdir)((0, import_node_path4.dirname)(oursPath), { recursive: true });
@@ -3206,6 +3241,7 @@ var NoPatchesFlow = class {
3206
3241
  results = await this.service.applicator.applyPatches(newPatches);
3207
3242
  this.service._lastApplyResults = results;
3208
3243
  this.service.recordDroppedContextLineWarnings(results);
3244
+ this.service.recordUnreachableBaseWarnings(results);
3209
3245
  this.service.revertConflictingFiles(results);
3210
3246
  for (const result of results) {
3211
3247
  if (result.status === "conflict") {
@@ -3361,6 +3397,7 @@ var NormalRegenerationFlow = class {
3361
3397
  const results = await this.service.applicator.applyPatches(allPatches);
3362
3398
  this.service._lastApplyResults = results;
3363
3399
  this.service.recordDroppedContextLineWarnings(results);
3400
+ this.service.recordUnreachableBaseWarnings(results);
3364
3401
  this.service.revertConflictingFiles(results);
3365
3402
  for (const result of results) {
3366
3403
  if (result.status === "conflict") {
@@ -4280,6 +4317,37 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
4280
4317
  }
4281
4318
  }
4282
4319
  }
4320
+ /**
4321
+ * After applyPatches(), surface a warning for each (patch × file) where a
4322
+ * customization's BASE could not be read because the patch's
4323
+ * base-generation tree is unreachable in this clone (shallow-clone window
4324
+ * too small / pruned history). In that state the merge pipeline falls
4325
+ * through to `new-file-both` and the customer's edit can be dropped with NO
4326
+ * conflict and NO error — the INC-866 silent-loss class. This converts that
4327
+ * silence into an explicit, actionable warning naming the file and the
4328
+ * likely cause (insufficient clone window).
4329
+ *
4330
+ * Mirrors `recordDroppedContextLineWarnings`'s "surface, never silently
4331
+ * drop" contract. De-duplicated per file path so a single insufficient
4332
+ * window doesn't spam one warning per touched file repeatedly.
4333
+ */
4334
+ /** @internal Used by Flow implementations in `src/flows/`. */
4335
+ recordUnreachableBaseWarnings(results) {
4336
+ const warned = /* @__PURE__ */ new Set();
4337
+ for (const result of results) {
4338
+ if (!result.fileResults) continue;
4339
+ for (const fr of result.fileResults) {
4340
+ if (!fr.baseTreeUnreachable) continue;
4341
+ if (warned.has(fr.file)) continue;
4342
+ warned.add(fr.file);
4343
+ const base = fr.conflictMetadata?.baseGeneration;
4344
+ const baseRef = base ? ` (base generation ${base.slice(0, 7)})` : "";
4345
+ this.warnings.push(
4346
+ `${fr.file}: could not read the prior generation's version of this file${baseRef} because that generation's tree is outside the clone window (insufficient shallow-clone depth or pruned history). The customization was treated as a new file, which can drop your edits silently. Re-run with the prior generation in the clone window \u2014 e.g. \`git fetch --shallow-since=<date just before the prior [fern-generated] commit>\` or \`git fetch --unshallow\` \u2014 then re-run replay.`
4347
+ );
4348
+ }
4349
+ }
4350
+ }
4283
4351
  /**
4284
4352
  * After applyPatches(), strip conflict markers from conflicting files
4285
4353
  * so only clean content is committed. Keeps the Generated (OURS) side.