@fern-api/replay 0.12.0 → 0.14.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
@@ -69,6 +69,15 @@ interface FileResult {
69
69
  conflicts?: ConflictRegion[];
70
70
  conflictReason?: ConflictReason;
71
71
  conflictMetadata?: ConflictMetadata;
72
+ /**
73
+ * Lines that appeared as unchanged context in the customer's THEIRS (also
74
+ * present in BASE) but were absent from the merged output because OURS
75
+ * deleted them. diff3 sides with the deletion correctly, but the customer
76
+ * may not have realized those lines depended on the generator never
77
+ * removing them. Surfaced as a warning so the customer can re-add them
78
+ * if needed. Empty/undefined when no such lines exist.
79
+ */
80
+ droppedContextLines?: string[];
72
81
  }
73
82
  interface ConflictRegion {
74
83
  startLine: number;
@@ -80,6 +89,14 @@ interface MergeResult {
80
89
  content: string;
81
90
  hasConflicts: boolean;
82
91
  conflicts: ConflictRegion[];
92
+ /**
93
+ * Lines present in BOTH base and theirs but absent from the merged output.
94
+ * These are lines the customer's THEIRS treated as context; OURS deleted
95
+ * them; diff3 sided with the deletion. Empty when no such lines exist.
96
+ * Only populated when `hasConflicts` is false — when conflicts surface,
97
+ * the customer already knows something needs review.
98
+ */
99
+ droppedContextLines?: string[];
83
100
  }
84
101
  interface CommitInfo {
85
102
  sha: string;
@@ -210,6 +227,31 @@ declare class ReplayDetector {
210
227
  readonly warnings: string[];
211
228
  constructor(git: GitClient, lockManager: LockfileManager, sdkOutputDir: string);
212
229
  detectNewPatches(): Promise<DetectionResult>;
230
+ /**
231
+ * FER-10201 — Non-linear-history detection via `merge-base(prevGen, HEAD)`.
232
+ *
233
+ * After a squash-merge of a Fern-bot PR, `lastGen.commit_sha` (the previous
234
+ * `[fern-generated]`) is orphaned but still in git's object database. The
235
+ * merge-base is the commit on main from which the bot's branch was created —
236
+ * a reachable ancestor of HEAD. Walking that range and classifying each
237
+ * commit via `isGenerationCommit` mirrors the linear path and eliminates
238
+ * the bug class where pipeline-driven changes (autoversion, replay,
239
+ * generation) get bundled as customer customizations.
240
+ *
241
+ * Falls through to the legacy composite path when no common ancestor exists
242
+ * (truly disjoint history — orphan branches with no shared root).
243
+ */
244
+ private detectPatchesViaMergeBase;
245
+ /**
246
+ * FER-10201 — Walk `${rangeStart}..HEAD`, classify each commit, emit one
247
+ * `StoredPatch` per customer commit. Body shared by the linear path
248
+ * (rangeStart = lastGen.commit_sha) and the merge-base path.
249
+ *
250
+ * Patch `base_generation` is always `lastGen.commit_sha` — the
251
+ * lockfile-tracked reference the applicator uses to fetch base file
252
+ * content, regardless of where the walk actually started.
253
+ */
254
+ private detectPatchesInRange;
213
255
  /**
214
256
  * Compute content hash for deduplication.
215
257
  *
@@ -321,6 +363,19 @@ declare class ReplayApplicator {
321
363
  private renameCache;
322
364
  private treeExistsCache;
323
365
  private fileTheirsAccumulator;
366
+ /**
367
+ * Apply mode for the current `applyPatches` invocation. Set at the start
368
+ * of `applyPatches`, read by `mergeFile` to decide:
369
+ * - whether to skip the intra-loop marker strip (kept in `applyPatches`)
370
+ * - whether to populate the accumulator after a conflicted merge
371
+ * (resolve mode populates so subsequent patches on the same file
372
+ * can use patch A's THEIRS as a structurally-correct merge base
373
+ * when their diff was authored against post-A structure)
374
+ * - whether to retry THEIRS reconstruction against the accumulator
375
+ * when the BASE-relative reconstruction produced markers from a
376
+ * cross-patch context mismatch
377
+ */
378
+ private currentApplyMode;
324
379
  constructor(git: GitClient, lockManager: LockfileManager, outputDir: string);
325
380
  /**
326
381
  * Resolve the GenerationRecord for a patch's base_generation.
@@ -344,9 +399,31 @@ declare class ReplayApplicator {
344
399
  /**
345
400
  * Apply all patches, returning results for each.
346
401
  * Skips patches that match exclude patterns in replay.yml
402
+ *
403
+ * `applyMode` controls the post-conflict marker strategy:
404
+ * - `"replay"` (default): strip conflict markers from disk between
405
+ * iterations whenever a later patch touches the same file. Lets the
406
+ * follow-up patch's `git apply --3way` see clean OURS content,
407
+ * which is what the silent-loss fix (PR #73) relies on. The replay
408
+ * pipeline calls `revertConflictingFiles` after the loop to clean
409
+ * any markers that survive.
410
+ * - `"resolve"`: keep markers on disk. The resolve command needs the
411
+ * customer to see them. Slow-path 3-way merge naturally preserves
412
+ * A's markers and B's clean writes when their regions don't overlap;
413
+ * when they do overlap, the customer gets nested markers and
414
+ * resolves manually. Either way they aren't silently dropped.
415
+ */
416
+ applyPatches(patches: StoredPatch[], opts?: {
417
+ applyMode?: "replay" | "resolve";
418
+ }): Promise<ReplayResult[]>;
419
+ /**
420
+ * Populate accumulator after git apply succeeds, AND collect per-file
421
+ * results including any "dropped context lines" — lines that were
422
+ * present in BASE and THEIRS (unchanged context) but absent from the
423
+ * final on-disk state because OURS deleted them. This is the fast-path
424
+ * counterpart to ThreeWayMerge.computeDroppedContextLines used by the
425
+ * 3-way slow path; both must surface the same warning.
347
426
  */
348
- applyPatches(patches: StoredPatch[]): Promise<ReplayResult[]>;
349
- /** Populate accumulator after git apply succeeds. */
350
427
  private populateAccumulatorForPatch;
351
428
  private applyPatchWithFallback;
352
429
  private applyWithThreeWayMerge;
@@ -378,7 +455,13 @@ declare class ReplayCommitter {
378
455
  private outputDir;
379
456
  constructor(git: GitClient, outputDir: string);
380
457
  commitGeneration(message: string, options?: CommitOptions): Promise<string>;
381
- commitReplay(_patchCount: number, patches?: StoredPatch[], message?: string): Promise<string>;
458
+ commitReplay(_patchCount: number, patches?: StoredPatch[], message?: string, options?: {
459
+ buckets?: {
460
+ applied: StoredPatch[];
461
+ unresolved: StoredPatch[];
462
+ absorbed: StoredPatch[];
463
+ };
464
+ }): Promise<string>;
382
465
  createGenerationRecord(options?: CommitOptions): Promise<GenerationRecord>;
383
466
  stageAll(): Promise<void>;
384
467
  hasStagedChanges(): Promise<boolean>;
@@ -643,6 +726,15 @@ declare class ReplayService {
643
726
  * Called BEFORE commitGeneration() while HEAD has customer code.
644
727
  */
645
728
  private preGenerationRebase;
729
+ /**
730
+ * After applyPatches(), surface a warning for each (patch × file) where
731
+ * diff3 silently dropped lines that were unchanged context in the
732
+ * customer's THEIRS. The deletion stays on disk (correct diff3 outcome),
733
+ * but the customer must learn so they can re-add the lines if they
734
+ * relied on them. This is the "surface or preserve, never silently drop"
735
+ * contract for legitimate-deletion cases.
736
+ */
737
+ private recordDroppedContextLineWarnings;
646
738
  /**
647
739
  * After applyPatches(), strip conflict markers from conflicting files
648
740
  * so only clean content is committed. Keeps the Generated (OURS) side.
@@ -814,6 +906,8 @@ interface ResolveResult {
814
906
  patchesApplied?: number;
815
907
  /** Number of patches resolved and committed (phase 2) */
816
908
  patchesResolved?: number;
909
+ /** Non-fatal diagnostic messages (e.g., per-patch fallbacks) */
910
+ warnings?: string[];
817
911
  }
818
912
  declare function resolve(outputDir: string, options?: ResolveOptions): Promise<ResolveResult>;
819
913
 
package/dist/index.d.ts CHANGED
@@ -69,6 +69,15 @@ interface FileResult {
69
69
  conflicts?: ConflictRegion[];
70
70
  conflictReason?: ConflictReason;
71
71
  conflictMetadata?: ConflictMetadata;
72
+ /**
73
+ * Lines that appeared as unchanged context in the customer's THEIRS (also
74
+ * present in BASE) but were absent from the merged output because OURS
75
+ * deleted them. diff3 sides with the deletion correctly, but the customer
76
+ * may not have realized those lines depended on the generator never
77
+ * removing them. Surfaced as a warning so the customer can re-add them
78
+ * if needed. Empty/undefined when no such lines exist.
79
+ */
80
+ droppedContextLines?: string[];
72
81
  }
73
82
  interface ConflictRegion {
74
83
  startLine: number;
@@ -80,6 +89,14 @@ interface MergeResult {
80
89
  content: string;
81
90
  hasConflicts: boolean;
82
91
  conflicts: ConflictRegion[];
92
+ /**
93
+ * Lines present in BOTH base and theirs but absent from the merged output.
94
+ * These are lines the customer's THEIRS treated as context; OURS deleted
95
+ * them; diff3 sided with the deletion. Empty when no such lines exist.
96
+ * Only populated when `hasConflicts` is false — when conflicts surface,
97
+ * the customer already knows something needs review.
98
+ */
99
+ droppedContextLines?: string[];
83
100
  }
84
101
  interface CommitInfo {
85
102
  sha: string;
@@ -210,6 +227,31 @@ declare class ReplayDetector {
210
227
  readonly warnings: string[];
211
228
  constructor(git: GitClient, lockManager: LockfileManager, sdkOutputDir: string);
212
229
  detectNewPatches(): Promise<DetectionResult>;
230
+ /**
231
+ * FER-10201 — Non-linear-history detection via `merge-base(prevGen, HEAD)`.
232
+ *
233
+ * After a squash-merge of a Fern-bot PR, `lastGen.commit_sha` (the previous
234
+ * `[fern-generated]`) is orphaned but still in git's object database. The
235
+ * merge-base is the commit on main from which the bot's branch was created —
236
+ * a reachable ancestor of HEAD. Walking that range and classifying each
237
+ * commit via `isGenerationCommit` mirrors the linear path and eliminates
238
+ * the bug class where pipeline-driven changes (autoversion, replay,
239
+ * generation) get bundled as customer customizations.
240
+ *
241
+ * Falls through to the legacy composite path when no common ancestor exists
242
+ * (truly disjoint history — orphan branches with no shared root).
243
+ */
244
+ private detectPatchesViaMergeBase;
245
+ /**
246
+ * FER-10201 — Walk `${rangeStart}..HEAD`, classify each commit, emit one
247
+ * `StoredPatch` per customer commit. Body shared by the linear path
248
+ * (rangeStart = lastGen.commit_sha) and the merge-base path.
249
+ *
250
+ * Patch `base_generation` is always `lastGen.commit_sha` — the
251
+ * lockfile-tracked reference the applicator uses to fetch base file
252
+ * content, regardless of where the walk actually started.
253
+ */
254
+ private detectPatchesInRange;
213
255
  /**
214
256
  * Compute content hash for deduplication.
215
257
  *
@@ -321,6 +363,19 @@ declare class ReplayApplicator {
321
363
  private renameCache;
322
364
  private treeExistsCache;
323
365
  private fileTheirsAccumulator;
366
+ /**
367
+ * Apply mode for the current `applyPatches` invocation. Set at the start
368
+ * of `applyPatches`, read by `mergeFile` to decide:
369
+ * - whether to skip the intra-loop marker strip (kept in `applyPatches`)
370
+ * - whether to populate the accumulator after a conflicted merge
371
+ * (resolve mode populates so subsequent patches on the same file
372
+ * can use patch A's THEIRS as a structurally-correct merge base
373
+ * when their diff was authored against post-A structure)
374
+ * - whether to retry THEIRS reconstruction against the accumulator
375
+ * when the BASE-relative reconstruction produced markers from a
376
+ * cross-patch context mismatch
377
+ */
378
+ private currentApplyMode;
324
379
  constructor(git: GitClient, lockManager: LockfileManager, outputDir: string);
325
380
  /**
326
381
  * Resolve the GenerationRecord for a patch's base_generation.
@@ -344,9 +399,31 @@ declare class ReplayApplicator {
344
399
  /**
345
400
  * Apply all patches, returning results for each.
346
401
  * Skips patches that match exclude patterns in replay.yml
402
+ *
403
+ * `applyMode` controls the post-conflict marker strategy:
404
+ * - `"replay"` (default): strip conflict markers from disk between
405
+ * iterations whenever a later patch touches the same file. Lets the
406
+ * follow-up patch's `git apply --3way` see clean OURS content,
407
+ * which is what the silent-loss fix (PR #73) relies on. The replay
408
+ * pipeline calls `revertConflictingFiles` after the loop to clean
409
+ * any markers that survive.
410
+ * - `"resolve"`: keep markers on disk. The resolve command needs the
411
+ * customer to see them. Slow-path 3-way merge naturally preserves
412
+ * A's markers and B's clean writes when their regions don't overlap;
413
+ * when they do overlap, the customer gets nested markers and
414
+ * resolves manually. Either way they aren't silently dropped.
415
+ */
416
+ applyPatches(patches: StoredPatch[], opts?: {
417
+ applyMode?: "replay" | "resolve";
418
+ }): Promise<ReplayResult[]>;
419
+ /**
420
+ * Populate accumulator after git apply succeeds, AND collect per-file
421
+ * results including any "dropped context lines" — lines that were
422
+ * present in BASE and THEIRS (unchanged context) but absent from the
423
+ * final on-disk state because OURS deleted them. This is the fast-path
424
+ * counterpart to ThreeWayMerge.computeDroppedContextLines used by the
425
+ * 3-way slow path; both must surface the same warning.
347
426
  */
348
- applyPatches(patches: StoredPatch[]): Promise<ReplayResult[]>;
349
- /** Populate accumulator after git apply succeeds. */
350
427
  private populateAccumulatorForPatch;
351
428
  private applyPatchWithFallback;
352
429
  private applyWithThreeWayMerge;
@@ -378,7 +455,13 @@ declare class ReplayCommitter {
378
455
  private outputDir;
379
456
  constructor(git: GitClient, outputDir: string);
380
457
  commitGeneration(message: string, options?: CommitOptions): Promise<string>;
381
- commitReplay(_patchCount: number, patches?: StoredPatch[], message?: string): Promise<string>;
458
+ commitReplay(_patchCount: number, patches?: StoredPatch[], message?: string, options?: {
459
+ buckets?: {
460
+ applied: StoredPatch[];
461
+ unresolved: StoredPatch[];
462
+ absorbed: StoredPatch[];
463
+ };
464
+ }): Promise<string>;
382
465
  createGenerationRecord(options?: CommitOptions): Promise<GenerationRecord>;
383
466
  stageAll(): Promise<void>;
384
467
  hasStagedChanges(): Promise<boolean>;
@@ -643,6 +726,15 @@ declare class ReplayService {
643
726
  * Called BEFORE commitGeneration() while HEAD has customer code.
644
727
  */
645
728
  private preGenerationRebase;
729
+ /**
730
+ * After applyPatches(), surface a warning for each (patch × file) where
731
+ * diff3 silently dropped lines that were unchanged context in the
732
+ * customer's THEIRS. The deletion stays on disk (correct diff3 outcome),
733
+ * but the customer must learn so they can re-add the lines if they
734
+ * relied on them. This is the "surface or preserve, never silently drop"
735
+ * contract for legitimate-deletion cases.
736
+ */
737
+ private recordDroppedContextLineWarnings;
646
738
  /**
647
739
  * After applyPatches(), strip conflict markers from conflicting files
648
740
  * so only clean content is committed. Keeps the Generated (OURS) side.
@@ -814,6 +906,8 @@ interface ResolveResult {
814
906
  patchesApplied?: number;
815
907
  /** Number of patches resolved and committed (phase 2) */
816
908
  patchesResolved?: number;
909
+ /** Non-fatal diagnostic messages (e.g., per-patch fallbacks) */
910
+ warnings?: string[];
817
911
  }
818
912
  declare function resolve(outputDir: string, options?: ResolveOptions): Promise<ResolveResult>;
819
913