@fern-api/replay 0.18.0 → 0.19.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
@@ -4,8 +4,40 @@ interface GenerationLock {
4
4
  current_generation: string;
5
5
  patches: StoredPatch[];
6
6
  forgotten_hashes?: string[];
7
+ /**
8
+ * Visibility metadata for dismissed customizations (patches retired via
9
+ * `forget` or by keeping the generator's version during `resolve`).
10
+ * `forgotten_hashes` remains the sole operative tombstone list the
11
+ * detector consults; this array exists so `status` can show the customer
12
+ * WHAT was dismissed (message + commit), not just that something was.
13
+ *
14
+ * Never stores patch content — the credential strip in
15
+ * `LockfileManager.save()` only scans patches, so diff text must not
16
+ * appear here. Like `forgotten_hashes`, this is lost when the lockfile
17
+ * is deleted (`reset`) or rebuilt (`bootstrap --force`) — full-amnesia
18
+ * semantics by design.
19
+ */
20
+ dismissals?: DismissalRecord[];
7
21
  replay_skipped_at?: string;
8
22
  }
23
+ interface DismissalRecord {
24
+ /** ID the patch had when it was dismissed (e.g. "patch-6cf172ea"). */
25
+ patch_id: string;
26
+ /** SHA of the customer commit the patch came from. Empty for synthetic patches. */
27
+ original_commit: string;
28
+ /** The customer's original commit message. */
29
+ original_message: string;
30
+ /**
31
+ * Every content hash tombstoned for this dismissal: the stored
32
+ * content_hash plus, when the original commit is reachable, the
33
+ * re-emission hash recomputed through the detection pipeline.
34
+ */
35
+ content_hashes: string[];
36
+ /** ISO timestamp of the dismissal. */
37
+ dismissed_at: string;
38
+ /** Which route dismissed it. */
39
+ via: "forget" | "resolve";
40
+ }
9
41
  interface GenerationRecord {
10
42
  commit_sha: string;
11
43
  tree_hash: string;
@@ -65,6 +97,35 @@ interface CustomizationsConfig {
65
97
  exclude?: string[];
66
98
  moves?: MoveDeclaration[];
67
99
  }
100
+ /**
101
+ * Machine-readable reasons a replay run is "degraded": the run completed,
102
+ * but Replay could not guarantee that customizations were carried forward
103
+ * because the baseline it needed (the previous generation) was not reachable
104
+ * during the run.
105
+ *
106
+ * These codes are wire keys (pipeline JSON, telemetry) — frozen once shipped.
107
+ * See CONTEXT.md's wire-contract section.
108
+ *
109
+ * - `generation-anchor-unreachable` — the recorded generation commit is not
110
+ * in the clone, and tracked patches exist that cannot be verified against
111
+ * it. Tree-diff recovery may still have reconstructed on-disk
112
+ * customizations.
113
+ * - `generation-tree-unreachable` — neither the recorded generation commit
114
+ * nor its recorded tree is reachable; detection fell back to a bounded
115
+ * commit scan that cannot see customizations older than the clone window.
116
+ * - `baseline-unresolvable` — the lockfile records a previous generation but
117
+ * no baseline could be resolved at all (no boundary in history, no matching
118
+ * generation record). Detection returned without a baseline.
119
+ * - `patch-base-unreachable` — at apply time, one or more customizations
120
+ * could not be checked against their base generation because that
121
+ * generation's tree was not reachable during this run.
122
+ */
123
+ type DegradedReasonCode = "generation-anchor-unreachable" | "generation-tree-unreachable" | "baseline-unresolvable" | "patch-base-unreachable";
124
+ /** One structured reason a run was degraded. `message` is customer-facing prose. */
125
+ interface DegradedReason {
126
+ code: DegradedReasonCode;
127
+ message: string;
128
+ }
68
129
  interface MoveDeclaration {
69
130
  from: string;
70
131
  to: string;
@@ -256,6 +317,13 @@ declare class LockfileManager {
256
317
  removePatch(patchId: string): void;
257
318
  clearPatches(): void;
258
319
  addForgottenHash(hash: string): void;
320
+ /**
321
+ * Record a dismissed customization: visibility metadata in `dismissals`
322
+ * plus a tombstone in `forgotten_hashes` for every content hash.
323
+ * `forgotten_hashes` stays the only list the detector consults; the
324
+ * metadata exists so `status` can show what was dismissed.
325
+ */
326
+ addDismissal(record: DismissalRecord): void;
259
327
  getUnresolvedPatches(): StoredPatch[];
260
328
  getResolvingPatches(): StoredPatch[];
261
329
  markPatchUnresolved(patchId: string): void;
@@ -273,12 +341,49 @@ interface DetectionResult {
273
341
  patches: StoredPatch[];
274
342
  revertedPatchIds: string[];
275
343
  }
344
+ /**
345
+ * Result of materializing a commit into patch content through the exact
346
+ * pipeline detection uses (file filtering → scoped diff or format-patch →
347
+ * content hash). `sensitiveFiles` / `fernignoredFiles` are surfaced so
348
+ * callers can emit the same customer-facing warnings detection does.
349
+ *
350
+ * - `ok` — patch content and its content hash were produced.
351
+ * - `unreachable` — the commit (or its patch text) could not be read, e.g.
352
+ * a shallow clone window or a GC'd object.
353
+ * - `empty` — nothing survives filtering (only infrastructure / sensitive /
354
+ * fernignored files, or the scoped diff is empty).
355
+ */
356
+ type MaterializedCommitPatch = {
357
+ status: "ok";
358
+ files: string[];
359
+ sensitiveFiles: string[];
360
+ fernignoredFiles: string[];
361
+ patchContent: string;
362
+ contentHash: string;
363
+ } | {
364
+ status: "unreachable";
365
+ sensitiveFiles: string[];
366
+ fernignoredFiles: string[];
367
+ } | {
368
+ status: "empty";
369
+ sensitiveFiles: string[];
370
+ fernignoredFiles: string[];
371
+ };
276
372
  declare class ReplayDetector {
277
373
  private git;
278
374
  private lockManager;
279
375
  private sdkOutputDir;
280
376
  private fernignorePatterns;
281
377
  readonly warnings: string[];
378
+ /**
379
+ * Structured degraded-state channel, mirroring `warnings`. Populated when
380
+ * detection cannot guarantee customizations were carried forward (baseline
381
+ * unreachable in this clone). Cleared per-run by `ReplayService.prepareReplay`
382
+ * alongside `warnings`. Merged into `ReplayReport.degradedReasons` by each
383
+ * flow handler. Degraded is observability only: detection results are
384
+ * unchanged.
385
+ */
386
+ readonly degradedReasons: DegradedReason[];
282
387
  constructor(git: GitClient, lockManager: LockfileManager, sdkOutputDir: string, fernignorePatterns?: readonly string[]);
283
388
  /**
284
389
  * Derive the previous-generation SHA from git history instead of trusting
@@ -308,6 +413,23 @@ declare class ReplayDetector {
308
413
  * content, regardless of where the walk actually started.
309
414
  */
310
415
  private detectPatchesInRange;
416
+ /**
417
+ * Materialize a commit into patch content exactly as detection does.
418
+ *
419
+ * Pipeline: `diff-tree --name-only` → filter infrastructure / sensitive /
420
+ * `.fernignore`d files → scoped `git diff` when files were stripped,
421
+ * otherwise full `git format-patch` → content hash.
422
+ *
423
+ * This is THE definition of a commit's re-emission identity: the content
424
+ * hash this method returns is what `detectPatchesInRange` will compute if
425
+ * the commit re-enters the scan range (e.g. after a regen PR is merged
426
+ * with a merge commit). Callers that tombstone a dismissed patch must use
427
+ * this hash — the stored `content_hash` drifts every time the patch is
428
+ * carried forward, so it does not identify the commit on re-detection.
429
+ *
430
+ * Never throws; failures are reported through the `status` discriminant.
431
+ */
432
+ materializeCommitPatch(sha: string): Promise<MaterializedCommitPatch>;
311
433
  /**
312
434
  * Compute content hash for deduplication.
313
435
  *
@@ -604,6 +726,16 @@ interface ReplayReport {
604
726
  unresolvedPatches?: UnresolvedPatchInfo[];
605
727
  wouldApply?: StoredPatch[];
606
728
  warnings?: string[];
729
+ /**
730
+ * True iff `degradedReasons` is non-empty: the run completed but could not
731
+ * guarantee customizations were carried forward because the baseline it
732
+ * needed (the previous generation) was unreachable during the run.
733
+ * Both fields are undefined on healthy runs, matching the `warnings`
734
+ * convention. Degraded is observability only — it never changes detection
735
+ * results, lockfile saves, or commit decisions.
736
+ */
737
+ degraded?: boolean;
738
+ degradedReasons?: DegradedReason[];
607
739
  }
608
740
  interface ReplayOptions {
609
741
  /** Log what would happen but don't modify anything */
@@ -689,6 +821,12 @@ interface InternalPreparationActive {
689
821
  };
690
822
  /** detector warnings snapshotted at end of phase 1 */
691
823
  detectorWarnings: string[];
824
+ /**
825
+ * Detector degraded reasons snapshotted at end of phase 1 (next to
826
+ * `detectorWarnings`) so degraded state survives the two-phase
827
+ * prepare/apply split and any mid-phase commits (e.g. autoversion).
828
+ */
829
+ detectorDegraded: DegradedReason[];
692
830
  revertedCount: number;
693
831
  /** [fern-generated] SHA; passed to rebasePatches as currentGenSha */
694
832
  genSha: string;
@@ -722,6 +860,16 @@ declare class ReplayService {
722
860
  * @internal Used by Flow implementations in `src/flows/`.
723
861
  */
724
862
  warnings: string[];
863
+ /**
864
+ * Service-level structured degraded reasons, mirroring `warnings`.
865
+ * Populated at apply time (e.g. `recordUnreachableBaseWarnings` when a
866
+ * patch's base-generation tree is unreachable). Merged with
867
+ * `detector.degradedReasons` into `ReplayReport.degradedReasons` by each
868
+ * flow handler. Cleared at the top of `prepareReplay()`.
869
+ *
870
+ * @internal Used by Flow implementations in `src/flows/`.
871
+ */
872
+ degradedReasons: DegradedReason[];
725
873
  /**
726
874
  * @internal Test-only accessor.
727
875
  * Returns the ReplayApplicator instance used for the last run. Tests use this
@@ -762,6 +910,17 @@ declare class ReplayService {
762
910
  * `diff --git` headers are found (fail-safe for exotic legacy formats).
763
911
  */
764
912
  private cleanseLegacyFernignoreEntries;
913
+ /**
914
+ * Migration step: strips infrastructure file diffs (`.fernignore`, `.fern/*`)
915
+ * from existing patches' `patch_content`. These diffs were bundled by older
916
+ * detection logic that did not scope `git diff` when only infrastructure files
917
+ * were stripped. Patches that end up with no surviving diff sections are
918
+ * removed entirely.
919
+ *
920
+ * Idempotent: a second run is a no-op because the first run already removed
921
+ * all infrastructure sections. Per-patch try/catch ensures isolation.
922
+ */
923
+ private cleanseInfrastructureFileEntries;
765
924
  /**
766
925
  * Phase 2 of the two-phase replay flow. Applies patches, rebases them against
767
926
  * the generation, saves the lockfile, and commits `[fern-replay]` (or stages
@@ -939,7 +1098,7 @@ declare class ReplayService {
939
1098
  conflictResolved: number;
940
1099
  conflictAbsorbed: number;
941
1100
  contentRefreshed: number;
942
- }, patchesReverted?: number): ReplayReport;
1101
+ }, patchesReverted?: number, degradedReasons?: DegradedReason[]): ReplayReport;
943
1102
  }
944
1103
 
945
1104
  interface MigrationAnalysis {
@@ -1060,7 +1219,7 @@ interface ForgetResult {
1060
1219
  /** Matching patches for interactive selection (search/no-arg mode only) */
1061
1220
  matched?: MatchedPatch[];
1062
1221
  }
1063
- declare function forget(outputDir: string, options?: ForgetOptions): ForgetResult;
1222
+ declare function forget(outputDir: string, options?: ForgetOptions): Promise<ForgetResult>;
1064
1223
 
1065
1224
  interface ResetOptions {
1066
1225
  /** Don't actually delete, just show what would happen */
@@ -1097,6 +1256,15 @@ interface ResolveResult {
1097
1256
  patchesApplied?: number;
1098
1257
  /** Number of patches resolved and committed (phase 2) */
1099
1258
  patchesResolved?: number;
1259
+ /**
1260
+ * Patches permanently dismissed during phase 2 — the customer kept the
1261
+ * generator's version for every file in the patch. These are tombstoned
1262
+ * in the lockfile and will not be re-detected on future generations.
1263
+ */
1264
+ patchesDismissed?: Array<{
1265
+ id: string;
1266
+ message: string;
1267
+ }>;
1100
1268
  /** Non-fatal diagnostic messages (e.g., per-patch fallbacks) */
1101
1269
  warnings?: string[];
1102
1270
  }
@@ -1115,6 +1283,28 @@ interface StatusResult {
1115
1283
  unresolvedCount: number;
1116
1284
  /** Exclude patterns from replay.yml */
1117
1285
  excludePatterns: string[];
1286
+ /**
1287
+ * Customizations the customer permanently dismissed — via `forget`, or by
1288
+ * keeping the generator's version when resolving a conflict. These will
1289
+ * not be re-detected on future generations.
1290
+ */
1291
+ dismissed: StatusDismissal[];
1292
+ /**
1293
+ * Count of forgotten hashes recorded before dismissal metadata existed.
1294
+ * Such entries still tombstone re-detection but carry no message/commit
1295
+ * details to display.
1296
+ */
1297
+ legacyDismissedCount: number;
1298
+ }
1299
+ interface StatusDismissal {
1300
+ /** The customer's original commit message */
1301
+ message: string;
1302
+ /** Short SHA of the original commit (7 chars; empty for synthetic patches) */
1303
+ sha: string;
1304
+ /** Which route dismissed it */
1305
+ via: "forget" | "resolve";
1306
+ /** ISO timestamp of the dismissal */
1307
+ dismissedAt: string;
1118
1308
  }
1119
1309
  interface StatusPatch {
1120
1310
  /** Patch ID (e.g. "patch-def45678") */
@@ -1146,4 +1336,4 @@ interface StatusGeneration {
1146
1336
  }
1147
1337
  declare function status(outputDir: string): StatusResult;
1148
1338
 
1149
- export { type ApplyOptions, type BootstrapOptions, type BootstrapResult, CREDENTIAL_PATTERNS, type CommitInfo, type CommitOptions, type ConflictDetail, type ConflictMetadata, type ConflictReason, type ConflictRegion, type CredentialPattern, type CustomizationsConfig, type DetectionResult, type DiffStat, FERN_BOT_EMAIL, FERN_BOT_LOGIN, FERN_BOT_NAME, FernignoreMigrator, type FileResult, type FlowKind, type ForgetOptions, type ForgetResult, type GenerationLock, type GenerationRecord, GitClient, LockfileManager, LockfileNotFoundError, type MatchedPatch, type MergeResult, type MigrationAnalysis, type MigrationResult, type MoveDeclaration, ReplayApplicator, ReplayCommitter, type ReplayConfig, ReplayDetector, type ReplayOptions, type ReplayPreparation, type ReplayReport, type ReplayResult, ReplayService, type ResetOptions, type ResetResult, type ResolveOptions, type ResolveResult, SENSITIVE_FILE_PATTERNS, type StatusGeneration, type StatusPatch, type StatusResult, type StoredPatch, type UnresolvedPatchInfo, bootstrap, forget, isGenerationCommit, isReplayCommit, isRevertCommit, isSensitiveFile, parseRevertedMessage, parseRevertedSha, reset, resolve, scanForCredentials, status, threeWayMerge };
1339
+ export { type ApplyOptions, type BootstrapOptions, type BootstrapResult, CREDENTIAL_PATTERNS, type CommitInfo, type CommitOptions, type ConflictDetail, type ConflictMetadata, type ConflictReason, type ConflictRegion, type CredentialPattern, type CustomizationsConfig, type DegradedReason, type DegradedReasonCode, type DetectionResult, type DiffStat, type DismissalRecord, FERN_BOT_EMAIL, FERN_BOT_LOGIN, FERN_BOT_NAME, FernignoreMigrator, type FileResult, type FlowKind, type ForgetOptions, type ForgetResult, type GenerationLock, type GenerationRecord, GitClient, LockfileManager, LockfileNotFoundError, type MatchedPatch, type MaterializedCommitPatch, type MergeResult, type MigrationAnalysis, type MigrationResult, type MoveDeclaration, ReplayApplicator, ReplayCommitter, type ReplayConfig, ReplayDetector, type ReplayOptions, type ReplayPreparation, type ReplayReport, type ReplayResult, ReplayService, type ResetOptions, type ResetResult, type ResolveOptions, type ResolveResult, SENSITIVE_FILE_PATTERNS, type StatusDismissal, type StatusGeneration, type StatusPatch, type StatusResult, type StoredPatch, type UnresolvedPatchInfo, bootstrap, forget, isGenerationCommit, isReplayCommit, isRevertCommit, isSensitiveFile, parseRevertedMessage, parseRevertedSha, reset, resolve, scanForCredentials, status, threeWayMerge };
package/dist/index.d.ts CHANGED
@@ -4,8 +4,40 @@ interface GenerationLock {
4
4
  current_generation: string;
5
5
  patches: StoredPatch[];
6
6
  forgotten_hashes?: string[];
7
+ /**
8
+ * Visibility metadata for dismissed customizations (patches retired via
9
+ * `forget` or by keeping the generator's version during `resolve`).
10
+ * `forgotten_hashes` remains the sole operative tombstone list the
11
+ * detector consults; this array exists so `status` can show the customer
12
+ * WHAT was dismissed (message + commit), not just that something was.
13
+ *
14
+ * Never stores patch content — the credential strip in
15
+ * `LockfileManager.save()` only scans patches, so diff text must not
16
+ * appear here. Like `forgotten_hashes`, this is lost when the lockfile
17
+ * is deleted (`reset`) or rebuilt (`bootstrap --force`) — full-amnesia
18
+ * semantics by design.
19
+ */
20
+ dismissals?: DismissalRecord[];
7
21
  replay_skipped_at?: string;
8
22
  }
23
+ interface DismissalRecord {
24
+ /** ID the patch had when it was dismissed (e.g. "patch-6cf172ea"). */
25
+ patch_id: string;
26
+ /** SHA of the customer commit the patch came from. Empty for synthetic patches. */
27
+ original_commit: string;
28
+ /** The customer's original commit message. */
29
+ original_message: string;
30
+ /**
31
+ * Every content hash tombstoned for this dismissal: the stored
32
+ * content_hash plus, when the original commit is reachable, the
33
+ * re-emission hash recomputed through the detection pipeline.
34
+ */
35
+ content_hashes: string[];
36
+ /** ISO timestamp of the dismissal. */
37
+ dismissed_at: string;
38
+ /** Which route dismissed it. */
39
+ via: "forget" | "resolve";
40
+ }
9
41
  interface GenerationRecord {
10
42
  commit_sha: string;
11
43
  tree_hash: string;
@@ -65,6 +97,35 @@ interface CustomizationsConfig {
65
97
  exclude?: string[];
66
98
  moves?: MoveDeclaration[];
67
99
  }
100
+ /**
101
+ * Machine-readable reasons a replay run is "degraded": the run completed,
102
+ * but Replay could not guarantee that customizations were carried forward
103
+ * because the baseline it needed (the previous generation) was not reachable
104
+ * during the run.
105
+ *
106
+ * These codes are wire keys (pipeline JSON, telemetry) — frozen once shipped.
107
+ * See CONTEXT.md's wire-contract section.
108
+ *
109
+ * - `generation-anchor-unreachable` — the recorded generation commit is not
110
+ * in the clone, and tracked patches exist that cannot be verified against
111
+ * it. Tree-diff recovery may still have reconstructed on-disk
112
+ * customizations.
113
+ * - `generation-tree-unreachable` — neither the recorded generation commit
114
+ * nor its recorded tree is reachable; detection fell back to a bounded
115
+ * commit scan that cannot see customizations older than the clone window.
116
+ * - `baseline-unresolvable` — the lockfile records a previous generation but
117
+ * no baseline could be resolved at all (no boundary in history, no matching
118
+ * generation record). Detection returned without a baseline.
119
+ * - `patch-base-unreachable` — at apply time, one or more customizations
120
+ * could not be checked against their base generation because that
121
+ * generation's tree was not reachable during this run.
122
+ */
123
+ type DegradedReasonCode = "generation-anchor-unreachable" | "generation-tree-unreachable" | "baseline-unresolvable" | "patch-base-unreachable";
124
+ /** One structured reason a run was degraded. `message` is customer-facing prose. */
125
+ interface DegradedReason {
126
+ code: DegradedReasonCode;
127
+ message: string;
128
+ }
68
129
  interface MoveDeclaration {
69
130
  from: string;
70
131
  to: string;
@@ -256,6 +317,13 @@ declare class LockfileManager {
256
317
  removePatch(patchId: string): void;
257
318
  clearPatches(): void;
258
319
  addForgottenHash(hash: string): void;
320
+ /**
321
+ * Record a dismissed customization: visibility metadata in `dismissals`
322
+ * plus a tombstone in `forgotten_hashes` for every content hash.
323
+ * `forgotten_hashes` stays the only list the detector consults; the
324
+ * metadata exists so `status` can show what was dismissed.
325
+ */
326
+ addDismissal(record: DismissalRecord): void;
259
327
  getUnresolvedPatches(): StoredPatch[];
260
328
  getResolvingPatches(): StoredPatch[];
261
329
  markPatchUnresolved(patchId: string): void;
@@ -273,12 +341,49 @@ interface DetectionResult {
273
341
  patches: StoredPatch[];
274
342
  revertedPatchIds: string[];
275
343
  }
344
+ /**
345
+ * Result of materializing a commit into patch content through the exact
346
+ * pipeline detection uses (file filtering → scoped diff or format-patch →
347
+ * content hash). `sensitiveFiles` / `fernignoredFiles` are surfaced so
348
+ * callers can emit the same customer-facing warnings detection does.
349
+ *
350
+ * - `ok` — patch content and its content hash were produced.
351
+ * - `unreachable` — the commit (or its patch text) could not be read, e.g.
352
+ * a shallow clone window or a GC'd object.
353
+ * - `empty` — nothing survives filtering (only infrastructure / sensitive /
354
+ * fernignored files, or the scoped diff is empty).
355
+ */
356
+ type MaterializedCommitPatch = {
357
+ status: "ok";
358
+ files: string[];
359
+ sensitiveFiles: string[];
360
+ fernignoredFiles: string[];
361
+ patchContent: string;
362
+ contentHash: string;
363
+ } | {
364
+ status: "unreachable";
365
+ sensitiveFiles: string[];
366
+ fernignoredFiles: string[];
367
+ } | {
368
+ status: "empty";
369
+ sensitiveFiles: string[];
370
+ fernignoredFiles: string[];
371
+ };
276
372
  declare class ReplayDetector {
277
373
  private git;
278
374
  private lockManager;
279
375
  private sdkOutputDir;
280
376
  private fernignorePatterns;
281
377
  readonly warnings: string[];
378
+ /**
379
+ * Structured degraded-state channel, mirroring `warnings`. Populated when
380
+ * detection cannot guarantee customizations were carried forward (baseline
381
+ * unreachable in this clone). Cleared per-run by `ReplayService.prepareReplay`
382
+ * alongside `warnings`. Merged into `ReplayReport.degradedReasons` by each
383
+ * flow handler. Degraded is observability only: detection results are
384
+ * unchanged.
385
+ */
386
+ readonly degradedReasons: DegradedReason[];
282
387
  constructor(git: GitClient, lockManager: LockfileManager, sdkOutputDir: string, fernignorePatterns?: readonly string[]);
283
388
  /**
284
389
  * Derive the previous-generation SHA from git history instead of trusting
@@ -308,6 +413,23 @@ declare class ReplayDetector {
308
413
  * content, regardless of where the walk actually started.
309
414
  */
310
415
  private detectPatchesInRange;
416
+ /**
417
+ * Materialize a commit into patch content exactly as detection does.
418
+ *
419
+ * Pipeline: `diff-tree --name-only` → filter infrastructure / sensitive /
420
+ * `.fernignore`d files → scoped `git diff` when files were stripped,
421
+ * otherwise full `git format-patch` → content hash.
422
+ *
423
+ * This is THE definition of a commit's re-emission identity: the content
424
+ * hash this method returns is what `detectPatchesInRange` will compute if
425
+ * the commit re-enters the scan range (e.g. after a regen PR is merged
426
+ * with a merge commit). Callers that tombstone a dismissed patch must use
427
+ * this hash — the stored `content_hash` drifts every time the patch is
428
+ * carried forward, so it does not identify the commit on re-detection.
429
+ *
430
+ * Never throws; failures are reported through the `status` discriminant.
431
+ */
432
+ materializeCommitPatch(sha: string): Promise<MaterializedCommitPatch>;
311
433
  /**
312
434
  * Compute content hash for deduplication.
313
435
  *
@@ -604,6 +726,16 @@ interface ReplayReport {
604
726
  unresolvedPatches?: UnresolvedPatchInfo[];
605
727
  wouldApply?: StoredPatch[];
606
728
  warnings?: string[];
729
+ /**
730
+ * True iff `degradedReasons` is non-empty: the run completed but could not
731
+ * guarantee customizations were carried forward because the baseline it
732
+ * needed (the previous generation) was unreachable during the run.
733
+ * Both fields are undefined on healthy runs, matching the `warnings`
734
+ * convention. Degraded is observability only — it never changes detection
735
+ * results, lockfile saves, or commit decisions.
736
+ */
737
+ degraded?: boolean;
738
+ degradedReasons?: DegradedReason[];
607
739
  }
608
740
  interface ReplayOptions {
609
741
  /** Log what would happen but don't modify anything */
@@ -689,6 +821,12 @@ interface InternalPreparationActive {
689
821
  };
690
822
  /** detector warnings snapshotted at end of phase 1 */
691
823
  detectorWarnings: string[];
824
+ /**
825
+ * Detector degraded reasons snapshotted at end of phase 1 (next to
826
+ * `detectorWarnings`) so degraded state survives the two-phase
827
+ * prepare/apply split and any mid-phase commits (e.g. autoversion).
828
+ */
829
+ detectorDegraded: DegradedReason[];
692
830
  revertedCount: number;
693
831
  /** [fern-generated] SHA; passed to rebasePatches as currentGenSha */
694
832
  genSha: string;
@@ -722,6 +860,16 @@ declare class ReplayService {
722
860
  * @internal Used by Flow implementations in `src/flows/`.
723
861
  */
724
862
  warnings: string[];
863
+ /**
864
+ * Service-level structured degraded reasons, mirroring `warnings`.
865
+ * Populated at apply time (e.g. `recordUnreachableBaseWarnings` when a
866
+ * patch's base-generation tree is unreachable). Merged with
867
+ * `detector.degradedReasons` into `ReplayReport.degradedReasons` by each
868
+ * flow handler. Cleared at the top of `prepareReplay()`.
869
+ *
870
+ * @internal Used by Flow implementations in `src/flows/`.
871
+ */
872
+ degradedReasons: DegradedReason[];
725
873
  /**
726
874
  * @internal Test-only accessor.
727
875
  * Returns the ReplayApplicator instance used for the last run. Tests use this
@@ -762,6 +910,17 @@ declare class ReplayService {
762
910
  * `diff --git` headers are found (fail-safe for exotic legacy formats).
763
911
  */
764
912
  private cleanseLegacyFernignoreEntries;
913
+ /**
914
+ * Migration step: strips infrastructure file diffs (`.fernignore`, `.fern/*`)
915
+ * from existing patches' `patch_content`. These diffs were bundled by older
916
+ * detection logic that did not scope `git diff` when only infrastructure files
917
+ * were stripped. Patches that end up with no surviving diff sections are
918
+ * removed entirely.
919
+ *
920
+ * Idempotent: a second run is a no-op because the first run already removed
921
+ * all infrastructure sections. Per-patch try/catch ensures isolation.
922
+ */
923
+ private cleanseInfrastructureFileEntries;
765
924
  /**
766
925
  * Phase 2 of the two-phase replay flow. Applies patches, rebases them against
767
926
  * the generation, saves the lockfile, and commits `[fern-replay]` (or stages
@@ -939,7 +1098,7 @@ declare class ReplayService {
939
1098
  conflictResolved: number;
940
1099
  conflictAbsorbed: number;
941
1100
  contentRefreshed: number;
942
- }, patchesReverted?: number): ReplayReport;
1101
+ }, patchesReverted?: number, degradedReasons?: DegradedReason[]): ReplayReport;
943
1102
  }
944
1103
 
945
1104
  interface MigrationAnalysis {
@@ -1060,7 +1219,7 @@ interface ForgetResult {
1060
1219
  /** Matching patches for interactive selection (search/no-arg mode only) */
1061
1220
  matched?: MatchedPatch[];
1062
1221
  }
1063
- declare function forget(outputDir: string, options?: ForgetOptions): ForgetResult;
1222
+ declare function forget(outputDir: string, options?: ForgetOptions): Promise<ForgetResult>;
1064
1223
 
1065
1224
  interface ResetOptions {
1066
1225
  /** Don't actually delete, just show what would happen */
@@ -1097,6 +1256,15 @@ interface ResolveResult {
1097
1256
  patchesApplied?: number;
1098
1257
  /** Number of patches resolved and committed (phase 2) */
1099
1258
  patchesResolved?: number;
1259
+ /**
1260
+ * Patches permanently dismissed during phase 2 — the customer kept the
1261
+ * generator's version for every file in the patch. These are tombstoned
1262
+ * in the lockfile and will not be re-detected on future generations.
1263
+ */
1264
+ patchesDismissed?: Array<{
1265
+ id: string;
1266
+ message: string;
1267
+ }>;
1100
1268
  /** Non-fatal diagnostic messages (e.g., per-patch fallbacks) */
1101
1269
  warnings?: string[];
1102
1270
  }
@@ -1115,6 +1283,28 @@ interface StatusResult {
1115
1283
  unresolvedCount: number;
1116
1284
  /** Exclude patterns from replay.yml */
1117
1285
  excludePatterns: string[];
1286
+ /**
1287
+ * Customizations the customer permanently dismissed — via `forget`, or by
1288
+ * keeping the generator's version when resolving a conflict. These will
1289
+ * not be re-detected on future generations.
1290
+ */
1291
+ dismissed: StatusDismissal[];
1292
+ /**
1293
+ * Count of forgotten hashes recorded before dismissal metadata existed.
1294
+ * Such entries still tombstone re-detection but carry no message/commit
1295
+ * details to display.
1296
+ */
1297
+ legacyDismissedCount: number;
1298
+ }
1299
+ interface StatusDismissal {
1300
+ /** The customer's original commit message */
1301
+ message: string;
1302
+ /** Short SHA of the original commit (7 chars; empty for synthetic patches) */
1303
+ sha: string;
1304
+ /** Which route dismissed it */
1305
+ via: "forget" | "resolve";
1306
+ /** ISO timestamp of the dismissal */
1307
+ dismissedAt: string;
1118
1308
  }
1119
1309
  interface StatusPatch {
1120
1310
  /** Patch ID (e.g. "patch-def45678") */
@@ -1146,4 +1336,4 @@ interface StatusGeneration {
1146
1336
  }
1147
1337
  declare function status(outputDir: string): StatusResult;
1148
1338
 
1149
- export { type ApplyOptions, type BootstrapOptions, type BootstrapResult, CREDENTIAL_PATTERNS, type CommitInfo, type CommitOptions, type ConflictDetail, type ConflictMetadata, type ConflictReason, type ConflictRegion, type CredentialPattern, type CustomizationsConfig, type DetectionResult, type DiffStat, FERN_BOT_EMAIL, FERN_BOT_LOGIN, FERN_BOT_NAME, FernignoreMigrator, type FileResult, type FlowKind, type ForgetOptions, type ForgetResult, type GenerationLock, type GenerationRecord, GitClient, LockfileManager, LockfileNotFoundError, type MatchedPatch, type MergeResult, type MigrationAnalysis, type MigrationResult, type MoveDeclaration, ReplayApplicator, ReplayCommitter, type ReplayConfig, ReplayDetector, type ReplayOptions, type ReplayPreparation, type ReplayReport, type ReplayResult, ReplayService, type ResetOptions, type ResetResult, type ResolveOptions, type ResolveResult, SENSITIVE_FILE_PATTERNS, type StatusGeneration, type StatusPatch, type StatusResult, type StoredPatch, type UnresolvedPatchInfo, bootstrap, forget, isGenerationCommit, isReplayCommit, isRevertCommit, isSensitiveFile, parseRevertedMessage, parseRevertedSha, reset, resolve, scanForCredentials, status, threeWayMerge };
1339
+ export { type ApplyOptions, type BootstrapOptions, type BootstrapResult, CREDENTIAL_PATTERNS, type CommitInfo, type CommitOptions, type ConflictDetail, type ConflictMetadata, type ConflictReason, type ConflictRegion, type CredentialPattern, type CustomizationsConfig, type DegradedReason, type DegradedReasonCode, type DetectionResult, type DiffStat, type DismissalRecord, FERN_BOT_EMAIL, FERN_BOT_LOGIN, FERN_BOT_NAME, FernignoreMigrator, type FileResult, type FlowKind, type ForgetOptions, type ForgetResult, type GenerationLock, type GenerationRecord, GitClient, LockfileManager, LockfileNotFoundError, type MatchedPatch, type MaterializedCommitPatch, type MergeResult, type MigrationAnalysis, type MigrationResult, type MoveDeclaration, ReplayApplicator, ReplayCommitter, type ReplayConfig, ReplayDetector, type ReplayOptions, type ReplayPreparation, type ReplayReport, type ReplayResult, ReplayService, type ResetOptions, type ResetResult, type ResolveOptions, type ResolveResult, SENSITIVE_FILE_PATTERNS, type StatusDismissal, type StatusGeneration, type StatusPatch, type StatusResult, type StoredPatch, type UnresolvedPatchInfo, bootstrap, forget, isGenerationCommit, isReplayCommit, isRevertCommit, isSensitiveFile, parseRevertedMessage, parseRevertedSha, reset, resolve, scanForCredentials, status, threeWayMerge };