@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/cli.cjs CHANGED
@@ -15081,6 +15081,22 @@ var ReplayDetector = class {
15081
15081
  return commit.sha;
15082
15082
  }
15083
15083
  }
15084
+ let fullLog;
15085
+ try {
15086
+ fullLog = await this.git.exec([
15087
+ "log",
15088
+ "--no-merges",
15089
+ "--format=%H%x00%an%x00%ae%x00%s",
15090
+ "HEAD"
15091
+ ]);
15092
+ } catch {
15093
+ return null;
15094
+ }
15095
+ for (const commit of this.parseGitLog(fullLog)) {
15096
+ if (isGenerationBoundary(commit)) {
15097
+ return commit.sha;
15098
+ }
15099
+ }
15084
15100
  return null;
15085
15101
  }
15086
15102
  async detectNewPatches() {
@@ -15092,6 +15108,11 @@ var ReplayDetector = class {
15092
15108
  }
15093
15109
  const lastGen = this.getLastGeneration(lock);
15094
15110
  if (!lastGen) {
15111
+ if (lock.current_generation) {
15112
+ this.warnings.push(
15113
+ `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.`
15114
+ );
15115
+ }
15095
15116
  return { patches: [], revertedPatchIds: [] };
15096
15117
  }
15097
15118
  const exists2 = await this.git.commitExists(lastGen.commit_sha);
@@ -15712,16 +15733,20 @@ async function resolveInputs(args) {
15712
15733
  const oursPath = (0, import_node_path4.join)(outputDir, resolvedPath);
15713
15734
  const ours = await (0, import_promises.readFile)(oursPath, "utf-8").catch(() => null);
15714
15735
  let ghostTheirs = null;
15715
- if (!base && ours && !renameSourcePath) {
15736
+ let baseTreeUnreachable = false;
15737
+ if (!base && !renameSourcePath) {
15716
15738
  const treeReachable = await isTreeReachable(baseGen.tree_hash);
15717
15739
  if (!treeReachable) {
15718
- const fileDiff = extractFileDiff2(patch.patch_content, filePath);
15719
- if (fileDiff) {
15720
- const { reconstructFromGhostPatch: reconstructFromGhostPatch2 } = await Promise.resolve().then(() => (init_HybridReconstruction(), HybridReconstruction_exports));
15721
- const result = reconstructFromGhostPatch2(fileDiff, ours);
15722
- if (result) {
15723
- base = result.base;
15724
- ghostTheirs = result.theirs;
15740
+ baseTreeUnreachable = true;
15741
+ if (ours) {
15742
+ const fileDiff = extractFileDiff2(patch.patch_content, filePath);
15743
+ if (fileDiff) {
15744
+ const { reconstructFromGhostPatch: reconstructFromGhostPatch2 } = await Promise.resolve().then(() => (init_HybridReconstruction(), HybridReconstruction_exports));
15745
+ const result = reconstructFromGhostPatch2(fileDiff, ours);
15746
+ if (result) {
15747
+ base = result.base;
15748
+ ghostTheirs = result.theirs;
15749
+ }
15725
15750
  }
15726
15751
  }
15727
15752
  }
@@ -15733,7 +15758,11 @@ async function resolveInputs(args) {
15733
15758
  ours,
15734
15759
  oursPath,
15735
15760
  renameSourcePath,
15736
- ghostTheirs
15761
+ ghostTheirs,
15762
+ // Only meaningful when ghost reconstruction did NOT recover a base; if it
15763
+ // did, `base` is non-null and the merge proceeds normally. We still set
15764
+ // the flag truthfully (the tree IS unreachable) so Phase 3 can decide.
15765
+ baseTreeUnreachable
15737
15766
  };
15738
15767
  }
15739
15768
 
@@ -16299,7 +16328,11 @@ async function buildMergePlan(args) {
16299
16328
  return { kind: "new-file-only-user", theirs: effective_theirs };
16300
16329
  }
16301
16330
  if (base == null && ours != null && effective_theirs != null && !useAccumulatorAsMergeBase) {
16302
- return { kind: "new-file-both", theirs: effective_theirs };
16331
+ return {
16332
+ kind: "new-file-both",
16333
+ theirs: effective_theirs,
16334
+ ...inputs.baseTreeUnreachable ? { baseTreeUnreachable: true } : {}
16335
+ };
16303
16336
  }
16304
16337
  if (effective_theirs == null) {
16305
16338
  return { kind: "skipped", reason: "missing-content" };
@@ -16347,17 +16380,19 @@ async function runMergeAndRecord(args) {
16347
16380
  const merged2 = threeWayMerge("", ours, plan.theirs);
16348
16381
  await (0, import_promises2.mkdir)((0, import_node_path5.dirname)(oursPath), { recursive: true });
16349
16382
  await (0, import_promises2.writeFile)(oursPath, merged2.content);
16383
+ const unreachableFlag = plan.baseTreeUnreachable ? { baseTreeUnreachable: true } : {};
16350
16384
  if (merged2.hasConflicts) {
16351
16385
  return {
16352
16386
  file: resolvedPath,
16353
16387
  status: "conflict",
16354
16388
  conflicts: merged2.conflicts,
16355
16389
  conflictReason: "new-file-both",
16356
- conflictMetadata: metadata
16390
+ conflictMetadata: metadata,
16391
+ ...unreachableFlag
16357
16392
  };
16358
16393
  }
16359
16394
  accumulator.recordMerge(resolvedPath, merged2.content, patch.base_generation);
16360
- return { file: resolvedPath, status: "merged" };
16395
+ return { file: resolvedPath, status: "merged", ...unreachableFlag };
16361
16396
  }
16362
16397
  const merged = threeWayMerge(plan.mergeBase, ours, plan.theirs);
16363
16398
  await (0, import_promises2.mkdir)((0, import_node_path5.dirname)(oursPath), { recursive: true });
@@ -17565,6 +17600,7 @@ var NoPatchesFlow = class {
17565
17600
  results = await this.service.applicator.applyPatches(newPatches);
17566
17601
  this.service._lastApplyResults = results;
17567
17602
  this.service.recordDroppedContextLineWarnings(results);
17603
+ this.service.recordUnreachableBaseWarnings(results);
17568
17604
  this.service.revertConflictingFiles(results);
17569
17605
  for (const result of results) {
17570
17606
  if (result.status === "conflict") {
@@ -17720,6 +17756,7 @@ var NormalRegenerationFlow = class {
17720
17756
  const results = await this.service.applicator.applyPatches(allPatches);
17721
17757
  this.service._lastApplyResults = results;
17722
17758
  this.service.recordDroppedContextLineWarnings(results);
17759
+ this.service.recordUnreachableBaseWarnings(results);
17723
17760
  this.service.revertConflictingFiles(results);
17724
17761
  for (const result of results) {
17725
17762
  if (result.status === "conflict") {
@@ -18639,6 +18676,37 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
18639
18676
  }
18640
18677
  }
18641
18678
  }
18679
+ /**
18680
+ * After applyPatches(), surface a warning for each (patch × file) where a
18681
+ * customization's BASE could not be read because the patch's
18682
+ * base-generation tree is unreachable in this clone (shallow-clone window
18683
+ * too small / pruned history). In that state the merge pipeline falls
18684
+ * through to `new-file-both` and the customer's edit can be dropped with NO
18685
+ * conflict and NO error — the INC-866 silent-loss class. This converts that
18686
+ * silence into an explicit, actionable warning naming the file and the
18687
+ * likely cause (insufficient clone window).
18688
+ *
18689
+ * Mirrors `recordDroppedContextLineWarnings`'s "surface, never silently
18690
+ * drop" contract. De-duplicated per file path so a single insufficient
18691
+ * window doesn't spam one warning per touched file repeatedly.
18692
+ */
18693
+ /** @internal Used by Flow implementations in `src/flows/`. */
18694
+ recordUnreachableBaseWarnings(results) {
18695
+ const warned = /* @__PURE__ */ new Set();
18696
+ for (const result of results) {
18697
+ if (!result.fileResults) continue;
18698
+ for (const fr of result.fileResults) {
18699
+ if (!fr.baseTreeUnreachable) continue;
18700
+ if (warned.has(fr.file)) continue;
18701
+ warned.add(fr.file);
18702
+ const base = fr.conflictMetadata?.baseGeneration;
18703
+ const baseRef = base ? ` (base generation ${base.slice(0, 7)})` : "";
18704
+ this.warnings.push(
18705
+ `${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.`
18706
+ );
18707
+ }
18708
+ }
18709
+ }
18642
18710
  /**
18643
18711
  * After applyPatches(), strip conflict markers from conflicting files
18644
18712
  * so only clean content is committed. Keeps the Generated (OURS) side.