@fern-api/replay 0.19.0 → 0.19.2
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 +132 -7
- package/dist/cli.cjs.map +1 -1
- package/dist/index.cjs +132 -7
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +40 -0
- package/dist/index.d.ts +40 -0
- package/dist/index.js +132 -7
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.cts
CHANGED
|
@@ -884,6 +884,31 @@ declare class ReplayService {
|
|
|
884
884
|
*/
|
|
885
885
|
getLastResults(): ReadonlyArray<ReplayResult>;
|
|
886
886
|
constructor(outputDir: string, _config: ReplayConfig);
|
|
887
|
+
/**
|
|
888
|
+
* Resolve the previous-generation SHA to export on
|
|
889
|
+
* `ReplayPreparation.previousGenerationSha`.
|
|
890
|
+
*
|
|
891
|
+
* `current_generation` is the SHA of a LOCALLY-created `[fern-generated]` commit.
|
|
892
|
+
* generator-cli's signed-commit push (`pushSignedCommit`) recreates that commit under a
|
|
893
|
+
* new remote SHA, and a squash-merge collapses it away, so the recorded value can be
|
|
894
|
+
* unreachable in a later clone (org "Natural": autoVersion then diffed against a phantom
|
|
895
|
+
* baseline and silently reported NO_CHANGE — or, pre-a38bbc6, shipped
|
|
896
|
+
* `0.0.0-fern-placeholder`). Consumers (generator-cli's autoVersion) diff against this
|
|
897
|
+
* baseline, so it MUST resolve to a real commit.
|
|
898
|
+
*
|
|
899
|
+
* Prefer the recorded SHA while it still resolves — the healthy case, and the pristine
|
|
900
|
+
* prior `[fern-generated]` tree. Otherwise re-anchor on the most recent reachable
|
|
901
|
+
* generation boundary in first-parent history: the SAME value the detector already
|
|
902
|
+
* derives for its own scan boundary (`findPreviousGenerationFromHistory`, whose
|
|
903
|
+
* `isGenerationBoundary` matches squash-merge subjects like `SDK Generation (#N)`). Fall
|
|
904
|
+
* back to the recorded SHA only when no boundary is reachable at all — no worse than the
|
|
905
|
+
* prior behavior. ADR 0002 (generator-cli) allows replay to expose this reconciled value;
|
|
906
|
+
* the consumer stays independently resilient.
|
|
907
|
+
*
|
|
908
|
+
* MUST be called before the new `[fern-generated]` commit is created, while HEAD is still
|
|
909
|
+
* the pre-generation (customer) state, so the walk anchors on the PREVIOUS generation.
|
|
910
|
+
*/
|
|
911
|
+
resolveReachablePreviousGeneration(recordedSha: string | null): Promise<string | null>;
|
|
887
912
|
/**
|
|
888
913
|
* Phase 1 of the two-phase replay flow. Runs preGenerationRebase (when applicable),
|
|
889
914
|
* detects new patches, and creates the `[fern-generated]` commit. Leaves HEAD at
|
|
@@ -970,6 +995,21 @@ declare class ReplayService {
|
|
|
970
995
|
* never be used during apply anyway).
|
|
971
996
|
*/
|
|
972
997
|
private readSnapshotFromDisk;
|
|
998
|
+
/**
|
|
999
|
+
* Autoversion-aware replacement for the naive `git diff currentGen -- files`
|
|
1000
|
+
* + on-disk snapshot used when rebasing a cleanly-applied patch.
|
|
1001
|
+
*
|
|
1002
|
+
* The generation tree (BASE) still carries the placeholder on version lines
|
|
1003
|
+
* (autoversion runs after `[fern-generated]`), but the working tree holds
|
|
1004
|
+
* autoversion's resolved semver. A raw diff/snapshot would fold the
|
|
1005
|
+
* placeholder->semver swap into the customer's patch_content/theirs_snapshot,
|
|
1006
|
+
* pinning a stale version. This re-derives both against a version-neutralized
|
|
1007
|
+
* THEIRS so the pipeline-owned version line never enters the patch.
|
|
1008
|
+
*
|
|
1009
|
+
* Returns null when no file is under autoversion (fixed `--version`, i.e. no
|
|
1010
|
+
* placeholder in the generation tree) — callers keep their existing behavior.
|
|
1011
|
+
*/
|
|
1012
|
+
private computeAutoversionNeutralizedRebase;
|
|
973
1013
|
/**
|
|
974
1014
|
* Detects autoversion contamination — returns true when a
|
|
975
1015
|
* `[fern-autoversion]` commit between `fromSha` and `toSha` touched any
|
package/dist/index.d.ts
CHANGED
|
@@ -884,6 +884,31 @@ declare class ReplayService {
|
|
|
884
884
|
*/
|
|
885
885
|
getLastResults(): ReadonlyArray<ReplayResult>;
|
|
886
886
|
constructor(outputDir: string, _config: ReplayConfig);
|
|
887
|
+
/**
|
|
888
|
+
* Resolve the previous-generation SHA to export on
|
|
889
|
+
* `ReplayPreparation.previousGenerationSha`.
|
|
890
|
+
*
|
|
891
|
+
* `current_generation` is the SHA of a LOCALLY-created `[fern-generated]` commit.
|
|
892
|
+
* generator-cli's signed-commit push (`pushSignedCommit`) recreates that commit under a
|
|
893
|
+
* new remote SHA, and a squash-merge collapses it away, so the recorded value can be
|
|
894
|
+
* unreachable in a later clone (org "Natural": autoVersion then diffed against a phantom
|
|
895
|
+
* baseline and silently reported NO_CHANGE — or, pre-a38bbc6, shipped
|
|
896
|
+
* `0.0.0-fern-placeholder`). Consumers (generator-cli's autoVersion) diff against this
|
|
897
|
+
* baseline, so it MUST resolve to a real commit.
|
|
898
|
+
*
|
|
899
|
+
* Prefer the recorded SHA while it still resolves — the healthy case, and the pristine
|
|
900
|
+
* prior `[fern-generated]` tree. Otherwise re-anchor on the most recent reachable
|
|
901
|
+
* generation boundary in first-parent history: the SAME value the detector already
|
|
902
|
+
* derives for its own scan boundary (`findPreviousGenerationFromHistory`, whose
|
|
903
|
+
* `isGenerationBoundary` matches squash-merge subjects like `SDK Generation (#N)`). Fall
|
|
904
|
+
* back to the recorded SHA only when no boundary is reachable at all — no worse than the
|
|
905
|
+
* prior behavior. ADR 0002 (generator-cli) allows replay to expose this reconciled value;
|
|
906
|
+
* the consumer stays independently resilient.
|
|
907
|
+
*
|
|
908
|
+
* MUST be called before the new `[fern-generated]` commit is created, while HEAD is still
|
|
909
|
+
* the pre-generation (customer) state, so the walk anchors on the PREVIOUS generation.
|
|
910
|
+
*/
|
|
911
|
+
resolveReachablePreviousGeneration(recordedSha: string | null): Promise<string | null>;
|
|
887
912
|
/**
|
|
888
913
|
* Phase 1 of the two-phase replay flow. Runs preGenerationRebase (when applicable),
|
|
889
914
|
* detects new patches, and creates the `[fern-generated]` commit. Leaves HEAD at
|
|
@@ -970,6 +995,21 @@ declare class ReplayService {
|
|
|
970
995
|
* never be used during apply anyway).
|
|
971
996
|
*/
|
|
972
997
|
private readSnapshotFromDisk;
|
|
998
|
+
/**
|
|
999
|
+
* Autoversion-aware replacement for the naive `git diff currentGen -- files`
|
|
1000
|
+
* + on-disk snapshot used when rebasing a cleanly-applied patch.
|
|
1001
|
+
*
|
|
1002
|
+
* The generation tree (BASE) still carries the placeholder on version lines
|
|
1003
|
+
* (autoversion runs after `[fern-generated]`), but the working tree holds
|
|
1004
|
+
* autoversion's resolved semver. A raw diff/snapshot would fold the
|
|
1005
|
+
* placeholder->semver swap into the customer's patch_content/theirs_snapshot,
|
|
1006
|
+
* pinning a stale version. This re-derives both against a version-neutralized
|
|
1007
|
+
* THEIRS so the pipeline-owned version line never enters the patch.
|
|
1008
|
+
*
|
|
1009
|
+
* Returns null when no file is under autoversion (fixed `--version`, i.e. no
|
|
1010
|
+
* placeholder in the generation tree) — callers keep their existing behavior.
|
|
1011
|
+
*/
|
|
1012
|
+
private computeAutoversionNeutralizedRebase;
|
|
973
1013
|
/**
|
|
974
1014
|
* Detects autoversion contamination — returns true when a
|
|
975
1015
|
* `[fern-autoversion]` commit between `fromSha` and `toSha` touched any
|
package/dist/index.js
CHANGED
|
@@ -2027,6 +2027,51 @@ async function buildMergePlan(args) {
|
|
|
2027
2027
|
// src/applicator/runMergeAndRecord.ts
|
|
2028
2028
|
import { mkdir, writeFile } from "fs/promises";
|
|
2029
2029
|
import { dirname as dirname2 } from "path";
|
|
2030
|
+
|
|
2031
|
+
// src/autoversion.ts
|
|
2032
|
+
var AUTOVERSION_PLACEHOLDERS = [
|
|
2033
|
+
"v0.0.0-fern-placeholder",
|
|
2034
|
+
// Go -- a superstring of the default; check first
|
|
2035
|
+
"0.0.0-fern-placeholder",
|
|
2036
|
+
// TS/Java/C#/Ruby/PHP/Rust/default
|
|
2037
|
+
"0.0.0.dev0"
|
|
2038
|
+
// Python (PEP 440 -- Poetry rejects hyphens)
|
|
2039
|
+
];
|
|
2040
|
+
var VERSION_TOKEN = /(?:v)?\d+\.\d+\.\d+[0-9A-Za-z.+-]*/g;
|
|
2041
|
+
var VERSION_MASK = "@@FERN_VERSION@@";
|
|
2042
|
+
function maskVersions(line) {
|
|
2043
|
+
return line.replace(VERSION_TOKEN, VERSION_MASK);
|
|
2044
|
+
}
|
|
2045
|
+
function containsPlaceholder(content) {
|
|
2046
|
+
if (!content) return false;
|
|
2047
|
+
return AUTOVERSION_PLACEHOLDERS.some((p) => content.includes(p));
|
|
2048
|
+
}
|
|
2049
|
+
function remapManagedLines(genBase, target, theirs) {
|
|
2050
|
+
if (!containsPlaceholder(genBase)) return theirs;
|
|
2051
|
+
const managedMasks = /* @__PURE__ */ new Set();
|
|
2052
|
+
for (const line of genBase.split("\n")) {
|
|
2053
|
+
if (AUTOVERSION_PLACEHOLDERS.some((p) => line.includes(p))) managedMasks.add(maskVersions(line));
|
|
2054
|
+
}
|
|
2055
|
+
if (managedMasks.size === 0) return theirs;
|
|
2056
|
+
const targetByMask = /* @__PURE__ */ new Map();
|
|
2057
|
+
for (const line of target.split("\n")) {
|
|
2058
|
+
const mask = maskVersions(line);
|
|
2059
|
+
if (managedMasks.has(mask) && !targetByMask.has(mask)) targetByMask.set(mask, line);
|
|
2060
|
+
}
|
|
2061
|
+
return theirs.split("\n").map((line) => {
|
|
2062
|
+
const mask = maskVersions(line);
|
|
2063
|
+
const replacement = managedMasks.has(mask) ? targetByMask.get(mask) : void 0;
|
|
2064
|
+
return replacement !== void 0 && replacement !== line ? replacement : line;
|
|
2065
|
+
}).join("\n");
|
|
2066
|
+
}
|
|
2067
|
+
function neutralizeAutoversionTheirs(genBase, theirs) {
|
|
2068
|
+
return remapManagedLines(genBase, genBase, theirs);
|
|
2069
|
+
}
|
|
2070
|
+
function alignAutoversionToOurs(genBase, ours, theirs) {
|
|
2071
|
+
return remapManagedLines(genBase, ours, theirs);
|
|
2072
|
+
}
|
|
2073
|
+
|
|
2074
|
+
// src/applicator/runMergeAndRecord.ts
|
|
2030
2075
|
async function runMergeAndRecord(args) {
|
|
2031
2076
|
const { plan, inputs, patch, accumulator } = args;
|
|
2032
2077
|
const { resolvedPath, metadata, oursPath, ours } = inputs;
|
|
@@ -2057,11 +2102,13 @@ async function runMergeAndRecord(args) {
|
|
|
2057
2102
|
accumulator.recordMerge(resolvedPath, merged2.content, patch.base_generation);
|
|
2058
2103
|
return { file: resolvedPath, status: "merged", ...unreachableFlag };
|
|
2059
2104
|
}
|
|
2060
|
-
const
|
|
2105
|
+
const mergeBase = inputs.base != null ? alignAutoversionToOurs(inputs.base, ours, plan.mergeBase) : plan.mergeBase;
|
|
2106
|
+
const theirs = inputs.base != null ? alignAutoversionToOurs(inputs.base, ours, plan.theirs) : plan.theirs;
|
|
2107
|
+
const merged = threeWayMerge(mergeBase, ours, theirs);
|
|
2061
2108
|
await mkdir(dirname2(oursPath), { recursive: true });
|
|
2062
2109
|
await writeFile(oursPath, merged.content);
|
|
2063
2110
|
if (merged.hasConflicts) {
|
|
2064
|
-
accumulator.recordConflict(resolvedPath,
|
|
2111
|
+
accumulator.recordConflict(resolvedPath, theirs, patch.base_generation);
|
|
2065
2112
|
return {
|
|
2066
2113
|
file: resolvedPath,
|
|
2067
2114
|
status: "conflict",
|
|
@@ -2070,7 +2117,7 @@ async function runMergeAndRecord(args) {
|
|
|
2070
2117
|
conflictMetadata: metadata
|
|
2071
2118
|
};
|
|
2072
2119
|
}
|
|
2073
|
-
accumulator.recordMerge(resolvedPath,
|
|
2120
|
+
accumulator.recordMerge(resolvedPath, theirs, patch.base_generation);
|
|
2074
2121
|
return {
|
|
2075
2122
|
file: resolvedPath,
|
|
2076
2123
|
status: "merged",
|
|
@@ -3198,7 +3245,9 @@ var NoPatchesFlow = class {
|
|
|
3198
3245
|
}
|
|
3199
3246
|
async prepare(options) {
|
|
3200
3247
|
const preLock = this.service.lockManager.read();
|
|
3201
|
-
const previousGenerationSha =
|
|
3248
|
+
const previousGenerationSha = await this.service.resolveReachablePreviousGeneration(
|
|
3249
|
+
preLock.current_generation ?? null
|
|
3250
|
+
);
|
|
3202
3251
|
let { patches: newPatches, revertedPatchIds } = await this.service.detector.detectNewPatches();
|
|
3203
3252
|
const detectorWarnings = [...this.service.detector.warnings];
|
|
3204
3253
|
const detectorDegraded = [...this.service.detector.degradedReasons];
|
|
@@ -3336,7 +3385,9 @@ var NormalRegenerationFlow = class {
|
|
|
3336
3385
|
}
|
|
3337
3386
|
async prepare(options) {
|
|
3338
3387
|
const preLock = this.service.lockManager.read();
|
|
3339
|
-
const previousGenerationSha =
|
|
3388
|
+
const previousGenerationSha = await this.service.resolveReachablePreviousGeneration(
|
|
3389
|
+
preLock.current_generation ?? null
|
|
3390
|
+
);
|
|
3340
3391
|
if (options?.dryRun) {
|
|
3341
3392
|
const existingPatches2 = this.service.lockManager.getPatches();
|
|
3342
3393
|
const { patches: newPatches2, revertedPatchIds: dryRunReverted } = await this.service.detector.detectNewPatches();
|
|
@@ -3574,6 +3625,37 @@ var ReplayService = class {
|
|
|
3574
3625
|
this.committer = new ReplayCommitter(git, outputDir);
|
|
3575
3626
|
this.fileOwnership = new FileOwnership(git);
|
|
3576
3627
|
}
|
|
3628
|
+
/**
|
|
3629
|
+
* Resolve the previous-generation SHA to export on
|
|
3630
|
+
* `ReplayPreparation.previousGenerationSha`.
|
|
3631
|
+
*
|
|
3632
|
+
* `current_generation` is the SHA of a LOCALLY-created `[fern-generated]` commit.
|
|
3633
|
+
* generator-cli's signed-commit push (`pushSignedCommit`) recreates that commit under a
|
|
3634
|
+
* new remote SHA, and a squash-merge collapses it away, so the recorded value can be
|
|
3635
|
+
* unreachable in a later clone (org "Natural": autoVersion then diffed against a phantom
|
|
3636
|
+
* baseline and silently reported NO_CHANGE — or, pre-a38bbc6, shipped
|
|
3637
|
+
* `0.0.0-fern-placeholder`). Consumers (generator-cli's autoVersion) diff against this
|
|
3638
|
+
* baseline, so it MUST resolve to a real commit.
|
|
3639
|
+
*
|
|
3640
|
+
* Prefer the recorded SHA while it still resolves — the healthy case, and the pristine
|
|
3641
|
+
* prior `[fern-generated]` tree. Otherwise re-anchor on the most recent reachable
|
|
3642
|
+
* generation boundary in first-parent history: the SAME value the detector already
|
|
3643
|
+
* derives for its own scan boundary (`findPreviousGenerationFromHistory`, whose
|
|
3644
|
+
* `isGenerationBoundary` matches squash-merge subjects like `SDK Generation (#N)`). Fall
|
|
3645
|
+
* back to the recorded SHA only when no boundary is reachable at all — no worse than the
|
|
3646
|
+
* prior behavior. ADR 0002 (generator-cli) allows replay to expose this reconciled value;
|
|
3647
|
+
* the consumer stays independently resilient.
|
|
3648
|
+
*
|
|
3649
|
+
* MUST be called before the new `[fern-generated]` commit is created, while HEAD is still
|
|
3650
|
+
* the pre-generation (customer) state, so the walk anchors on the PREVIOUS generation.
|
|
3651
|
+
*/
|
|
3652
|
+
async resolveReachablePreviousGeneration(recordedSha) {
|
|
3653
|
+
if (recordedSha != null && await this.git.commitExists(recordedSha)) {
|
|
3654
|
+
return recordedSha;
|
|
3655
|
+
}
|
|
3656
|
+
const derived = await this.detector.findPreviousGenerationFromHistory();
|
|
3657
|
+
return derived ?? recordedSha;
|
|
3658
|
+
}
|
|
3577
3659
|
/**
|
|
3578
3660
|
* Phase 1 of the two-phase replay flow. Runs preGenerationRebase (when applicable),
|
|
3579
3661
|
* detects new patches, and creates the `[fern-generated]` commit. Leaves HEAD at
|
|
@@ -3856,7 +3938,9 @@ var ReplayService = class {
|
|
|
3856
3938
|
if (baseChanged) repointed++;
|
|
3857
3939
|
continue;
|
|
3858
3940
|
}
|
|
3859
|
-
const
|
|
3941
|
+
const rawDiff = await this.git.exec(["diff", currentGenSha, "--", ...patch.files]).catch(() => null);
|
|
3942
|
+
const neutralizedRebase = rawDiff != null && containsPlaceholder(rawDiff) ? await this.computeAutoversionNeutralizedRebase(patch.files, currentGenSha) : null;
|
|
3943
|
+
const diff = neutralizedRebase ? neutralizedRebase.diff : rawDiff;
|
|
3860
3944
|
if (diff === null) continue;
|
|
3861
3945
|
if (!diff.trim()) {
|
|
3862
3946
|
if (hasProtectedFiles) {
|
|
@@ -3912,7 +3996,7 @@ var ReplayService = class {
|
|
|
3912
3996
|
patch.base_generation = currentGenSha;
|
|
3913
3997
|
patch.patch_content = diff;
|
|
3914
3998
|
patch.content_hash = newContentHash;
|
|
3915
|
-
const snapshot = await this.readSnapshotFromDisk(patch.files);
|
|
3999
|
+
const snapshot = neutralizedRebase ? neutralizedRebase.snapshot : await this.readSnapshotFromDisk(patch.files);
|
|
3916
4000
|
const snapshotIsNonEmpty = Object.keys(snapshot).length > 0;
|
|
3917
4001
|
if (snapshotIsNonEmpty) {
|
|
3918
4002
|
patch.theirs_snapshot = snapshot;
|
|
@@ -3952,6 +4036,47 @@ var ReplayService = class {
|
|
|
3952
4036
|
}
|
|
3953
4037
|
return snapshot;
|
|
3954
4038
|
}
|
|
4039
|
+
/**
|
|
4040
|
+
* Autoversion-aware replacement for the naive `git diff currentGen -- files`
|
|
4041
|
+
* + on-disk snapshot used when rebasing a cleanly-applied patch.
|
|
4042
|
+
*
|
|
4043
|
+
* The generation tree (BASE) still carries the placeholder on version lines
|
|
4044
|
+
* (autoversion runs after `[fern-generated]`), but the working tree holds
|
|
4045
|
+
* autoversion's resolved semver. A raw diff/snapshot would fold the
|
|
4046
|
+
* placeholder->semver swap into the customer's patch_content/theirs_snapshot,
|
|
4047
|
+
* pinning a stale version. This re-derives both against a version-neutralized
|
|
4048
|
+
* THEIRS so the pipeline-owned version line never enters the patch.
|
|
4049
|
+
*
|
|
4050
|
+
* Returns null when no file is under autoversion (fixed `--version`, i.e. no
|
|
4051
|
+
* placeholder in the generation tree) — callers keep their existing behavior.
|
|
4052
|
+
*/
|
|
4053
|
+
async computeAutoversionNeutralizedRebase(files, currentGenSha) {
|
|
4054
|
+
const genByFile = /* @__PURE__ */ new Map();
|
|
4055
|
+
let anyPlaceholder = false;
|
|
4056
|
+
for (const file of files) {
|
|
4057
|
+
const gen = await this.git.showFile(currentGenSha, file).catch(() => null);
|
|
4058
|
+
genByFile.set(file, gen);
|
|
4059
|
+
if (containsPlaceholder(gen)) anyPlaceholder = true;
|
|
4060
|
+
}
|
|
4061
|
+
if (!anyPlaceholder) return null;
|
|
4062
|
+
const snapshot = {};
|
|
4063
|
+
const fragments = [];
|
|
4064
|
+
for (const file of files) {
|
|
4065
|
+
if (isBinaryFile(file)) continue;
|
|
4066
|
+
const disk = await readFileAsync(join7(this.outputDir, file), "utf-8").catch(() => null);
|
|
4067
|
+
if (disk == null) continue;
|
|
4068
|
+
const gen = genByFile.get(file) ?? null;
|
|
4069
|
+
const neutralized = gen != null ? neutralizeAutoversionTheirs(gen, disk) : disk;
|
|
4070
|
+
snapshot[file] = neutralized;
|
|
4071
|
+
if (gen == null) {
|
|
4072
|
+
fragments.push(synthesizeNewFileDiff(file, neutralized));
|
|
4073
|
+
} else if (gen !== neutralized) {
|
|
4074
|
+
const fileDiff = await unifiedDiff(gen, neutralized, file);
|
|
4075
|
+
if (fileDiff.trim()) fragments.push(fileDiff);
|
|
4076
|
+
}
|
|
4077
|
+
}
|
|
4078
|
+
return { diff: fragments.join(""), snapshot };
|
|
4079
|
+
}
|
|
3955
4080
|
/**
|
|
3956
4081
|
* Detects autoversion contamination — returns true when a
|
|
3957
4082
|
* `[fern-autoversion]` commit between `fromSha` and `toSha` touched any
|