@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/cli.cjs +427 -120
- package/dist/cli.cjs.map +1 -1
- package/dist/index.cjs +372 -111
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +193 -3
- package/dist/index.d.ts +193 -3
- package/dist/index.js +355 -94
- package/dist/index.js.map +1 -1
- package/package.json +3 -1
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 };
|