@fern-api/replay 0.15.2 → 0.16.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/index.d.cts CHANGED
@@ -12,8 +12,6 @@ interface GenerationRecord {
12
12
  timestamp: string;
13
13
  cli_version: string;
14
14
  generator_versions: Record<string, string>;
15
- /** SHA of the base branch HEAD before replay ran. Always on main's lineage. */
16
- base_branch_head?: string;
17
15
  }
18
16
  interface StoredPatch {
19
17
  id: string;
@@ -259,22 +257,24 @@ declare class ReplayDetector {
259
257
  private sdkOutputDir;
260
258
  readonly warnings: string[];
261
259
  constructor(git: GitClient, lockManager: LockfileManager, sdkOutputDir: string);
262
- detectNewPatches(): Promise<DetectionResult>;
263
260
  /**
264
- * Non-linear-history detection via `merge-base(prevGen, HEAD)`.
261
+ * Derive the previous-generation SHA from git history instead of trusting
262
+ * `lock.current_generation`.
263
+ *
264
+ * Walks `git log HEAD --first-parent` and returns the SHA of the most recent
265
+ * commit that `isGenerationBoundary` classifies as a customization-baseline
266
+ * boundary, or null if no such commit is reachable from HEAD on the mainline.
265
267
  *
266
- * After a squash-merge of a Fern-bot PR, `lastGen.commit_sha` (the previous
267
- * `[fern-generated]`) is orphaned but still in git's object database. The
268
- * merge-base is the commit on main from which the bot's branch was created —
269
- * a reachable ancestor of HEAD. Walking that range and classifying each
270
- * commit via `isGenerationCommit` mirrors the linear path and eliminates
271
- * the bug class where pipeline-driven changes (autoversion, replay,
272
- * generation) get bundled as customer customizations.
268
+ * `isGenerationBoundary` is intentionally narrower than `isGenerationCommit`:
269
+ * it excludes `[fern-autoversion]` (lightweight bumps that don't reset the
270
+ * baseline) and the GitHub merge-commit subject for fern-bot regen PRs
271
+ * (which sits on first-parent but where the customer fix-up commits live
272
+ * on second-parent stopping at the merge would hide them).
273
273
  *
274
- * Falls through to the legacy composite path when no common ancestor exists
275
- * (truly disjoint history — orphan branches with no shared root).
274
+ * See docs/adr/0001-derived-scan-boundary.md.
276
275
  */
277
- private detectPatchesViaMergeBase;
276
+ findPreviousGenerationFromHistory(): Promise<string | null>;
277
+ detectNewPatches(): Promise<DetectionResult>;
278
278
  /**
279
279
  * Walk `${rangeStart}..HEAD`, classify each commit, emit one
280
280
  * `StoredPatch` per customer commit. Body shared by the linear path
@@ -368,6 +368,13 @@ declare class ReplayDetector {
368
368
  private resolveDiffBase;
369
369
  private parseGitLog;
370
370
  private getLastGeneration;
371
+ /**
372
+ * Build a transient `GenerationRecord` from a SHA derived by walking git
373
+ * history. Only `commit_sha` (and `tree_hash` when needed downstream) is
374
+ * load-bearing here; the rest are filled with defensible placeholders.
375
+ * This record is consumed by `detectPatchesInRange` and is never persisted.
376
+ */
377
+ private synthesizeGenerationRecord;
371
378
  }
372
379
 
373
380
  /**
@@ -468,7 +475,6 @@ declare class ReplayApplicator {
468
475
  interface CommitOptions {
469
476
  cliVersion: string;
470
477
  generatorVersions: Record<string, string>;
471
- baseBranchHead?: string;
472
478
  }
473
479
  declare class ReplayCommitter {
474
480
  private git;
@@ -587,12 +593,10 @@ interface ReplayOptions {
587
593
  generatorVersions?: Record<string, string>;
588
594
  /** Commit generation + update lockfile, skip detection/application */
589
595
  skipApplication?: boolean;
590
- /** SHA of the base branch HEAD before replay runs. Stored in lockfile for squash merge recovery. */
591
- baseBranchHead?: string;
592
596
  }
