@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.cjs
CHANGED
|
@@ -2082,6 +2082,51 @@ async function buildMergePlan(args) {
|
|
|
2082
2082
|
// src/applicator/runMergeAndRecord.ts
|
|
2083
2083
|
var import_promises2 = require("fs/promises");
|
|
2084
2084
|
var import_node_path4 = require("path");
|
|
2085
|
+
|
|
2086
|
+
// src/autoversion.ts
|
|
2087
|
+
var AUTOVERSION_PLACEHOLDERS = [
|
|
2088
|
+
"v0.0.0-fern-placeholder",
|
|
2089
|
+
// Go -- a superstring of the default; check first
|
|
2090
|
+
"0.0.0-fern-placeholder",
|
|
2091
|
+
// TS/Java/C#/Ruby/PHP/Rust/default
|
|
2092
|
+
"0.0.0.dev0"
|
|
2093
|
+
// Python (PEP 440 -- Poetry rejects hyphens)
|
|
2094
|
+
];
|
|
2095
|
+
var VERSION_TOKEN = /(?:v)?\d+\.\d+\.\d+[0-9A-Za-z.+-]*/g;
|
|
2096
|
+
var VERSION_MASK = "@@FERN_VERSION@@";
|
|
2097
|
+
function maskVersions(line) {
|
|
2098
|
+
return line.replace(VERSION_TOKEN, VERSION_MASK);
|
|
2099
|
+
}
|
|
2100
|
+
function containsPlaceholder(content) {
|
|
2101
|
+
if (!content) return false;
|
|
2102
|
+
return AUTOVERSION_PLACEHOLDERS.some((p) => content.includes(p));
|
|
2103
|
+
}
|
|
2104
|
+
function remapManagedLines(genBase, target, theirs) {
|
|
2105
|
+
if (!containsPlaceholder(genBase)) return theirs;
|
|
2106
|
+
const managedMasks = /* @__PURE__ */ new Set();
|
|
2107
|
+
for (const line of genBase.split("\n")) {
|
|
2108
|
+
if (AUTOVERSION_PLACEHOLDERS.some((p) => line.includes(p))) managedMasks.add(maskVersions(line));
|
|
2109
|
+
}
|
|
2110
|
+
if (managedMasks.size === 0) return theirs;
|
|
2111
|
+
const targetByMask = /* @__PURE__ */ new Map();
|
|
2112
|
+
for (const line of target.split("\n")) {
|
|
2113
|
+
const mask = maskVersions(line);
|
|
2114
|
+
if (managedMasks.has(mask) && !targetByMask.has(mask)) targetByMask.set(mask, line);
|
|
2115
|
+
}
|
|
2116
|
+
return theirs.split("\n").map((line) => {
|
|
2117
|
+
const mask = maskVersions(line);
|
|
2118
|
+
const replacement = managedMasks.has(mask) ? targetByMask.get(mask) : void 0;
|
|
2119
|
+
return replacement !== void 0 && replacement !== line ? replacement : line;
|
|
2120
|
+
}).join("\n");
|
|
2121
|
+
}
|
|
2122
|
+
function neutralizeAutoversionTheirs(genBase, theirs) {
|
|
2123
|
+
return remapManagedLines(genBase, genBase, theirs);
|
|
2124
|
+
}
|
|
2125
|
+
function alignAutoversionToOurs(genBase, ours, theirs) {
|
|
2126
|
+
return remapManagedLines(genBase, ours, theirs);
|
|
2127
|
+
}
|
|
2128
|
+
|
|
2129
|
+
// src/applicator/runMergeAndRecord.ts
|
|
2085
2130
|
async function runMergeAndRecord(args) {
|
|
2086
2131
|
const { plan, inputs, patch, accumulator } = args;
|
|
2087
2132
|
const { resolvedPath, metadata, oursPath, ours } = inputs;
|
|
@@ -2112,11 +2157,13 @@ async function runMergeAndRecord(args) {
|
|
|
2112
2157
|
accumulator.recordMerge(resolvedPath, merged2.content, patch.base_generation);
|
|
2113
2158
|
return { file: resolvedPath, status: "merged", ...unreachableFlag };
|
|
2114
2159
|
}
|
|
2115
|
-
const
|
|
2160
|
+
const mergeBase = inputs.base != null ? alignAutoversionToOurs(inputs.base, ours, plan.mergeBase) : plan.mergeBase;
|
|
2161
|
+
const theirs = inputs.base != null ? alignAutoversionToOurs(inputs.base, ours, plan.theirs) : plan.theirs;
|
|
2162
|
+
const merged = threeWayMerge(mergeBase, ours, theirs);
|
|
2116
2163
|
await (0, import_promises2.mkdir)((0, import_node_path4.dirname)(oursPath), { recursive: true });
|
|
2117
2164
|
await (0, import_promises2.writeFile)(oursPath, merged.content);
|
|
2118
2165
|
if (merged.hasConflicts) {
|
|
2119
|
-
accumulator.recordConflict(resolvedPath,
|
|
2166
|
+
accumulator.recordConflict(resolvedPath, theirs, patch.base_generation);
|
|
2120
2167
|
return {
|
|
2121
2168
|
file: resolvedPath,
|
|
2122
2169
|
status: "conflict",
|
|
@@ -2125,7 +2172,7 @@ async function runMergeAndRecord(args) {
|
|
|
2125
2172
|
conflictMetadata: metadata
|
|
2126
2173
|
};
|
|
2127
2174
|
}
|
|
2128
|
-
accumulator.recordMerge(resolvedPath,
|
|
2175
|
+
accumulator.recordMerge(resolvedPath, theirs, patch.base_generation);
|
|
2129
2176
|
return {
|
|
2130
2177
|
file: resolvedPath,
|
|
2131
2178
|
status: "merged",
|
|
@@ -3253,7 +3300,9 @@ var NoPatchesFlow = class {
|
|
|
3253
3300
|
}
|
|
3254
3301
|
async prepare(options) {
|
|
3255
3302
|
const preLock = this.service.lockManager.read();
|
|
3256
|
-
const previousGenerationSha =
|
|
3303
|
+
const previousGenerationSha = await this.service.resolveReachablePreviousGeneration(
|
|
3304
|
+
preLock.current_generation ?? null
|
|
3305
|
+
);
|
|
3257
3306
|
let { patches: newPatches, revertedPatchIds } = await this.service.detector.detectNewPatches();
|
|
3258
3307
|
const detectorWarnings = [...this.service.detector.warnings];
|
|
3259
3308
|
const detectorDegraded = [...this.service.detector.degradedReasons];
|
|
@@ -3391,7 +3440,9 @@ var NormalRegenerationFlow = class {
|
|
|
3391
3440
|
}
|
|
3392
3441
|
async prepare(options) {
|
|
3393
3442
|
const preLock = this.service.lockManager.read();
|
|
3394
|
-
const previousGenerationSha =
|
|
3443
|
+
const previousGenerationSha = await this.service.resolveReachablePreviousGeneration(
|
|
3444
|
+
preLock.current_generation ?? null
|
|
3445
|
+
);
|
|
3395
3446
|
if (options?.dryRun) {
|
|
3396
3447
|
const existingPatches2 = this.service.lockManager.getPatches();
|
|
3397
3448
|
const { patches: newPatches2, revertedPatchIds: dryRunReverted } = await this.service.detector.detectNewPatches();
|
|
@@ -3629,6 +3680,37 @@ var ReplayService = class {
|
|
|
3629
3680
|
this.committer = new ReplayCommitter(git, outputDir);
|
|
3630
3681
|
this.fileOwnership = new FileOwnership(git);
|
|
3631
3682
|
}
|
|
3683
|
+
/**
|
|
3684
|
+
* Resolve the previous-generation SHA to export on
|
|
3685
|
+
* `ReplayPreparation.previousGenerationSha`.
|
|
3686
|
+
*
|
|
3687
|
+
* `current_generation` is the SHA of a LOCALLY-created `[fern-generated]` commit.
|
|
3688
|
+
* generator-cli's signed-commit push (`pushSignedCommit`) recreates that commit under a
|
|
3689
|
+
* new remote SHA, and a squash-merge collapses it away, so the recorded value can be
|
|
3690
|
+
* unreachable in a later clone (org "Natural": autoVersion then diffed against a phantom
|
|
3691
|
+
* baseline and silently reported NO_CHANGE — or, pre-a38bbc6, shipped
|
|
3692
|
+
* `0.0.0-fern-placeholder`). Consumers (generator-cli's autoVersion) diff against this
|
|
3693
|
+
* baseline, so it MUST resolve to a real commit.
|
|
3694
|
+
*
|
|
3695
|
+
* Prefer the recorded SHA while it still resolves — the healthy case, and the pristine
|
|
3696
|
+
* prior `[fern-generated]` tree. Otherwise re-anchor on the most recent reachable
|
|
3697
|
+
* generation boundary in first-parent history: the SAME value the detector already
|
|
3698
|
+
* derives for its own scan boundary (`findPreviousGenerationFromHistory`, whose
|
|
3699
|
+
* `isGenerationBoundary` matches squash-merge subjects like `SDK Generation (#N)`). Fall
|
|
3700
|
+
* back to the recorded SHA only when no boundary is reachable at all — no worse than the
|
|
3701
|
+
* prior behavior. ADR 0002 (generator-cli) allows replay to expose this reconciled value;
|
|
3702
|
+
* the consumer stays independently resilient.
|
|
3703
|
+
*
|
|
3704
|
+
* MUST be called before the new `[fern-generated]` commit is created, while HEAD is still
|
|
3705
|
+
* the pre-generation (customer) state, so the walk anchors on the PREVIOUS generation.
|
|
3706
|
+
*/
|
|
3707
|
+
async resolveReachablePreviousGeneration(recordedSha) {
|
|
3708
|
+
if (recordedSha != null && await this.git.commitExists(recordedSha)) {
|
|
3709
|
+
return recordedSha;
|
|
3710
|
+
}
|
|
3711
|
+
const derived = await this.detector.findPreviousGenerationFromHistory();
|
|
3712
|
+
return derived ?? recordedSha;
|
|
3713
|
+
}
|
|
3632
3714
|
/**
|
|
3633
3715
|
* Phase 1 of the two-phase replay flow. Runs preGenerationRebase (when applicable),
|
|
3634
3716
|
* detects new patches, and creates the `[fern-generated]` commit. Leaves HEAD at
|
|
@@ -3911,7 +3993,9 @@ var ReplayService = class {
|
|
|
3911
3993
|
if (baseChanged) repointed++;
|
|
3912
3994
|
continue;
|
|
3913
3995
|
}
|
|
3914
|
-
const
|
|
3996
|
+
const rawDiff = await this.git.exec(["diff", currentGenSha, "--", ...patch.files]).catch(() => null);
|
|
3997
|
+
const neutralizedRebase = rawDiff != null && containsPlaceholder(rawDiff) ? await this.computeAutoversionNeutralizedRebase(patch.files, currentGenSha) : null;
|
|
3998
|
+
const diff = neutralizedRebase ? neutralizedRebase.diff : rawDiff;
|
|
3915
3999
|
if (diff === null) continue;
|
|
3916
4000
|
if (!diff.trim()) {
|
|
3917
4001
|
if (hasProtectedFiles) {
|
|
@@ -3967,7 +4051,7 @@ var ReplayService = class {
|
|
|
3967
4051
|
patch.base_generation = currentGenSha;
|
|
3968
4052
|
patch.patch_content = diff;
|
|
3969
4053
|
patch.content_hash = newContentHash;
|
|
3970
|
-
const snapshot = await this.readSnapshotFromDisk(patch.files);
|
|
4054
|
+
const snapshot = neutralizedRebase ? neutralizedRebase.snapshot : await this.readSnapshotFromDisk(patch.files);
|
|
3971
4055
|
const snapshotIsNonEmpty = Object.keys(snapshot).length > 0;
|
|
3972
4056
|
if (snapshotIsNonEmpty) {
|
|
3973
4057
|
patch.theirs_snapshot = snapshot;
|
|
@@ -4007,6 +4091,47 @@ var ReplayService = class {
|
|
|
4007
4091
|
}
|
|
4008
4092
|
return snapshot;
|
|
4009
4093
|
}
|
|
4094
|
+
/**
|
|
4095
|
+
* Autoversion-aware replacement for the naive `git diff currentGen -- files`
|
|
4096
|
+
* + on-disk snapshot used when rebasing a cleanly-applied patch.
|
|
4097
|
+
*
|
|
4098
|
+
* The generation tree (BASE) still carries the placeholder on version lines
|
|
4099
|
+
* (autoversion runs after `[fern-generated]`), but the working tree holds
|
|
4100
|
+
* autoversion's resolved semver. A raw diff/snapshot would fold the
|
|
4101
|
+
* placeholder->semver swap into the customer's patch_content/theirs_snapshot,
|
|
4102
|
+
* pinning a stale version. This re-derives both against a version-neutralized
|
|
4103
|
+
* THEIRS so the pipeline-owned version line never enters the patch.
|
|
4104
|
+
*
|
|
4105
|
+
* Returns null when no file is under autoversion (fixed `--version`, i.e. no
|
|
4106
|
+
* placeholder in the generation tree) — callers keep their existing behavior.
|
|
4107
|
+
*/
|
|
4108
|
+
async computeAutoversionNeutralizedRebase(files, currentGenSha) {
|
|
4109
|
+
const genByFile = /* @__PURE__ */ new Map();
|
|
4110
|
+
let anyPlaceholder = false;
|
|
4111
|
+
for (const file of files) {
|
|
4112
|
+
const gen = await this.git.showFile(currentGenSha, file).catch(() => null);
|
|
4113
|
+
genByFile.set(file, gen);
|
|
4114
|
+
if (containsPlaceholder(gen)) anyPlaceholder = true;
|
|
4115
|
+
}
|
|
4116
|
+
if (!anyPlaceholder) return null;
|
|
4117
|
+
const snapshot = {};
|
|
4118
|
+
const fragments = [];
|
|
4119
|
+
for (const file of files) {
|
|
4120
|
+
if (isBinaryFile(file)) continue;
|
|
4121
|
+
const disk = await (0, import_promises6.readFile)((0, import_node_path9.join)(this.outputDir, file), "utf-8").catch(() => null);
|
|
4122
|
+
if (disk == null) continue;
|
|
4123
|
+
const gen = genByFile.get(file) ?? null;
|
|
4124
|
+
const neutralized = gen != null ? neutralizeAutoversionTheirs(gen, disk) : disk;
|
|
4125
|
+
snapshot[file] = neutralized;
|
|
4126
|
+
if (gen == null) {
|
|
4127
|
+
fragments.push(synthesizeNewFileDiff(file, neutralized));
|
|
4128
|
+
} else if (gen !== neutralized) {
|
|
4129
|
+
const fileDiff = await unifiedDiff(gen, neutralized, file);
|
|
4130
|
+
if (fileDiff.trim()) fragments.push(fileDiff);
|
|
4131
|
+
}
|
|
4132
|
+
}
|
|
4133
|
+
return { diff: fragments.join(""), snapshot };
|
|
4134
|
+
}
|
|
4010
4135
|
/**
|
|
4011
4136
|
* Detects autoversion contamination — returns true when a
|
|
4012
4137
|
* `[fern-autoversion]` commit between `fromSha` and `toSha` touched any
|