@fern-api/replay 0.16.2 → 0.17.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.cjs +80 -12
- package/dist/cli.cjs.map +1 -1
- package/dist/index.cjs +80 -12
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +27 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.js +80 -12
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.cts
CHANGED
|
@@ -101,6 +101,17 @@ interface FileResult {
|
|
|
101
101
|
* if needed. Empty/undefined when no such lines exist.
|
|
102
102
|
*/
|
|
103
103
|
droppedContextLines?: string[];
|
|
104
|
+
/**
|
|
105
|
+
* INC-866 silent-loss signal. Set on a `new-file-both` result when BASE was
|
|
106
|
+
* null because the patch's base-generation tree is unreachable (shallow
|
|
107
|
+
* clone window too small / pruned history) rather than because the file is
|
|
108
|
+
* genuinely new. In that state an edited-existing-file customization can be
|
|
109
|
+
* dropped with no conflict and no error. Surfaced as a ReplayReport warning
|
|
110
|
+
* (`recordUnreachableBaseWarnings`) so callers/CLI learn the clone window
|
|
111
|
+
* was insufficient instead of losing the edit silently. Undefined/false in
|
|
112
|
+
* the benign case.
|
|
113
|
+
*/
|
|
114
|
+
baseTreeUnreachable?: boolean;
|
|
104
115
|
}
|
|
105
116
|
interface ConflictRegion {
|
|
106
117
|
startLine: number;
|
|
@@ -885,6 +896,22 @@ declare class ReplayService {
|
|
|
885
896
|
*/
|
|
886
897
|
/** @internal Used by Flow implementations in `src/flows/`. */
|
|
887
898
|
recordDroppedContextLineWarnings(results: ReplayResult[]): void;
|
|
899
|
+
/**
|
|
900
|
+
* After applyPatches(), surface a warning for each (patch × file) where a
|
|
901
|
+
* customization's BASE could not be read because the patch's
|
|
902
|
+
* base-generation tree is unreachable in this clone (shallow-clone window
|
|
903
|
+
* too small / pruned history). In that state the merge pipeline falls
|
|
904
|
+
* through to `new-file-both` and the customer's edit can be dropped with NO
|
|
905
|
+
* conflict and NO error — the INC-866 silent-loss class. This converts that
|
|
906
|
+
* silence into an explicit, actionable warning naming the file and the
|
|
907
|
+
* likely cause (insufficient clone window).
|
|
908
|
+
*
|
|
909
|
+
* Mirrors `recordDroppedContextLineWarnings`'s "surface, never silently
|
|
910
|
+
* drop" contract. De-duplicated per file path so a single insufficient
|
|
911
|
+
* window doesn't spam one warning per touched file repeatedly.
|
|
912
|
+
*/
|
|
913
|
+
/** @internal Used by Flow implementations in `src/flows/`. */
|
|
914
|
+
recordUnreachableBaseWarnings(results: ReplayResult[]): void;
|
|
888
915
|
/**
|
|
889
916
|
* After applyPatches(), strip conflict markers from conflicting files
|
|
890
917
|
* so only clean content is committed. Keeps the Generated (OURS) side.
|
package/dist/index.d.ts
CHANGED
|
@@ -101,6 +101,17 @@ interface FileResult {
|
|
|
101
101
|
* if needed. Empty/undefined when no such lines exist.
|
|
102
102
|
*/
|
|
103
103
|
droppedContextLines?: string[];
|
|
104
|
+
/**
|
|
105
|
+
* INC-866 silent-loss signal. Set on a `new-file-both` result when BASE was
|
|
106
|
+
* null because the patch's base-generation tree is unreachable (shallow
|
|
107
|
+
* clone window too small / pruned history) rather than because the file is
|
|
108
|
+
* genuinely new. In that state an edited-existing-file customization can be
|
|
109
|
+
* dropped with no conflict and no error. Surfaced as a ReplayReport warning
|
|
110
|
+
* (`recordUnreachableBaseWarnings`) so callers/CLI learn the clone window
|
|
111
|
+
* was insufficient instead of losing the edit silently. Undefined/false in
|
|
112
|
+
* the benign case.
|
|
113
|
+
*/
|
|
114
|
+
baseTreeUnreachable?: boolean;
|
|
104
115
|
}
|
|
105
116
|
interface ConflictRegion {
|
|
106
117
|
startLine: number;
|
|
@@ -885,6 +896,22 @@ declare class ReplayService {
|
|
|
885
896
|
*/
|
|
886
897
|
/** @internal Used by Flow implementations in `src/flows/`. */
|
|
887
898
|
recordDroppedContextLineWarnings(results: ReplayResult[]): void;
|
|
899
|
+
/**
|
|
900
|
+
* After applyPatches(), surface a warning for each (patch × file) where a
|
|
901
|
+
* customization's BASE could not be read because the patch's
|
|
902
|
+
* base-generation tree is unreachable in this clone (shallow-clone window
|
|
903
|
+
* too small / pruned history). In that state the merge pipeline falls
|
|
904
|
+
* through to `new-file-both` and the customer's edit can be dropped with NO
|
|
905
|
+
* conflict and NO error — the INC-866 silent-loss class. This converts that
|
|
906
|
+
* silence into an explicit, actionable warning naming the file and the
|
|
907
|
+
* likely cause (insufficient clone window).
|
|
908
|
+
*
|
|
909
|
+
* Mirrors `recordDroppedContextLineWarnings`'s "surface, never silently
|
|
910
|
+
* drop" contract. De-duplicated per file path so a single insufficient
|
|
911
|
+
* window doesn't spam one warning per touched file repeatedly.
|
|
912
|
+
*/
|
|
913
|
+
/** @internal Used by Flow implementations in `src/flows/`. */
|
|
914
|
+
recordUnreachableBaseWarnings(results: ReplayResult[]): void;
|
|
888
915
|
/**
|
|
889
916
|
* After applyPatches(), strip conflict markers from conflicting files
|
|
890
917
|
* so only clean content is committed. Keeps the Generated (OURS) side.
|
package/dist/index.js
CHANGED
|
@@ -908,6 +908,22 @@ var ReplayDetector = class {
|
|
|
908
908
|
return commit.sha;
|
|
909
909
|
}
|
|
910
910
|
}
|
|
911
|
+
let fullLog;
|
|
912
|
+
try {
|
|
913
|
+
fullLog = await this.git.exec([
|
|
914
|
+
"log",
|
|
915
|
+
"--no-merges",
|
|
916
|
+
"--format=%H%x00%an%x00%ae%x00%s",
|
|
917
|
+
"HEAD"
|
|
918
|
+
]);
|
|
919
|
+
} catch {
|
|
920
|
+
return null;
|
|
921
|
+
}
|
|
922
|
+
for (const commit of this.parseGitLog(fullLog)) {
|
|
923
|
+
if (isGenerationBoundary(commit)) {
|
|
924
|
+
return commit.sha;
|
|
925
|
+
}
|
|
926
|
+
}
|
|
911
927
|
return null;
|
|
912
928
|
}
|
|
913
929
|
async detectNewPatches() {
|
|
@@ -919,6 +935,11 @@ var ReplayDetector = class {
|
|
|
919
935
|
}
|
|
920
936
|
const lastGen = this.getLastGeneration(lock);
|
|
921
937
|
if (!lastGen) {
|
|
938
|
+
if (lock.current_generation) {
|
|
939
|
+
this.warnings.push(
|
|
940
|
+
`No generation boundary found in git history and the lockfile's recorded generation (${lock.current_generation.slice(0, 7)}) matches no entry in its generation records. Customer customizations may differ from the last known generation and could be lost on the next regeneration. Run \`fern-replay bootstrap --force\` to re-initialize tracking.`
|
|
941
|
+
);
|
|
942
|
+
}
|
|
922
943
|
return { patches: [], revertedPatchIds: [] };
|
|
923
944
|
}
|
|
924
945
|
const exists = await this.git.commitExists(lastGen.commit_sha);
|
|
@@ -1673,16 +1694,20 @@ async function resolveInputs(args) {
|
|
|
1673
1694
|
const oursPath = join2(outputDir, resolvedPath);
|
|
1674
1695
|
const ours = await readFile(oursPath, "utf-8").catch(() => null);
|
|
1675
1696
|
let ghostTheirs = null;
|
|
1676
|
-
|
|
1697
|
+
let baseTreeUnreachable = false;
|
|
1698
|
+
if (!base && !renameSourcePath) {
|
|
1677
1699
|
const treeReachable = await isTreeReachable(baseGen.tree_hash);
|
|
1678
1700
|
if (!treeReachable) {
|
|
1679
|
-
|
|
1680
|
-
if (
|
|
1681
|
-
const
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1701
|
+
baseTreeUnreachable = true;
|
|
1702
|
+
if (ours) {
|
|
1703
|
+
const fileDiff = extractFileDiff2(patch.patch_content, filePath);
|
|
1704
|
+
if (fileDiff) {
|
|
1705
|
+
const { reconstructFromGhostPatch: reconstructFromGhostPatch2 } = await Promise.resolve().then(() => (init_HybridReconstruction(), HybridReconstruction_exports));
|
|
1706
|
+
const result = reconstructFromGhostPatch2(fileDiff, ours);
|
|
1707
|
+
if (result) {
|
|
1708
|
+
base = result.base;
|
|
1709
|
+
ghostTheirs = result.theirs;
|
|
1710
|
+
}
|
|
1686
1711
|
}
|
|
1687
1712
|
}
|
|
1688
1713
|
}
|
|
@@ -1694,7 +1719,11 @@ async function resolveInputs(args) {
|
|
|
1694
1719
|
ours,
|
|
1695
1720
|
oursPath,
|
|
1696
1721
|
renameSourcePath,
|
|
1697
|
-
ghostTheirs
|
|
1722
|
+
ghostTheirs,
|
|
1723
|
+
// Only meaningful when ghost reconstruction did NOT recover a base; if it
|
|
1724
|
+
// did, `base` is non-null and the merge proceeds normally. We still set
|
|
1725
|
+
// the flag truthfully (the tree IS unreachable) so Phase 3 can decide.
|
|
1726
|
+
baseTreeUnreachable
|
|
1698
1727
|
};
|
|
1699
1728
|
}
|
|
1700
1729
|
|
|
@@ -1877,7 +1906,11 @@ async function buildMergePlan(args) {
|
|
|
1877
1906
|
return { kind: "new-file-only-user", theirs: effective_theirs };
|
|
1878
1907
|
}
|
|
1879
1908
|
if (base == null && ours != null && effective_theirs != null && !useAccumulatorAsMergeBase) {
|
|
1880
|
-
return {
|
|
1909
|
+
return {
|
|
1910
|
+
kind: "new-file-both",
|
|
1911
|
+
theirs: effective_theirs,
|
|
1912
|
+
...inputs.baseTreeUnreachable ? { baseTreeUnreachable: true } : {}
|
|
1913
|
+
};
|
|
1881
1914
|
}
|
|
1882
1915
|
if (effective_theirs == null) {
|
|
1883
1916
|
return { kind: "skipped", reason: "missing-content" };
|
|
@@ -1925,17 +1958,19 @@ async function runMergeAndRecord(args) {
|
|
|
1925
1958
|
const merged2 = threeWayMerge("", ours, plan.theirs);
|
|
1926
1959
|
await mkdir(dirname2(oursPath), { recursive: true });
|
|
1927
1960
|
await writeFile(oursPath, merged2.content);
|
|
1961
|
+
const unreachableFlag = plan.baseTreeUnreachable ? { baseTreeUnreachable: true } : {};
|
|
1928
1962
|
if (merged2.hasConflicts) {
|
|
1929
1963
|
return {
|
|
1930
1964
|
file: resolvedPath,
|
|
1931
1965
|
status: "conflict",
|
|
1932
1966
|
conflicts: merged2.conflicts,
|
|
1933
1967
|
conflictReason: "new-file-both",
|
|
1934
|
-
conflictMetadata: metadata
|
|
1968
|
+
conflictMetadata: metadata,
|
|
1969
|
+
...unreachableFlag
|
|
1935
1970
|
};
|
|
1936
1971
|
}
|
|
1937
1972
|
accumulator.recordMerge(resolvedPath, merged2.content, patch.base_generation);
|
|
1938
|
-
return { file: resolvedPath, status: "merged" };
|
|
1973
|
+
return { file: resolvedPath, status: "merged", ...unreachableFlag };
|
|
1939
1974
|
}
|
|
1940
1975
|
const merged = threeWayMerge(plan.mergeBase, ours, plan.theirs);
|
|
1941
1976
|
await mkdir(dirname2(oursPath), { recursive: true });
|
|
@@ -3151,6 +3186,7 @@ var NoPatchesFlow = class {
|
|
|
3151
3186
|
results = await this.service.applicator.applyPatches(newPatches);
|
|
3152
3187
|
this.service._lastApplyResults = results;
|
|
3153
3188
|
this.service.recordDroppedContextLineWarnings(results);
|
|
3189
|
+
this.service.recordUnreachableBaseWarnings(results);
|
|
3154
3190
|
this.service.revertConflictingFiles(results);
|
|
3155
3191
|
for (const result of results) {
|
|
3156
3192
|
if (result.status === "conflict") {
|
|
@@ -3306,6 +3342,7 @@ var NormalRegenerationFlow = class {
|
|
|
3306
3342
|
const results = await this.service.applicator.applyPatches(allPatches);
|
|
3307
3343
|
this.service._lastApplyResults = results;
|
|
3308
3344
|
this.service.recordDroppedContextLineWarnings(results);
|
|
3345
|
+
this.service.recordUnreachableBaseWarnings(results);
|
|
3309
3346
|
this.service.revertConflictingFiles(results);
|
|
3310
3347
|
for (const result of results) {
|
|
3311
3348
|
if (result.status === "conflict") {
|
|
@@ -4225,6 +4262,37 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
|
|
|
4225
4262
|
}
|
|
4226
4263
|
}
|
|
4227
4264
|
}
|
|
4265
|
+
/**
|
|
4266
|
+
* After applyPatches(), surface a warning for each (patch × file) where a
|
|
4267
|
+
* customization's BASE could not be read because the patch's
|
|
4268
|
+
* base-generation tree is unreachable in this clone (shallow-clone window
|
|
4269
|
+
* too small / pruned history). In that state the merge pipeline falls
|
|
4270
|
+
* through to `new-file-both` and the customer's edit can be dropped with NO
|
|
4271
|
+
* conflict and NO error — the INC-866 silent-loss class. This converts that
|
|
4272
|
+
* silence into an explicit, actionable warning naming the file and the
|
|
4273
|
+
* likely cause (insufficient clone window).
|
|
4274
|
+
*
|
|
4275
|
+
* Mirrors `recordDroppedContextLineWarnings`'s "surface, never silently
|
|
4276
|
+
* drop" contract. De-duplicated per file path so a single insufficient
|
|
4277
|
+
* window doesn't spam one warning per touched file repeatedly.
|
|
4278
|
+
*/
|
|
4279
|
+
/** @internal Used by Flow implementations in `src/flows/`. */
|
|
4280
|
+
recordUnreachableBaseWarnings(results) {
|
|
4281
|
+
const warned = /* @__PURE__ */ new Set();
|
|
4282
|
+
for (const result of results) {
|
|
4283
|
+
if (!result.fileResults) continue;
|
|
4284
|
+
for (const fr of result.fileResults) {
|
|
4285
|
+
if (!fr.baseTreeUnreachable) continue;
|
|
4286
|
+
if (warned.has(fr.file)) continue;
|
|
4287
|
+
warned.add(fr.file);
|
|
4288
|
+
const base = fr.conflictMetadata?.baseGeneration;
|
|
4289
|
+
const baseRef = base ? ` (base generation ${base.slice(0, 7)})` : "";
|
|
4290
|
+
this.warnings.push(
|
|
4291
|
+
`${fr.file}: could not read the prior generation's version of this file${baseRef} because that generation's tree is outside the clone window (insufficient shallow-clone depth or pruned history). The customization was treated as a new file, which can drop your edits silently. Re-run with the prior generation in the clone window \u2014 e.g. \`git fetch --shallow-since=<date just before the prior [fern-generated] commit>\` or \`git fetch --unshallow\` \u2014 then re-run replay.`
|
|
4292
|
+
);
|
|
4293
|
+
}
|
|
4294
|
+
}
|
|
4295
|
+
}
|
|
4228
4296
|
/**
|
|
4229
4297
|
* After applyPatches(), strip conflict markers from conflicting files
|
|
4230
4298
|
* so only clean content is committed. Keeps the Generated (OURS) side.
|