@fern-api/replay 0.13.0 → 0.14.1
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 +3722 -2789
- package/dist/cli.cjs.map +1 -1
- package/dist/index.cjs +2227 -1288
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +158 -5
- package/dist/index.d.ts +158 -5
- package/dist/index.js +2226 -1288
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.cts
CHANGED
|
@@ -37,6 +37,31 @@ interface StoredPatch {
|
|
|
37
37
|
* the generator will not start producing a file it never produced before.
|
|
38
38
|
*/
|
|
39
39
|
user_owned?: boolean;
|
|
40
|
+
/**
|
|
41
|
+
* THEIRS snapshot — the post-customer-edit content of each file the patch
|
|
42
|
+
* touches. When present, replay's slow-path 3-way merge uses this directly
|
|
43
|
+
* as THEIRS, bypassing `applyPatchToContent(BASE, patch_content)`
|
|
44
|
+
* reconstruction.
|
|
45
|
+
*
|
|
46
|
+
* Why this exists: `patch_content` is a unified diff anchored to specific
|
|
47
|
+
* line numbers in BASE. When the generator structurally changes a file
|
|
48
|
+
* across regens (different types, dropped/added imports, etc.), those
|
|
49
|
+
* line anchors no longer survive — reconstruction fails or produces
|
|
50
|
+
* garbage, and the customer's customization gets lost. The snapshot
|
|
51
|
+
* stores the customer's intent at content level, not at line-anchor
|
|
52
|
+
* level. diff3 then operates on actual content (BASE vs OURS vs THEIRS)
|
|
53
|
+
* and naturally handles structural changes via merge semantics rather
|
|
54
|
+
* than line bookkeeping.
|
|
55
|
+
*
|
|
56
|
+
* Keys are post-rename paths (matching the `files` array). Files the
|
|
57
|
+
* patch deletes are omitted from the map; the `+++ /dev/null` marker
|
|
58
|
+
* in `patch_content` remains the canonical delete signal.
|
|
59
|
+
*
|
|
60
|
+
* Optional for backward compat. Existing lockfiles without this field
|
|
61
|
+
* auto-migrate on the first regen post-upgrade via
|
|
62
|
+
* `preGenerationRebase`'s migration pass.
|
|
63
|
+
*/
|
|
64
|
+
theirs_snapshot?: Record<string, string>;
|
|
40
65
|
}
|
|
41
66
|
interface CustomizationsConfig {
|
|
42
67
|
exclude?: string[];
|
|
@@ -69,6 +94,15 @@ interface FileResult {
|
|
|
69
94
|
conflicts?: ConflictRegion[];
|
|
70
95
|
conflictReason?: ConflictReason;
|
|
71
96
|
conflictMetadata?: ConflictMetadata;
|
|
97
|
+
/**
|
|
98
|
+
* Lines that appeared as unchanged context in the customer's THEIRS (also
|
|
99
|
+
* present in BASE) but were absent from the merged output because OURS
|
|
100
|
+
* deleted them. diff3 sides with the deletion correctly, but the customer
|
|
101
|
+
* may not have realized those lines depended on the generator never
|
|
102
|
+
* removing them. Surfaced as a warning so the customer can re-add them
|
|
103
|
+
* if needed. Empty/undefined when no such lines exist.
|
|
104
|
+
*/
|
|
105
|
+
droppedContextLines?: string[];
|
|
72
106
|
}
|
|
73
107
|
interface ConflictRegion {
|
|
74
108
|
startLine: number;
|
|
@@ -80,6 +114,14 @@ interface MergeResult {
|
|
|
80
114
|
content: string;
|
|
81
115
|
hasConflicts: boolean;
|
|
82
116
|
conflicts: ConflictRegion[];
|
|
117
|
+
/**
|
|
118
|
+
* Lines present in BOTH base and theirs but absent from the merged output.
|
|
119
|
+
* These are lines the customer's THEIRS treated as context; OURS deleted
|
|
120
|
+
* them; diff3 sided with the deletion. Empty when no such lines exist.
|
|
121
|
+
* Only populated when `hasConflicts` is false — when conflicts surface,
|
|
122
|
+
* the customer already knows something needs review.
|
|
123
|
+
*/
|
|
124
|
+
droppedContextLines?: string[];
|
|
83
125
|
}
|
|
84
126
|
interface CommitInfo {
|
|
85
127
|
sha: string;
|
|
@@ -182,14 +224,14 @@ declare class LockfileManager {
|
|
|
182
224
|
save(): void;
|
|
183
225
|
addGeneration(record: GenerationRecord): void;
|
|
184
226
|
addPatch(patch: StoredPatch): void;
|
|
185
|
-
updatePatch(patchId: string, updates: Partial<Pick<StoredPatch, "base_generation" | "patch_content" | "content_hash" | "files" | "status" | "user_owned">>): void;
|
|
227
|
+
updatePatch(patchId: string, updates: Partial<Pick<StoredPatch, "base_generation" | "patch_content" | "content_hash" | "files" | "status" | "user_owned" | "theirs_snapshot">>): void;
|
|
186
228
|
removePatch(patchId: string): void;
|
|
187
229
|
clearPatches(): void;
|
|
188
230
|
addForgottenHash(hash: string): void;
|
|
189
231
|
getUnresolvedPatches(): StoredPatch[];
|
|
190
232
|
getResolvingPatches(): StoredPatch[];
|
|
191
233
|
markPatchUnresolved(patchId: string): void;
|
|
192
|
-
markPatchResolved(patchId: string, updates: Pick<StoredPatch, "patch_content" | "content_hash" | "base_generation" | "files">): void;
|
|
234
|
+
markPatchResolved(patchId: string, updates: Pick<StoredPatch, "patch_content" | "content_hash" | "base_generation" | "files" | "theirs_snapshot">): void;
|
|
193
235
|
getPatches(): StoredPatch[];
|
|
194
236
|
setReplaySkippedAt(timestamp: string): void;
|
|
195
237
|
clearReplaySkippedAt(): void;
|
|
@@ -346,6 +388,39 @@ declare class ReplayApplicator {
|
|
|
346
388
|
private renameCache;
|
|
347
389
|
private treeExistsCache;
|
|
348
390
|
private fileTheirsAccumulator;
|
|
391
|
+
/**
|
|
392
|
+
* Apply mode for the current `applyPatches` invocation. Set at the start
|
|
393
|
+
* of `applyPatches`, read by `mergeFile` to decide:
|
|
394
|
+
* - whether to skip the intra-loop marker strip (kept in `applyPatches`)
|
|
395
|
+
* - whether to populate the accumulator after a conflicted merge
|
|
396
|
+
* (resolve mode populates so subsequent patches on the same file
|
|
397
|
+
* can use patch A's THEIRS as a structurally-correct merge base
|
|
398
|
+
* when their diff was authored against post-A structure)
|
|
399
|
+
* - whether to retry THEIRS reconstruction against the accumulator
|
|
400
|
+
* when the BASE-relative reconstruction produced markers from a
|
|
401
|
+
* cross-patch context mismatch
|
|
402
|
+
*/
|
|
403
|
+
private currentApplyMode;
|
|
404
|
+
/**
|
|
405
|
+
* Set of files that appear in 2+ patches in the current `applyPatches`
|
|
406
|
+
* invocation. Computed at the top of `applyPatches`. Read by `mergeFile`
|
|
407
|
+
* and `populateAccumulatorForPatch` to decide whether to use the patch's
|
|
408
|
+
* `theirs_snapshot` directly as THEIRS (snapshot-as-primary) or fall
|
|
409
|
+
* back to line-anchored reconstruction (FER-9525 cross-patch isolation).
|
|
410
|
+
*
|
|
411
|
+
* Snapshot-as-primary is correct when the patch owns its file (no other
|
|
412
|
+
* patch in this run touches it): the snapshot is what the customer
|
|
413
|
+
* intends for that file post-regen, regardless of generator structural
|
|
414
|
+
* changes that would invalidate the diff's line anchors.
|
|
415
|
+
*
|
|
416
|
+
* When a file IS shared, snapshots are CUMULATIVE across the patches
|
|
417
|
+
* touching it (each one's snapshot includes prior patches' contributions).
|
|
418
|
+
* Using a later patch's cumulative snapshot as its individual THEIRS
|
|
419
|
+
* would propagate prior patches' edits into the merge — surfacing nested
|
|
420
|
+
* conflicts at regions the patch alone never touched. Reconstruction
|
|
421
|
+
* via `applyPatchToContent` is required there.
|
|
422
|
+
*/
|
|
423
|
+
private sharedFiles;
|
|
349
424
|
constructor(git: GitClient, lockManager: LockfileManager, outputDir: string);
|
|
350
425
|
/**
|
|
351
426
|
* Resolve the GenerationRecord for a patch's base_generation.
|
|
@@ -369,9 +444,31 @@ declare class ReplayApplicator {
|
|
|
369
444
|
/**
|
|
370
445
|
* Apply all patches, returning results for each.
|
|
371
446
|
* Skips patches that match exclude patterns in replay.yml
|
|
447
|
+
*
|
|
448
|
+
* `applyMode` controls the post-conflict marker strategy:
|
|
449
|
+
* - `"replay"` (default): strip conflict markers from disk between
|
|
450
|
+
* iterations whenever a later patch touches the same file. Lets the
|
|
451
|
+
* follow-up patch's `git apply --3way` see clean OURS content,
|
|
452
|
+
* which is what the silent-loss fix (PR #73) relies on. The replay
|
|
453
|
+
* pipeline calls `revertConflictingFiles` after the loop to clean
|
|
454
|
+
* any markers that survive.
|
|
455
|
+
* - `"resolve"`: keep markers on disk. The resolve command needs the
|
|
456
|
+
* customer to see them. Slow-path 3-way merge naturally preserves
|
|
457
|
+
* A's markers and B's clean writes when their regions don't overlap;
|
|
458
|
+
* when they do overlap, the customer gets nested markers and
|
|
459
|
+
* resolves manually. Either way they aren't silently dropped.
|
|
460
|
+
*/
|
|
461
|
+
applyPatches(patches: StoredPatch[], opts?: {
|
|
462
|
+
applyMode?: "replay" | "resolve";
|
|
463
|
+
}): Promise<ReplayResult[]>;
|
|
464
|
+
/**
|
|
465
|
+
* Populate accumulator after git apply succeeds, AND collect per-file
|
|
466
|
+
* results including any "dropped context lines" — lines that were
|
|
467
|
+
* present in BASE and THEIRS (unchanged context) but absent from the
|
|
468
|
+
* final on-disk state because OURS deleted them. This is the fast-path
|
|
469
|
+
* counterpart to ThreeWayMerge.computeDroppedContextLines used by the
|
|
470
|
+
* 3-way slow path; both must surface the same warning.
|
|
372
471
|
*/
|
|
373
|
-
applyPatches(patches: StoredPatch[]): Promise<ReplayResult[]>;
|
|
374
|
-
/** Populate accumulator after git apply succeeds. */
|
|
375
472
|
private populateAccumulatorForPatch;
|
|
376
473
|
private applyPatchWithFallback;
|
|
377
474
|
private applyWithThreeWayMerge;
|
|
@@ -403,7 +500,13 @@ declare class ReplayCommitter {
|
|
|
403
500
|
private outputDir;
|
|
404
501
|
constructor(git: GitClient, outputDir: string);
|
|
405
502
|
commitGeneration(message: string, options?: CommitOptions): Promise<string>;
|
|
406
|
-
commitReplay(_patchCount: number, patches?: StoredPatch[], message?: string
|
|
503
|
+
commitReplay(_patchCount: number, patches?: StoredPatch[], message?: string, options?: {
|
|
504
|
+
buckets?: {
|
|
505
|
+
applied: StoredPatch[];
|
|
506
|
+
unresolved: StoredPatch[];
|
|
507
|
+
absorbed: StoredPatch[];
|
|
508
|
+
};
|
|
509
|
+
}): Promise<string>;
|
|
407
510
|
createGenerationRecord(options?: CommitOptions): Promise<GenerationRecord>;
|
|
408
511
|
stageAll(): Promise<void>;
|
|
409
512
|
hasStagedChanges(): Promise<boolean>;
|
|
@@ -622,6 +725,45 @@ declare class ReplayService {
|
|
|
622
725
|
* Returns the number of patches rebased.
|
|
623
726
|
*/
|
|
624
727
|
private rebasePatches;
|
|
728
|
+
/**
|
|
729
|
+
* Read THEIRS snapshot (post-customer-edit content) for a list of files
|
|
730
|
+
* from the working tree on disk. Used by `rebasePatches` after a clean
|
|
731
|
+
* apply — disk = correctly-merged customer state at the new generation.
|
|
732
|
+
* Skips binary files (matches `mergeFile`/`populateAccumulatorForPatch`
|
|
733
|
+
* policy — binary content as `utf-8` is lossy and snapshots of it would
|
|
734
|
+
* never be used during apply anyway).
|
|
735
|
+
*/
|
|
736
|
+
private readSnapshotFromDisk;
|
|
737
|
+
/**
|
|
738
|
+
* Read THEIRS snapshot from a git tree-ish (commit, branch, or tree).
|
|
739
|
+
* Used when the diff that produced `patch_content` was relative to that
|
|
740
|
+
* tree (e.g., `git diff currentGen HEAD --` → snapshot from HEAD).
|
|
741
|
+
* Skips binary files.
|
|
742
|
+
*/
|
|
743
|
+
private readSnapshotFromTree;
|
|
744
|
+
/**
|
|
745
|
+
* One-shot auto-migration: backfill `theirs_snapshot` on existing
|
|
746
|
+
* patches that predate the snapshot encoding. Computes the snapshot by
|
|
747
|
+
* applying the patch's `patch_content` to its `base_generation` tree —
|
|
748
|
+
* yielding the patch's INDIVIDUAL contribution.
|
|
749
|
+
*
|
|
750
|
+
* **Skips patches sharing files with other patches.** HEAD's content
|
|
751
|
+
* for a file shared by N patches is cumulative across all of them, so
|
|
752
|
+
* a HEAD-fallback would falsely encode other patches' contributions
|
|
753
|
+
* into one patch's snapshot — breaking FER-9525 cross-patch isolation
|
|
754
|
+
* if the snapshot fallback later fires. Per-patch snapshots only
|
|
755
|
+
* make sense when the patch owns its file. Multi-patch-same-file
|
|
756
|
+
* legacy lockfiles keep using line-anchored reconstruction (the
|
|
757
|
+
* existing path); they're no worse than pre-upgrade.
|
|
758
|
+
*
|
|
759
|
+
* **Does not fall back to HEAD when BASE-reconstruction fails.** Same
|
|
760
|
+
* reason — HEAD content can be cumulative. Patches whose
|
|
761
|
+
* reconstruction fails just don't get a snapshot; legacy paths
|
|
762
|
+
* (PR #73's by-association gate, accumulator fallback) handle them.
|
|
763
|
+
*
|
|
764
|
+
* Skips patches that already have a snapshot.
|
|
765
|
+
*/
|
|
766
|
+
private migratePatchesToSnapshot;
|
|
625
767
|
/**
|
|
626
768
|
* Determine whether `file` is user-owned (never produced by the generator).
|
|
627
769
|
*
|
|
@@ -668,6 +810,15 @@ declare class ReplayService {
|
|
|
668
810
|
* Called BEFORE commitGeneration() while HEAD has customer code.
|
|
669
811
|
*/
|
|
670
812
|
private preGenerationRebase;
|
|
813
|
+
/**
|
|
814
|
+
* After applyPatches(), surface a warning for each (patch × file) where
|
|
815
|
+
* diff3 silently dropped lines that were unchanged context in the
|
|
816
|
+
* customer's THEIRS. The deletion stays on disk (correct diff3 outcome),
|
|
817
|
+
* but the customer must learn so they can re-add the lines if they
|
|
818
|
+
* relied on them. This is the "surface or preserve, never silently drop"
|
|
819
|
+
* contract for legitimate-deletion cases.
|
|
820
|
+
*/
|
|
821
|
+
private recordDroppedContextLineWarnings;
|
|
671
822
|
/**
|
|
672
823
|
* After applyPatches(), strip conflict markers from conflicting files
|
|
673
824
|
* so only clean content is committed. Keeps the Generated (OURS) side.
|
|
@@ -839,6 +990,8 @@ interface ResolveResult {
|
|
|
839
990
|
patchesApplied?: number;
|
|
840
991
|
/** Number of patches resolved and committed (phase 2) */
|
|
841
992
|
patchesResolved?: number;
|
|
993
|
+
/** Non-fatal diagnostic messages (e.g., per-patch fallbacks) */
|
|
994
|
+
warnings?: string[];
|
|
842
995
|
}
|
|
843
996
|
declare function resolve(outputDir: string, options?: ResolveOptions): Promise<ResolveResult>;
|
|
844
997
|
|
package/dist/index.d.ts
CHANGED
|
@@ -37,6 +37,31 @@ interface StoredPatch {
|
|
|
37
37
|
* the generator will not start producing a file it never produced before.
|
|
38
38
|
*/
|
|
39
39
|
user_owned?: boolean;
|
|
40
|
+
/**
|
|
41
|
+
* THEIRS snapshot — the post-customer-edit content of each file the patch
|
|
42
|
+
* touches. When present, replay's slow-path 3-way merge uses this directly
|
|
43
|
+
* as THEIRS, bypassing `applyPatchToContent(BASE, patch_content)`
|
|
44
|
+
* reconstruction.
|
|
45
|
+
*
|
|
46
|
+
* Why this exists: `patch_content` is a unified diff anchored to specific
|
|
47
|
+
* line numbers in BASE. When the generator structurally changes a file
|
|
48
|
+
* across regens (different types, dropped/added imports, etc.), those
|
|
49
|
+
* line anchors no longer survive — reconstruction fails or produces
|
|
50
|
+
* garbage, and the customer's customization gets lost. The snapshot
|
|
51
|
+
* stores the customer's intent at content level, not at line-anchor
|
|
52
|
+
* level. diff3 then operates on actual content (BASE vs OURS vs THEIRS)
|
|
53
|
+
* and naturally handles structural changes via merge semantics rather
|
|
54
|
+
* than line bookkeeping.
|
|
55
|
+
*
|
|
56
|
+
* Keys are post-rename paths (matching the `files` array). Files the
|
|
57
|
+
* patch deletes are omitted from the map; the `+++ /dev/null` marker
|
|
58
|
+
* in `patch_content` remains the canonical delete signal.
|
|
59
|
+
*
|
|
60
|
+
* Optional for backward compat. Existing lockfiles without this field
|
|
61
|
+
* auto-migrate on the first regen post-upgrade via
|
|
62
|
+
* `preGenerationRebase`'s migration pass.
|
|
63
|
+
*/
|
|
64
|
+
theirs_snapshot?: Record<string, string>;
|
|
40
65
|
}
|
|
41
66
|
interface CustomizationsConfig {
|
|
42
67
|
exclude?: string[];
|
|
@@ -69,6 +94,15 @@ interface FileResult {
|
|
|
69
94
|
conflicts?: ConflictRegion[];
|
|
70
95
|
conflictReason?: ConflictReason;
|
|
71
96
|
conflictMetadata?: ConflictMetadata;
|
|
97
|
+
/**
|
|
98
|
+
* Lines that appeared as unchanged context in the customer's THEIRS (also
|
|
99
|
+
* present in BASE) but were absent from the merged output because OURS
|
|
100
|
+
* deleted them. diff3 sides with the deletion correctly, but the customer
|
|
101
|
+
* may not have realized those lines depended on the generator never
|
|
102
|
+
* removing them. Surfaced as a warning so the customer can re-add them
|
|
103
|
+
* if needed. Empty/undefined when no such lines exist.
|
|
104
|
+
*/
|
|
105
|
+
droppedContextLines?: string[];
|
|
72
106
|
}
|
|
73
107
|
interface ConflictRegion {
|
|
74
108
|
startLine: number;
|
|
@@ -80,6 +114,14 @@ interface MergeResult {
|
|
|
80
114
|
content: string;
|
|
81
115
|
hasConflicts: boolean;
|
|
82
116
|
conflicts: ConflictRegion[];
|
|
117
|
+
/**
|
|
118
|
+
* Lines present in BOTH base and theirs but absent from the merged output.
|
|
119
|
+
* These are lines the customer's THEIRS treated as context; OURS deleted
|
|
120
|
+
* them; diff3 sided with the deletion. Empty when no such lines exist.
|
|
121
|
+
* Only populated when `hasConflicts` is false — when conflicts surface,
|
|
122
|
+
* the customer already knows something needs review.
|
|
123
|
+
*/
|
|
124
|
+
droppedContextLines?: string[];
|
|
83
125
|
}
|
|
84
126
|
interface CommitInfo {
|
|
85
127
|
sha: string;
|
|
@@ -182,14 +224,14 @@ declare class LockfileManager {
|
|
|
182
224
|
save(): void;
|
|
183
225
|
addGeneration(record: GenerationRecord): void;
|
|
184
226
|
addPatch(patch: StoredPatch): void;
|
|
185
|
-
updatePatch(patchId: string, updates: Partial<Pick<StoredPatch, "base_generation" | "patch_content" | "content_hash" | "files" | "status" | "user_owned">>): void;
|
|
227
|
+
updatePatch(patchId: string, updates: Partial<Pick<StoredPatch, "base_generation" | "patch_content" | "content_hash" | "files" | "status" | "user_owned" | "theirs_snapshot">>): void;
|
|
186
228
|
removePatch(patchId: string): void;
|
|
187
229
|
clearPatches(): void;
|
|
188
230
|
addForgottenHash(hash: string): void;
|
|
189
231
|
getUnresolvedPatches(): StoredPatch[];
|
|
190
232
|
getResolvingPatches(): StoredPatch[];
|
|
191
233
|
markPatchUnresolved(patchId: string): void;
|
|
192
|
-
markPatchResolved(patchId: string, updates: Pick<StoredPatch, "patch_content" | "content_hash" | "base_generation" | "files">): void;
|
|
234
|
+
markPatchResolved(patchId: string, updates: Pick<StoredPatch, "patch_content" | "content_hash" | "base_generation" | "files" | "theirs_snapshot">): void;
|
|
193
235
|
getPatches(): StoredPatch[];
|
|
194
236
|
setReplaySkippedAt(timestamp: string): void;
|
|
195
237
|
clearReplaySkippedAt(): void;
|
|
@@ -346,6 +388,39 @@ declare class ReplayApplicator {
|
|
|
346
388
|
private renameCache;
|
|
347
389
|
private treeExistsCache;
|
|
348
390
|
private fileTheirsAccumulator;
|
|
391
|
+
/**
|
|
392
|
+
* Apply mode for the current `applyPatches` invocation. Set at the start
|
|
393
|
+
* of `applyPatches`, read by `mergeFile` to decide:
|
|
394
|
+
* - whether to skip the intra-loop marker strip (kept in `applyPatches`)
|
|
395
|
+
* - whether to populate the accumulator after a conflicted merge
|
|
396
|
+
* (resolve mode populates so subsequent patches on the same file
|
|
397
|
+
* can use patch A's THEIRS as a structurally-correct merge base
|
|
398
|
+
* when their diff was authored against post-A structure)
|
|
399
|
+
* - whether to retry THEIRS reconstruction against the accumulator
|
|
400
|
+
* when the BASE-relative reconstruction produced markers from a
|
|
401
|
+
* cross-patch context mismatch
|
|
402
|
+
*/
|
|
403
|
+
private currentApplyMode;
|
|
404
|
+
/**
|
|
405
|
+
* Set of files that appear in 2+ patches in the current `applyPatches`
|
|
406
|
+
* invocation. Computed at the top of `applyPatches`. Read by `mergeFile`
|
|
407
|
+
* and `populateAccumulatorForPatch` to decide whether to use the patch's
|
|
408
|
+
* `theirs_snapshot` directly as THEIRS (snapshot-as-primary) or fall
|
|
409
|
+
* back to line-anchored reconstruction (FER-9525 cross-patch isolation).
|
|
410
|
+
*
|
|
411
|
+
* Snapshot-as-primary is correct when the patch owns its file (no other
|
|
412
|
+
* patch in this run touches it): the snapshot is what the customer
|
|
413
|
+
* intends for that file post-regen, regardless of generator structural
|
|
414
|
+
* changes that would invalidate the diff's line anchors.
|
|
415
|
+
*
|
|
416
|
+
* When a file IS shared, snapshots are CUMULATIVE across the patches
|
|
417
|
+
* touching it (each one's snapshot includes prior patches' contributions).
|
|
418
|
+
* Using a later patch's cumulative snapshot as its individual THEIRS
|
|
419
|
+
* would propagate prior patches' edits into the merge — surfacing nested
|
|
420
|
+
* conflicts at regions the patch alone never touched. Reconstruction
|
|
421
|
+
* via `applyPatchToContent` is required there.
|
|
422
|
+
*/
|
|
423
|
+
private sharedFiles;
|
|
349
424
|
constructor(git: GitClient, lockManager: LockfileManager, outputDir: string);
|
|
350
425
|
/**
|
|
351
426
|
* Resolve the GenerationRecord for a patch's base_generation.
|
|
@@ -369,9 +444,31 @@ declare class ReplayApplicator {
|
|
|
369
444
|
/**
|
|
370
445
|
* Apply all patches, returning results for each.
|
|
371
446
|
* Skips patches that match exclude patterns in replay.yml
|
|
447
|
+
*
|
|
448
|
+
* `applyMode` controls the post-conflict marker strategy:
|
|
449
|
+
* - `"replay"` (default): strip conflict markers from disk between
|
|
450
|
+
* iterations whenever a later patch touches the same file. Lets the
|
|
451
|
+
* follow-up patch's `git apply --3way` see clean OURS content,
|
|
452
|
+
* which is what the silent-loss fix (PR #73) relies on. The replay
|
|
453
|
+
* pipeline calls `revertConflictingFiles` after the loop to clean
|
|
454
|
+
* any markers that survive.
|
|
455
|
+
* - `"resolve"`: keep markers on disk. The resolve command needs the
|
|
456
|
+
* customer to see them. Slow-path 3-way merge naturally preserves
|
|
457
|
+
* A's markers and B's clean writes when their regions don't overlap;
|
|
458
|
+
* when they do overlap, the customer gets nested markers and
|
|
459
|
+
* resolves manually. Either way they aren't silently dropped.
|
|
460
|
+
*/
|
|
461
|
+
applyPatches(patches: StoredPatch[], opts?: {
|
|
462
|
+
applyMode?: "replay" | "resolve";
|
|
463
|
+
}): Promise<ReplayResult[]>;
|
|
464
|
+
/**
|
|
465
|
+
* Populate accumulator after git apply succeeds, AND collect per-file
|
|
466
|
+
* results including any "dropped context lines" — lines that were
|
|
467
|
+
* present in BASE and THEIRS (unchanged context) but absent from the
|
|
468
|
+
* final on-disk state because OURS deleted them. This is the fast-path
|
|
469
|
+
* counterpart to ThreeWayMerge.computeDroppedContextLines used by the
|
|
470
|
+
* 3-way slow path; both must surface the same warning.
|
|
372
471
|
*/
|
|
373
|
-
applyPatches(patches: StoredPatch[]): Promise<ReplayResult[]>;
|
|
374
|
-
/** Populate accumulator after git apply succeeds. */
|
|
375
472
|
private populateAccumulatorForPatch;
|
|
376
473
|
private applyPatchWithFallback;
|
|
377
474
|
private applyWithThreeWayMerge;
|
|
@@ -403,7 +500,13 @@ declare class ReplayCommitter {
|
|
|
403
500
|
private outputDir;
|
|
404
501
|
constructor(git: GitClient, outputDir: string);
|
|
405
502
|
commitGeneration(message: string, options?: CommitOptions): Promise<string>;
|
|
406
|
-
commitReplay(_patchCount: number, patches?: StoredPatch[], message?: string
|
|
503
|
+
commitReplay(_patchCount: number, patches?: StoredPatch[], message?: string, options?: {
|
|
504
|
+
buckets?: {
|
|
505
|
+
applied: StoredPatch[];
|
|
506
|
+
unresolved: StoredPatch[];
|
|
507
|
+
absorbed: StoredPatch[];
|
|
508
|
+
};
|
|
509
|
+
}): Promise<string>;
|
|
407
510
|
createGenerationRecord(options?: CommitOptions): Promise<GenerationRecord>;
|
|
408
511
|
stageAll(): Promise<void>;
|
|
409
512
|
hasStagedChanges(): Promise<boolean>;
|
|
@@ -622,6 +725,45 @@ declare class ReplayService {
|
|
|
622
725
|
* Returns the number of patches rebased.
|
|
623
726
|
*/
|
|
624
727
|
private rebasePatches;
|
|
728
|
+
/**
|
|
729
|
+
* Read THEIRS snapshot (post-customer-edit content) for a list of files
|
|
730
|
+
* from the working tree on disk. Used by `rebasePatches` after a clean
|
|
731
|
+
* apply — disk = correctly-merged customer state at the new generation.
|
|
732
|
+
* Skips binary files (matches `mergeFile`/`populateAccumulatorForPatch`
|
|
733
|
+
* policy — binary content as `utf-8` is lossy and snapshots of it would
|
|
734
|
+
* never be used during apply anyway).
|
|
735
|
+
*/
|
|
736
|
+
private readSnapshotFromDisk;
|
|
737
|
+
/**
|
|
738
|
+
* Read THEIRS snapshot from a git tree-ish (commit, branch, or tree).
|
|
739
|
+
* Used when the diff that produced `patch_content` was relative to that
|
|
740
|
+
* tree (e.g., `git diff currentGen HEAD --` → snapshot from HEAD).
|
|
741
|
+
* Skips binary files.
|
|
742
|
+
*/
|
|
743
|
+
private readSnapshotFromTree;
|
|
744
|
+
/**
|
|
745
|
+
* One-shot auto-migration: backfill `theirs_snapshot` on existing
|
|
746
|
+
* patches that predate the snapshot encoding. Computes the snapshot by
|
|
747
|
+
* applying the patch's `patch_content` to its `base_generation` tree —
|
|
748
|
+
* yielding the patch's INDIVIDUAL contribution.
|
|
749
|
+
*
|
|
750
|
+
* **Skips patches sharing files with other patches.** HEAD's content
|
|
751
|
+
* for a file shared by N patches is cumulative across all of them, so
|
|
752
|
+
* a HEAD-fallback would falsely encode other patches' contributions
|
|
753
|
+
* into one patch's snapshot — breaking FER-9525 cross-patch isolation
|
|
754
|
+
* if the snapshot fallback later fires. Per-patch snapshots only
|
|
755
|
+
* make sense when the patch owns its file. Multi-patch-same-file
|
|
756
|
+
* legacy lockfiles keep using line-anchored reconstruction (the
|
|
757
|
+
* existing path); they're no worse than pre-upgrade.
|
|
758
|
+
*
|
|
759
|
+
* **Does not fall back to HEAD when BASE-reconstruction fails.** Same
|
|
760
|
+
* reason — HEAD content can be cumulative. Patches whose
|
|
761
|
+
* reconstruction fails just don't get a snapshot; legacy paths
|
|
762
|
+
* (PR #73's by-association gate, accumulator fallback) handle them.
|
|
763
|
+
*
|
|
764
|
+
* Skips patches that already have a snapshot.
|
|
765
|
+
*/
|
|
766
|
+
private migratePatchesToSnapshot;
|
|
625
767
|
/**
|
|
626
768
|
* Determine whether `file` is user-owned (never produced by the generator).
|
|
627
769
|
*
|
|
@@ -668,6 +810,15 @@ declare class ReplayService {
|
|
|
668
810
|
* Called BEFORE commitGeneration() while HEAD has customer code.
|
|
669
811
|
*/
|
|
670
812
|
private preGenerationRebase;
|
|
813
|
+
/**
|
|
814
|
+
* After applyPatches(), surface a warning for each (patch × file) where
|
|
815
|
+
* diff3 silently dropped lines that were unchanged context in the
|
|
816
|
+
* customer's THEIRS. The deletion stays on disk (correct diff3 outcome),
|
|
817
|
+
* but the customer must learn so they can re-add the lines if they
|
|
818
|
+
* relied on them. This is the "surface or preserve, never silently drop"
|
|
819
|
+
* contract for legitimate-deletion cases.
|
|
820
|
+
*/
|
|
821
|
+
private recordDroppedContextLineWarnings;
|
|
671
822
|
/**
|
|
672
823
|
* After applyPatches(), strip conflict markers from conflicting files
|
|
673
824
|
* so only clean content is committed. Keeps the Generated (OURS) side.
|
|
@@ -839,6 +990,8 @@ interface ResolveResult {
|
|
|
839
990
|
patchesApplied?: number;
|
|
840
991
|
/** Number of patches resolved and committed (phase 2) */
|
|
841
992
|
patchesResolved?: number;
|
|
993
|
+
/** Non-fatal diagnostic messages (e.g., per-patch fallbacks) */
|
|
994
|
+
warnings?: string[];
|
|
842
995
|
}
|
|
843
996
|
declare function resolve(outputDir: string, options?: ResolveOptions): Promise<ResolveResult>;
|
|
844
997
|
|