593
597
  /**
594
598
  * Options honored by `applyPreparedReplay()`. `cliVersion`, `generatorVersions`,
595
- * `baseBranchHead`, `dryRun`, and `skipApplication` were all captured in phase 1
599
+ * `dryRun`, and `skipApplication` were all captured in phase 1
596
600
  * and re-supplying them here is meaningless.
597
601
  */
598
602
  interface ApplyOptions {
@@ -612,8 +616,8 @@ interface ApplyOptions {
612
616
  * respected. `rebasePatches` uses `currentGenerationSha` (the pure `[fern-generated]`
613
617
  * SHA), NOT HEAD — mid-phase commits do not retarget patches' `base_generation`.
614
618
  * - The service instance is single-use across the phase boundary: do not call
615
- * other service methods (`syncFromDivergentMerge`, another `prepareReplay`, etc.)
616
- * between phases. In-memory lockfile and warning state would be corrupted.
619
+ * another `prepareReplay()` between phases. In-memory lockfile and warning
620
+ * state would be corrupted.
617
621
  * - If the caller's mid-phase work throws or `applyPreparedReplay` is never called,
618
622
  * the repo is left with the new `[fern-generated]` commit but a stale lockfile.
619
623
  * Consumers MUST either (a) still call `applyPreparedReplay` so the lockfile
@@ -628,8 +632,6 @@ interface ReplayPreparation {
628
632
  previousGenerationSha: string | null;
629
633
  /** SHA of the new [fern-generated] commit created by prepareReplay (or HEAD for dry-run terminals) */
630
634
  currentGenerationSha: string;
631
- /** options.baseBranchHead passed through */
632
- baseBranchHead: string | null;
633
635
  /** @internal Opaque state consumed by applyPreparedReplay */
634
636
  readonly _prepared: InternalPreparationState;
635
637
  }
@@ -749,21 +751,6 @@ declare class ReplayService {
749
751
  * Behavior is byte-identical to the pre-split implementation for all existing callers.
750
752
  */
751
753
  runReplay(options?: ReplayOptions): Promise<ReplayReport>;
752
- /**
753
- * Sync the lockfile after a divergent PR was squash-merged.
754
- * Call this BEFORE runReplay() when the CLI detects a merged divergent PR.
755
- *
756
- * After updating the generation record, re-detects customer patches via
757
- * tree diff between the generation tag (pure generation tree) and HEAD
758
- * (which includes customer customizations after squash merge). This ensures
759
- * patches survive the squash merge → regenerate cycle even when the lockfile
760
- * was restored from the base branch during conflict PR creation.
761
- */
762
- syncFromDivergentMerge(generationCommitSha: string, options?: {
763
- cliVersion?: string;
764
- generatorVersions?: Record<string, string>;
765
- baseBranchHead?: string;
766
- }): Promise<void>;
767
754
  private determineFlow;
768
755
  /**
769
756
  * Rebase cleanly applied patches so they are relative to the current generation.
package/dist/index.d.ts CHANGED
@@ -12,8 +12,6 @@ interface GenerationRecord {
12
12
  timestamp: string;
13
13
  cli_version: string;
14
14
  generator_versions: Record<string, string>;
15
- /** SHA of the base branch HEAD before replay ran. Always on main's lineage. */
16
- base_branch_head?: string;
17
15
  }
18
16
  interface StoredPatch {
19
17
  id: string;
@@ -259,22 +257,24 @@ declare class ReplayDetector {
259
257
  private sdkOutputDir;
260
258
  readonly warnings: string[];
261
259
  constructor(git: GitClient, lockManager: LockfileManager, sdkOutputDir: string);
262
- detectNewPatches(): Promise<DetectionResult>;
263
260
  /**
264
- * Non-linear-history detection via `merge-base(prevGen, HEAD)`.
261
+ * Derive the previous-generation SHA from git history instead of trusting
262
+ * `lock.current_generation`.
263
+ *
264
+ * Walks `git log HEAD --first-parent` and returns the SHA of the most recent
265
+ * commit that `isGenerationBoundary` classifies as a customization-baseline
266
+ * boundary, or null if no such commit is reachable from HEAD on the mainline.
265
267
  *
266
- * After a squash-merge of a Fern-bot PR, `lastGen.commit_sha` (the previous
267
- * `[fern-generated]`) is orphaned but still in git's object database. The
268
- * merge-base is the commit on main from which the bot's branch was created —
269
- * a reachable ancestor of HEAD. Walking that range and classifying each
270
- * commit via `isGenerationCommit` mirrors the linear path and eliminates
271
- * the bug class where pipeline-driven changes (autoversion, replay,
272
- * generation) get bundled as customer customizations.
268
+ * `isGenerationBoundary` is intentionally narrower than `isGenerationCommit`:
269
+ * it excludes `[fern-autoversion]` (lightweight bumps that don't reset the
270
+ * baseline) and the GitHub merge-commit subject for fern-bot regen PRs
271
+ * (which sits on first-parent but where the customer fix-up commits live
272
+ * on second-parent stopping at the merge would hide them).
273
273
  *
274
- * Falls through to the legacy composite path when no common ancestor exists
275
- * (truly disjoint history — orphan branches with no shared root).
274
+ * See docs/adr/0001-derived-scan-boundary.md.
276
275
  */
277
- private detectPatchesViaMergeBase;
276
+ findPreviousGenerationFromHistory(): Promise<string | null>;
277
+ detectNewPatches(): Promise<DetectionResult>;
278
278
  /**
279
279
  * Walk `${rangeStart}..HEAD`, classify each commit, emit one
280
280
  * `StoredPatch` per customer commit. Body shared by the linear path
@@ -368,6 +368,13 @@ declare class ReplayDetector {
368
368
  private resolveDiffBase;
369
369
  private parseGitLog;
370
370
  private getLastGeneration;
371
+ /**
372
+ * Build a transient `GenerationRecord` from a SHA derived by walking git
373
+ * history. Only `commit_sha` (and `tree_hash` when needed downstream) is
374
+ * load-bearing here; the rest are filled with defensible placeholders.
375
+ * This record is consumed by `detectPatchesInRange` and is never persisted.
376
+ */
377
+ private synthesizeGenerationRecord;
371
378
  }
372
379
 
373
380
  /**
@@ -468,7 +475,6 @@ declare class ReplayApplicator {
468
475
  interface CommitOptions {
469
476
  cliVersion: string;
470
477
  generatorVersions: Record<string, string>;
471
- baseBranchHead?: string;
472
478
  }
473
479
  declare class ReplayCommitter {
474
480
  private git;
@@ -587,12 +593,10 @@ interface ReplayOptions {
587
593
  generatorVersions?: Record<string, string>;
588
594
  /** Commit generation + update lockfile, skip detection/application */
589
595
  skipApplication?: boolean;
590
- /** SHA of the base branch HEAD before replay runs. Stored in lockfile for squash merge recovery. */
591
- baseBranchHead?: string;
592
596
  }
593
597
  /**
594
598
  * Options honored by `applyPreparedReplay()`. `cliVersion`, `generatorVersions`,
595
- * `baseBranchHead`, `dryRun`, and `skipApplication` were all captured in phase 1
599
+ * `dryRun`, and `skipApplication` were all captured in phase 1
596
600
  * and re-supplying them here is meaningless.
597
601
  */
598
602
  interface ApplyOptions {
@@ -612,8 +616,8 @@ interface ApplyOptions {
612
616
  * respected. `rebasePatches` uses `currentGenerationSha` (the pure `[fern-generated]`
613
617
  * SHA), NOT HEAD — mid-phase commits do not retarget patches' `base_generation`.
614
618
  * - The service instance is single-use across the phase boundary: do not call
615
- * other service methods (`syncFromDivergentMerge`, another `prepareReplay`, etc.)
616
- * between phases. In-memory lockfile and warning state would be corrupted.
619
+ * another `prepareReplay()` between phases. In-memory lockfile and warning
620
+ * state would be corrupted.
617
621
  * - If the caller's mid-phase work throws or `applyPreparedReplay` is never called,
618
622
  * the repo is left with the new `[fern-generated]` commit but a stale lockfile.
619
623
  * Consumers MUST either (a) still call `applyPreparedReplay` so the lockfile
@@ -628,8 +632,6 @@ interface ReplayPreparation {
628
632
  previousGenerationSha: string | null;
629
633
  /** SHA of the new [fern-generated] commit created by prepareReplay (or HEAD for dry-run terminals) */
630
634
  currentGenerationSha: string;
631
- /** options.baseBranchHead passed through */
632
- baseBranchHead: string | null;
633
635
  /** @internal Opaque state consumed by applyPreparedReplay */
634
636
  readonly _prepared: InternalPreparationState;
635
637
  }
@@ -749,21 +751,6 @@ declare class ReplayService {
749
751
  * Behavior is byte-identical to the pre-split implementation for all existing callers.
750
752
  */
751
753
  runReplay(options?: ReplayOptions): Promise<ReplayReport>;
752
- /**
753
- * Sync the lockfile after a divergent PR was squash-merged.
754
- * Call this BEFORE runReplay() when the CLI detects a merged divergent PR.
755
- *
756
- * After updating the generation record, re-detects customer patches via
757
- * tree diff between the generation tag (pure generation tree) and HEAD
758
- * (which includes customer customizations after squash merge). This ensures
759
- * patches survive the squash merge → regenerate cycle even when the lockfile
760
- * was restored from the base branch during conflict PR creation.
761
- */
762
- syncFromDivergentMerge(generationCommitSha: string, options?: {
763
- cliVersion?: string;
764
- generatorVersions?: Record<string, string>;
765
- baseBranchHead?: string;
766
- }): Promise<void>;
767
754
  private determineFlow;
768
755
  /**
769
756
  * Rebase cleanly applied patches so they are relative to the current generation.
package/dist/index.js CHANGED
@@ -499,9 +499,22 @@ function isGenerationCommit(commit) {
499
499
  const hasGenerationMarker = commit.message.startsWith("[fern-generated]") || commit.message.startsWith("[fern-replay]") || commit.message.startsWith("[fern-autoversion]") || commit.message.includes("Generated by Fern") || commit.message.includes("\u{1F916} Generated with Fern") || // Squash merge of a Fern-generated PR uses the PR title as commit message.
500
500
  // The default PR title is "SDK Generation" (from GithubStep's commitMessage default).
501
501
  // GitHub appends "(#N)" for the PR number, e.g. "SDK Generation (#70)".
502
- commit.message.startsWith("SDK Generation");
502
+ commit.message.startsWith("SDK Generation") || // Merge commit (GitHub's "Create a merge commit" option) of a fern-bot
503
+ // regen PR. First-parent walking lands on this commit, not on the
504
+ // generation commit on the side branch, so the classifier must treat
505
+ // it as a generation boundary. GitHub's default subject is
506
+ // "Merge pull request #N from <owner>/<branch>"; fern-bot branches are
507
+ // named "<owner>/fern-bot/<...>".
508
+ /^Merge pull request #\d+ from [^/]+\/fern-bot\//.test(commit.message);
503
509
  return isBotAuthor || hasGenerationMarker;
504
510
  }
511
+ function isGenerationBoundary(commit) {
512
+ const isFernSupport = FERN_SUPPORT_NAMES.includes(commit.authorName);
513
+ const isBotAuthor = !isFernSupport && (commit.authorLogin === FERN_BOT_LOGIN || commit.authorEmail === FERN_BOT_EMAIL || commit.authorName === FERN_BOT_NAME);
514
+ const isAutoversion = commit.message.startsWith("[fern-autoversion]");
515
+ const hasBoundaryMarker = commit.message.startsWith("[fern-generated]") || commit.message.startsWith("[fern-replay]") || commit.message.includes("Generated by Fern") || commit.message.includes("\u{1F916} Generated with Fern") || commit.message.startsWith("SDK Generation");
516
+ return isBotAuthor && !isAutoversion || hasBoundaryMarker;
517
+ }
505
518
  function isReplayCommit(commit) {
506
519
  return commit.message.startsWith("[fern-replay]");
507
520
  }
@@ -829,8 +842,48 @@ var ReplayDetector = class {
829
842
  this.lockManager = lockManager;
830
843
  this.sdkOutputDir = sdkOutputDir;
831
844
  }
845
+ /**
846
+ * Derive the previous-generation SHA from git history instead of trusting
847
+ * `lock.current_generation`.
848
+ *
849
+ * Walks `git log HEAD --first-parent` and returns the SHA of the most recent
850
+ * commit that `isGenerationBoundary` classifies as a customization-baseline
851
+ * boundary, or null if no such commit is reachable from HEAD on the mainline.
852
+ *
853
+ * `isGenerationBoundary` is intentionally narrower than `isGenerationCommit`:
854
+ * it excludes `[fern-autoversion]` (lightweight bumps that don't reset the
855
+ * baseline) and the GitHub merge-commit subject for fern-bot regen PRs
856
+ * (which sits on first-parent but where the customer fix-up commits live
857
+ * on second-parent — stopping at the merge would hide them).
858
+ *
859
+ * See docs/adr/0001-derived-scan-boundary.md.
860
+ */
861
+ async findPreviousGenerationFromHistory() {
862
+ let log;
863
+ try {
864
+ log = await this.git.exec([
865
+ "log",
866
+ "--first-parent",
867
+ "--format=%H%x00%an%x00%ae%x00%s",
868
+ "HEAD"
869
+ ]);
870
+ } catch {
871
+ return null;
872
+ }
873
+ for (const commit of this.parseGitLog(log)) {
874
+ if (isGenerationBoundary(commit)) {
875
+ return commit.sha;
876
+ }
877
+ }
878
+ return null;
879
+ }
832
880
  async detectNewPatches() {
833
881
  const lock = this.lockManager.read();
882
+ const derivedSha = await this.findPreviousGenerationFromHistory();
883
+ if (derivedSha !== null) {
884
+ const derivedRecord = await this.synthesizeGenerationRecord(derivedSha);
885
+ return this.detectPatchesInRange(derivedSha, derivedRecord, lock);
886
+ }
834
887
  const lastGen = this.getLastGeneration(lock);
835
888
  if (!lastGen) {
836
889
  return { patches: [], revertedPatchIds: [] };
@@ -838,7 +891,7 @@ var ReplayDetector = class {
838
891
  const exists = await this.git.commitExists(lastGen.commit_sha);
839
892
  if (!exists) {
840
893
  this.warnings.push(
841
- `Generation commit ${lastGen.commit_sha.slice(0, 7)} not found in git history. Falling back to alternate detection.`
894
+ `Generation commit ${lastGen.commit_sha.slice(0, 7)} not found in git history. Falling back to tree-diff detection (likely a shallow clone).`
842
895
  );
843
896
  return this.detectPatchesViaTreeDiff(
844
897
  lastGen,
@@ -846,44 +899,8 @@ var ReplayDetector = class {
846
899
  true
847
900
  );
848
901
  }
849
- const isAncestor = await this.git.isAncestor(lastGen.commit_sha, "HEAD");
850
- if (!isAncestor) {
851
- return this.detectPatchesViaMergeBase(lastGen, lock);
852
- }
853
902
  return this.detectPatchesInRange(lastGen.commit_sha, lastGen, lock);
854
903
  }
855
- /**
856
- * Non-linear-history detection via `merge-base(prevGen, HEAD)`.
857
- *
858
- * After a squash-merge of a Fern-bot PR, `lastGen.commit_sha` (the previous
859
- * `[fern-generated]`) is orphaned but still in git's object database. The
860
- * merge-base is the commit on main from which the bot's branch was created —
861
- * a reachable ancestor of HEAD. Walking that range and classifying each
862
- * commit via `isGenerationCommit` mirrors the linear path and eliminates
863
- * the bug class where pipeline-driven changes (autoversion, replay,
864
- * generation) get bundled as customer customizations.
865
- *
866
- * Falls through to the legacy composite path when no common ancestor exists
867
- * (truly disjoint history — orphan branches with no shared root).
868
- */
869
- async detectPatchesViaMergeBase(lastGen, lock) {
870
- let mergeBase = "";
871
- try {
872
- mergeBase = (await this.git.exec(["merge-base", lastGen.commit_sha, "HEAD"])).trim();
873
- } catch {
874
- }
875
- if (!mergeBase) {
876
- this.warnings.push(
877
- `No common ancestor between previous generation ${lastGen.commit_sha.slice(0, 7)} and HEAD. Falling back to composite tree-diff detection.`
878
- );
879
- return this.detectPatchesViaTreeDiff(
880
- lastGen,
881
- /* commitKnownMissing */
882
- false
883
- );
884
- }
885
- return this.detectPatchesInRange(mergeBase, lastGen, lock);
886
- }
887
904
  /**
888
905
  * Walk `${rangeStart}..HEAD`, classify each commit, emit one
889
906
  * `StoredPatch` per customer commit. Body shared by the linear path
@@ -1319,6 +1336,26 @@ var ReplayDetector = class {
1319
1336
  getLastGeneration(lock) {
1320
1337
  return lock.generations.find((g) => g.commit_sha === lock.current_generation);
1321
1338
  }
1339
+ /**
1340
+ * Build a transient `GenerationRecord` from a SHA derived by walking git
1341
+ * history. Only `commit_sha` (and `tree_hash` when needed downstream) is
1342
+ * load-bearing here; the rest are filled with defensible placeholders.
1343
+ * This record is consumed by `detectPatchesInRange` and is never persisted.
1344
+ */
1345
+ async synthesizeGenerationRecord(sha) {
1346
+ let tree_hash = "";
1347
+ try {
1348
+ tree_hash = await this.git.getTreeHash(sha);
1349
+ } catch {
1350
+ }
1351
+ return {
1352
+ commit_sha: sha,
1353
+ tree_hash,
1354
+ timestamp: (/* @__PURE__ */ new Date(0)).toISOString(),
1355
+ cli_version: "",
1356
+ generator_versions: {}
1357
+ };
1358
+ }
1322
1359
  };
1323
1360
 
1324
1361
  // src/ThreeWayMerge.ts
@@ -2458,8 +2495,7 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
2458
2495
  tree_hash: treeHash,
2459
2496
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2460
2497
  cli_version: options?.cliVersion ?? "unknown",
2461
- generator_versions: options?.generatorVersions ?? {},
2462
- base_branch_head: options?.baseBranchHead
2498
+ generator_versions: options?.generatorVersions ?? {}
2463
2499
  };
2464
2500
  }
2465
2501
  async stageAll() {
@@ -2846,7 +2882,6 @@ var FirstGenerationFlow = class {
2846
2882
  flow: "first-generation",
2847
2883
  previousGenerationSha: null,
2848
2884
  currentGenerationSha: headSha,
2849
- baseBranchHead: options.baseBranchHead ?? null,
2850
2885
  _prepared: {
2851
2886
  kind: "terminal",
2852
2887
  flow: "first-generation",
@@ -2856,8 +2891,7 @@ var FirstGenerationFlow = class {
2856
2891
  }
2857
2892
  const commitOpts = options ? {
2858
2893
  cliVersion: options.cliVersion ?? "unknown",
2859
- generatorVersions: options.generatorVersions ?? {},
2860
- baseBranchHead: options.baseBranchHead
2894
+ generatorVersions: options.generatorVersions ?? {}
2861
2895
  } : void 0;
2862
2896
  await this.service.committer.commitGeneration("Initial SDK generation", commitOpts);
2863
2897
  const genRecord = await this.service.committer.createGenerationRecord(commitOpts);
@@ -2866,7 +2900,6 @@ var FirstGenerationFlow = class {
2866
2900
  flow: "first-generation",
2867
2901
  previousGenerationSha: null,
2868
2902
  currentGenerationSha: genRecord.commit_sha,
2869
- baseBranchHead: options?.baseBranchHead ?? null,
2870
2903
  _prepared: {
2871
2904
  kind: "active",
2872
2905
  flow: "first-generation",
@@ -2903,8 +2936,7 @@ var SkipApplicationFlow = class {
2903
2936
  async prepare(options) {
2904
2937
  const commitOpts = options ? {
2905
2938
  cliVersion: options.cliVersion ?? "unknown",
2906
- generatorVersions: options.generatorVersions ?? {},
2907
- baseBranchHead: options.baseBranchHead
2939
+ generatorVersions: options.generatorVersions ?? {}
2908
2940
  } : void 0;
2909
2941
  await this.service.committer.commitGeneration("Update SDK (replay skipped)", commitOpts);
2910
2942
  await this.service.cleanupStaleConflictMarkers();
@@ -2926,7 +2958,6 @@ var SkipApplicationFlow = class {
2926
2958
  flow: "skip-application",
2927
2959
  previousGenerationSha,
2928
2960
  currentGenerationSha: genRecord.commit_sha,
2929
- baseBranchHead: options?.baseBranchHead ?? null,
2930
2961
  _prepared: {
2931
2962
  kind: "active",
2932
2963
  flow: "skip-application",
@@ -2987,7 +3018,6 @@ var NoPatchesFlow = class {
2987
3018
  flow: "no-patches",
2988
3019
  previousGenerationSha,
2989
3020
  currentGenerationSha: headSha,
2990
- baseBranchHead: options.baseBranchHead ?? null,
2991
3021
  _prepared: {
2992
3022
  kind: "terminal",
2993
3023
  flow: "no-patches",
@@ -3019,8 +3049,7 @@ var NoPatchesFlow = class {
3019
3049
  }
3020
3050
  const commitOpts = options ? {
3021
3051
  cliVersion: options.cliVersion ?? "unknown",
3022
- generatorVersions: options.generatorVersions ?? {},
3023
- baseBranchHead: options.baseBranchHead
3052
+ generatorVersions: options.generatorVersions ?? {}
3024
3053
  } : void 0;
3025
3054
  await this.service.committer.commitGeneration("Update SDK", commitOpts);
3026
3055
  await this.service.cleanupStaleConflictMarkers();
@@ -3030,7 +3059,6 @@ var NoPatchesFlow = class {
3030
3059
  flow: "no-patches",
3031
3060
  previousGenerationSha,
3032
3061
  currentGenerationSha: genRecord.commit_sha,
3033
- baseBranchHead: options?.baseBranchHead ?? null,
3034
3062
  _prepared: {
3035
3063
  kind: "active",
3036
3064
  flow: "no-patches",
@@ -3120,7 +3148,6 @@ var NormalRegenerationFlow = class {
3120
3148
  flow: "normal-regeneration",
3121
3149
  previousGenerationSha,
3122
3150
  currentGenerationSha: headSha,
3123
- baseBranchHead: options.baseBranchHead ?? null,
3124
3151
  _prepared: {
3125
3152
  kind: "terminal",
3126
3153
  flow: "normal-regeneration",
@@ -3138,14 +3165,16 @@ var NormalRegenerationFlow = class {
3138
3165
  existingPatches = this.service.lockManager.getPatches();
3139
3166
  const seenHashes = /* @__PURE__ */ new Map();
3140
3167
  for (const p of existingPatches) {
3141
- const priorPatchId = seenHashes.get(p.content_hash);
3142
- if (priorPatchId !== void 0) {
3143
- this.service.lockManager.removePatch(p.id);
3144
- this.service.warnings.push(
3145
- `Consolidated patch ${p.id} (commit ${(p.original_commit || "<missing>").slice(0, 7)}) into prior patch ${priorPatchId}: byte-identical patch_content. Per-commit attribution preserved in git log; lockfile collapsed to avoid duplicate-apply corruption on next regen.`
3146
- );
3147
- } else {
3148
- seenHashes.set(p.content_hash, p.id);
3168
+ if (!p.original_commit || p.original_commit === "") {
3169
+ const priorPatchId = seenHashes.get(p.content_hash);
3170
+ if (priorPatchId !== void 0) {
3171
+ this.service.lockManager.removePatch(p.id);
3172
+ this.service.warnings.push(
3173
+ `Consolidated patch ${p.id} into prior patch ${priorPatchId}: byte-identical patch_content. Lockfile collapsed to avoid duplicate-apply corruption on next regen.`
3174
+ );
3175
+ } else {
3176
+ seenHashes.set(p.content_hash, p.id);
3177
+ }
3149
3178
  }
3150
3179
  }
3151
3180
  existingPatches = this.service.lockManager.getPatches();
@@ -3174,8 +3203,7 @@ var NormalRegenerationFlow = class {
3174
3203
  const allPatches = [...existingPatches, ...newPatches];
3175
3204
  const commitOpts = options ? {
3176
3205
  cliVersion: options.cliVersion ?? "unknown",
3177
- generatorVersions: options.generatorVersions ?? {},
3178
- baseBranchHead: options.baseBranchHead
3206
+ generatorVersions: options.generatorVersions ?? {}
3179
3207
  } : void 0;
3180
3208
  await this.service.committer.commitGeneration("Update SDK", commitOpts);
3181
3209
  await this.service.cleanupStaleConflictMarkers();
@@ -3185,7 +3213,6 @@ var NormalRegenerationFlow = class {
3185
3213
  flow: "normal-regeneration",
3186
3214
  previousGenerationSha,
3187
3215
  currentGenerationSha: genRecord.commit_sha,
3188
- baseBranchHead: options?.baseBranchHead ?? null,
3189
3216
  _prepared: {
3190
3217
  kind: "active",
3191
3218
  flow: "normal-regeneration",
@@ -3378,68 +3405,6 @@ var ReplayService = class {
3378
3405
  const prep = await this.prepareReplay(options);
3379
3406
  return this.applyPreparedReplay(prep, options);
3380
3407
  }
3381
- /**
3382
- * Sync the lockfile after a divergent PR was squash-merged.
3383
- * Call this BEFORE runReplay() when the CLI detects a merged divergent PR.
3384
- *
3385
- * After updating the generation record, re-detects customer patches via
3386
- * tree diff between the generation tag (pure generation tree) and HEAD
3387
- * (which includes customer customizations after squash merge). This ensures
3388
- * patches survive the squash merge → regenerate cycle even when the lockfile
3389
- * was restored from the base branch during conflict PR creation.
3390
- */
3391
- async syncFromDivergentMerge(generationCommitSha, options) {
3392
- const treeHash = await this.git.getTreeHash(generationCommitSha);
3393
- const timestamp = (await this.git.exec(["log", "-1", "--format=%aI", generationCommitSha])).trim();
3394
- const record = {
3395
- commit_sha: generationCommitSha,
3396
- tree_hash: treeHash,
3397
- timestamp,
3398
- cli_version: options?.cliVersion ?? "unknown",
3399
- generator_versions: options?.generatorVersions ?? {},
3400
- base_branch_head: options?.baseBranchHead
3401
- };
3402
- let resolvedPatches;
3403
- if (!this.lockManager.exists()) {
3404
- this.lockManager.initializeInMemory(record);
3405
- } else {
3406
- this.lockManager.read();
3407
- const unresolvedPatches = [
3408
- ...this.lockManager.getUnresolvedPatches(),
3409
- ...this.lockManager.getResolvingPatches()
3410
- ];
3411
- resolvedPatches = this.lockManager.getPatches().filter((p) => p.status == null);
3412
- this.lockManager.addGeneration(record);
3413
- this.lockManager.clearPatches();
3414
- for (const patch of unresolvedPatches) {
3415
- this.lockManager.addPatch(patch);
3416
- }
3417
- }
3418
- try {
3419
- const { patches: redetectedPatches } = await this.detector.detectNewPatches();
3420
- if (redetectedPatches.length > 0) {
3421
- const redetectedFiles = new Set(redetectedPatches.flatMap((p) => p.files));
3422
- const currentPatches = this.lockManager.getPatches();
3423
- for (const patch of currentPatches) {
3424
- if (patch.status != null && patch.files.some((f) => redetectedFiles.has(f))) {
3425
- this.lockManager.removePatch(patch.id);
3426
- }
3427
- }
3428
- for (const patch of redetectedPatches) {
3429
- this.lockManager.addPatch(patch);
3430
- }
3431
- }
3432
- } catch (error) {
3433
- for (const patch of resolvedPatches ?? []) {
3434
- this.lockManager.addPatch(patch);
3435
- }
3436
- this.detector.warnings.push(
3437
- `Patch re-detection failed after divergent merge sync. ${(resolvedPatches ?? []).length} previously resolved patch(es) preserved. Error: ${error instanceof Error ? error.message : String(error)}`
3438
- );
3439
- }
3440
- this.lockManager.save();
3441
- this.detector.warnings.push(...this.lockManager.warnings);
3442
- }
3443
3408
  determineFlow() {
3444
3409
  try {
3445
3410
  const lock = this.lockManager.read();