@fern-api/fern-api-dev 5.23.3-941-g239fc7b1855 → 5.23.3-947-ga9032a3d959

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.
Files changed (2) hide show
  1. package/cli.cjs +387 -128
  2. package/package.json +1 -1
package/cli.cjs CHANGED
@@ -569724,9 +569724,9 @@ var require_diff3 = __commonJS({
569724
569724
  }
569725
569725
  });
569726
569726
 
569727
- // ../../../node_modules/.pnpm/@fern-api+replay@0.18.0/node_modules/@fern-api/replay/dist/index.cjs
569727
+ // ../../../node_modules/.pnpm/@fern-api+replay@0.19.0/node_modules/@fern-api/replay/dist/index.cjs
569728
569728
  var require_dist24 = __commonJS({
569729
- "../../../node_modules/.pnpm/@fern-api+replay@0.18.0/node_modules/@fern-api/replay/dist/index.cjs"(exports2, module4) {
569729
+ "../../../node_modules/.pnpm/@fern-api+replay@0.19.0/node_modules/@fern-api/replay/dist/index.cjs"(exports2, module4) {
569730
569730
  "use strict";
569731
569731
  var __defProp5 = Object.defineProperty;
569732
569732
  var __getOwnPropDesc4 = Object.getOwnPropertyDescriptor;
@@ -570144,15 +570144,15 @@ var require_dist24 = __commonJS({
570144
570144
  async function applyPatchToContent(base4, patchContent, filePath) {
570145
570145
  const fileDiff = extractFileDiff(patchContent, filePath);
570146
570146
  if (!fileDiff) return null;
570147
- const tempDir2 = await (0, import_promises510.mkdtemp)((0, import_node_path72.join)((0, import_node_os32.tmpdir)(), "replay-migrate-"));
570147
+ const tempDir2 = await (0, import_promises510.mkdtemp)((0, import_node_path82.join)((0, import_node_os32.tmpdir)(), "replay-migrate-"));
570148
570148
  const tempGit = new GitClient(tempDir2);
570149
570149
  try {
570150
570150
  await tempGit.exec(["init"]);
570151
570151
  await tempGit.exec(["config", "user.email", "migrate@fern.com"]);
570152
570152
  await tempGit.exec(["config", "user.name", "Fern Replay Migration"]);
570153
570153
  await tempGit.exec(["config", "commit.gpgSign", "false"]);
570154
- const tempFilePath = (0, import_node_path72.join)(tempDir2, filePath);
570155
- await (0, import_promises510.mkdir)((0, import_node_path72.dirname)(tempFilePath), { recursive: true });
570154
+ const tempFilePath = (0, import_node_path82.join)(tempDir2, filePath);
570155
+ await (0, import_promises510.mkdir)((0, import_node_path82.dirname)(tempFilePath), { recursive: true });
570156
570156
  await (0, import_promises510.writeFile)(tempFilePath, base4);
570157
570157
  await tempGit.exec(["add", filePath]);
570158
570158
  await tempGit.exec(["commit", "-m", `base for ${filePath}`, "--allow-empty"]);
@@ -570185,13 +570185,13 @@ var require_dist24 = __commonJS({
570185
570185
  }
570186
570186
  var import_promises510;
570187
570187
  var import_node_os32;
570188
- var import_node_path72;
570188
+ var import_node_path82;
570189
570189
  var init_PatchApplyTheirs = __esm3({
570190
570190
  "src/PatchApplyTheirs.ts"() {
570191
570191
  "use strict";
570192
570192
  import_promises510 = require("fs/promises");
570193
570193
  import_node_os32 = require("os");
570194
- import_node_path72 = require("path");
570194
+ import_node_path82 = require("path");
570195
570195
  init_GitClient();
570196
570196
  }
570197
570197
  });
@@ -570456,6 +570456,22 @@ var require_dist24 = __commonJS({
570456
570456
  this.lock.forgotten_hashes.push(hash5);
570457
570457
  }
570458
570458
  }
570459
+ /**
570460
+ * Record a dismissed customization: visibility metadata in `dismissals`
570461
+ * plus a tombstone in `forgotten_hashes` for every content hash.
570462
+ * `forgotten_hashes` stays the only list the detector consults; the
570463
+ * metadata exists so `status` can show what was dismissed.
570464
+ */
570465
+ addDismissal(record23) {
570466
+ this.ensureLoaded();
570467
+ if (!this.lock.dismissals) {
570468
+ this.lock.dismissals = [];
570469
+ }
570470
+ this.lock.dismissals.push(record23);
570471
+ for (const hash5 of record23.content_hashes) {
570472
+ this.addForgottenHash(hash5);
570473
+ }
570474
+ }
570459
570475
  getUnresolvedPatches() {
570460
570476
  this.ensureLoaded();
570461
570477
  return this.lock.patches.filter((p6) => p6.status === "unresolved");
@@ -570617,12 +570633,14 @@ var require_dist24 = __commonJS({
570617
570633
  return snapshot;
570618
570634
  }
570619
570635
  function filterInfrastructureAndSensitiveFiles(rawFilesOutput, fernignorePatterns) {
570620
- const allFiles = rawFilesOutput.trim().split("\n").filter(Boolean).filter((f3) => !INFRASTRUCTURE_FILES.has(f3)).filter((f3) => !f3.startsWith(".fern/"));
570636
+ const rawFiles = rawFilesOutput.trim().split("\n").filter(Boolean);
570637
+ const infrastructureFiles = rawFiles.filter((f3) => INFRASTRUCTURE_FILES.has(f3) || f3.startsWith(".fern/"));
570638
+ const allFiles = rawFiles.filter((f3) => !INFRASTRUCTURE_FILES.has(f3) && !f3.startsWith(".fern/"));
570621
570639
  const sensitiveFiles = allFiles.filter((f3) => isSensitiveFile(f3));
570622
570640
  const nonSensitive = allFiles.filter((f3) => !isSensitiveFile(f3));
570623
570641
  const fernignoredFiles = nonSensitive.filter((f3) => matchesFernignorePattern(f3, fernignorePatterns));
570624
570642
  const files = nonSensitive.filter((f3) => !matchesFernignorePattern(f3, fernignorePatterns));
570625
- return { files, sensitiveFiles, fernignoredFiles };
570643
+ return { files, sensitiveFiles, fernignoredFiles, infrastructureFiles };
570626
570644
  }
570627
570645
  var ReplayDetector = class {
570628
570646
  git;
@@ -570630,6 +570648,15 @@ var require_dist24 = __commonJS({
570630
570648
  sdkOutputDir;
570631
570649
  fernignorePatterns;
570632
570650
  warnings = [];
570651
+ /**
570652
+ * Structured degraded-state channel, mirroring `warnings`. Populated when
570653
+ * detection cannot guarantee customizations were carried forward (baseline
570654
+ * unreachable in this clone). Cleared per-run by `ReplayService.prepareReplay`
570655
+ * alongside `warnings`. Merged into `ReplayReport.degradedReasons` by each
570656
+ * flow handler. Degraded is observability only: detection results are
570657
+ * unchanged.
570658
+ */
570659
+ degradedReasons = [];
570633
570660
  constructor(git, lockManager, sdkOutputDir, fernignorePatterns = []) {
570634
570661
  this.git = git;
570635
570662
  this.lockManager = lockManager;
@@ -570700,6 +570727,10 @@ var require_dist24 = __commonJS({
570700
570727
  this.warnings.push(
570701
570728
  `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.`
570702
570729
  );
570730
+ this.degradedReasons.push({
570731
+ code: "baseline-unresolvable",
570732
+ message: `The previous generation recorded in the lockfile (${lock.current_generation.slice(0, 7)}) could not be resolved to any reachable baseline during this run. Customizations may not have been carried forward \u2014 review the result before merging.`
570733
+ });
570703
570734
  }
570704
570735
  return { patches: [], revertedPatchIds: [] };
570705
570736
  }
@@ -570708,6 +570739,12 @@ var require_dist24 = __commonJS({
570708
570739
  this.warnings.push(
570709
570740
  `Generation commit ${lastGen.commit_sha.slice(0, 7)} not found in git history. Falling back to tree-diff detection (likely a shallow clone).`
570710
570741
  );
570742
+ if (lock.patches.length > 0) {
570743
+ this.degradedReasons.push({
570744
+ code: "generation-anchor-unreachable",
570745
+ message: `The previous generation (${lastGen.commit_sha.slice(0, 7)}) was not reachable during this run, and ${lock.patches.length} tracked customization(s) could not be verified against it. Customizations may not have been carried forward \u2014 review the result before merging.`
570746
+ });
570747
+ }
570711
570748
  return this.detectPatchesViaTreeDiff(
570712
570749
  lastGen,
570713
570750
  /* commitKnownMissing */
@@ -570747,54 +570784,27 @@ var require_dist24 = __commonJS({
570747
570784
  if (lock.patches.find((p6) => p6.original_commit === commit.sha)) {
570748
570785
  continue;
570749
570786
  }
570750
- const parents = await this.git.getCommitParents(commit.sha);
570751
- let filesOutput;
570752
- try {
570753
- filesOutput = await this.git.exec(["diff-tree", "--no-commit-id", "--name-only", "-r", commit.sha]);
570754
- } catch {
570787
+ const materialized = await this.materializeCommitPatch(commit.sha);
570788
+ if (materialized.sensitiveFiles.length > 0) {
570755
570789
  this.warnings.push(
570756
- `Could not generate patch for commit ${commit.sha.slice(0, 7)} \u2014 it may be unreachable in a shallow clone. Skipping.`
570790
+ `Sensitive file(s) excluded from patch for commit ${commit.sha.slice(0, 7)}: ${materialized.sensitiveFiles.join(", ")}. These files will not be tracked by replay.`
570757
570791
  );
570758
- continue;
570759
570792
  }
570760
- const { files, sensitiveFiles, fernignoredFiles } = filterInfrastructureAndSensitiveFiles(
570761
- filesOutput,
570762
- this.fernignorePatterns
570763
- );
570764
- if (sensitiveFiles.length > 0) {
570793
+ if (materialized.fernignoredFiles.length > 0) {
570765
570794
  this.warnings.push(
570766
- `Sensitive file(s) excluded from patch for commit ${commit.sha.slice(0, 7)}: ${sensitiveFiles.join(", ")}. These files will not be tracked by replay.`
570795
+ `.fernignore-protected file(s) excluded from patch for commit ${commit.sha.slice(0, 7)}: ${materialized.fernignoredFiles.join(", ")}. These files are managed by .fernignore, not Replay.`
570767
570796
  );
570768
570797
  }
570769
- if (fernignoredFiles.length > 0) {
570798
+ if (materialized.status === "unreachable") {
570770
570799
  this.warnings.push(
570771
- `.fernignore-protected file(s) excluded from patch for commit ${commit.sha.slice(0, 7)}: ${fernignoredFiles.join(", ")}. These files are managed by .fernignore, not Replay.`
570800
+ `Could not generate patch for commit ${commit.sha.slice(0, 7)} \u2014 it may be unreachable in a shallow clone. Skipping.`
570772
570801
  );
570773
- }
570774
- if (files.length === 0) {
570775
570802
  continue;
570776
570803
  }
570777
- const wasFiltered = sensitiveFiles.length > 0 || fernignoredFiles.length > 0;
570778
- const parentSha = parents[0];
570779
- let patchContent;
570780
- if (wasFiltered && parentSha) {
570781
- try {
570782
- patchContent = await this.git.exec(["diff", parentSha, commit.sha, "--", ...files]);
570783
- } catch {
570784
- continue;
570785
- }
570786
- if (!patchContent.trim()) continue;
570787
- } else {
570788
- try {
570789
- patchContent = await this.git.formatPatch(commit.sha);
570790
- } catch {
570791
- this.warnings.push(
570792
- `Could not generate patch for commit ${commit.sha.slice(0, 7)} \u2014 it may be unreachable in a shallow clone. Skipping.`
570793
- );
570794
- continue;
570795
- }
570804
+ if (materialized.status === "empty") {
570805
+ continue;
570796
570806
  }
570797
- const contentHash = this.computeContentHash(patchContent);
570807
+ const { files, patchContent, contentHash } = materialized;
570798
570808
  if (lock.patches.find((p6) => p6.content_hash === contentHash) || forgottenHashes.has(contentHash)) {
570799
570809
  continue;
570800
570810
  }
@@ -570871,6 +570881,63 @@ var require_dist24 = __commonJS({
570871
570881
  const filteredPatches = survivingPatches.filter((_5, i5) => !revertIndicesToRemove.has(i5));
570872
570882
  return { patches: filteredPatches, revertedPatchIds: [...revertedPatchIdSet] };
570873
570883
  }
570884
+ /**
570885
+ * Materialize a commit into patch content exactly as detection does.
570886
+ *
570887
+ * Pipeline: `diff-tree --name-only` → filter infrastructure / sensitive /
570888
+ * `.fernignore`d files → scoped `git diff` when files were stripped,
570889
+ * otherwise full `git format-patch` → content hash.
570890
+ *
570891
+ * This is THE definition of a commit's re-emission identity: the content
570892
+ * hash this method returns is what `detectPatchesInRange` will compute if
570893
+ * the commit re-enters the scan range (e.g. after a regen PR is merged
570894
+ * with a merge commit). Callers that tombstone a dismissed patch must use
570895
+ * this hash — the stored `content_hash` drifts every time the patch is
570896
+ * carried forward, so it does not identify the commit on re-detection.
570897
+ *
570898
+ * Never throws; failures are reported through the `status` discriminant.
570899
+ */
570900
+ async materializeCommitPatch(sha) {
570901
+ let parents;
570902
+ let filesOutput;
570903
+ try {
570904
+ parents = await this.git.getCommitParents(sha);
570905
+ filesOutput = await this.git.exec(["diff-tree", "--no-commit-id", "--name-only", "-r", sha]);
570906
+ } catch {
570907
+ return { status: "unreachable", sensitiveFiles: [], fernignoredFiles: [] };
570908
+ }
570909
+ const { files, sensitiveFiles, fernignoredFiles, infrastructureFiles } = filterInfrastructureAndSensitiveFiles(filesOutput, this.fernignorePatterns);
570910
+ if (files.length === 0) {
570911
+ return { status: "empty", sensitiveFiles, fernignoredFiles };
570912
+ }
570913
+ const wasFiltered = sensitiveFiles.length > 0 || fernignoredFiles.length > 0 || infrastructureFiles.length > 0;
570914
+ const parentSha = parents[0];
570915
+ let patchContent;
570916
+ if (wasFiltered && parentSha) {
570917
+ try {
570918
+ patchContent = await this.git.exec(["diff", parentSha, sha, "--", ...files]);
570919
+ } catch {
570920
+ return { status: "empty", sensitiveFiles, fernignoredFiles };
570921
+ }
570922
+ if (!patchContent.trim()) {
570923
+ return { status: "empty", sensitiveFiles, fernignoredFiles };
570924
+ }
570925
+ } else {
570926
+ try {
570927
+ patchContent = await this.git.formatPatch(sha);
570928
+ } catch {
570929
+ return { status: "unreachable", sensitiveFiles, fernignoredFiles };
570930
+ }
570931
+ }
570932
+ return {
570933
+ status: "ok",
570934
+ files,
570935
+ sensitiveFiles,
570936
+ fernignoredFiles,
570937
+ patchContent,
570938
+ contentHash: this.computeContentHash(patchContent)
570939
+ };
570940
+ }
570874
570941
  /**
570875
570942
  * Compute content hash for deduplication.
570876
570943
  *
@@ -570975,6 +571042,13 @@ var require_dist24 = __commonJS({
570975
571042
  async detectPatchesViaTreeDiff(lastGen, commitKnownMissing) {
570976
571043
  const diffBase = await this.resolveDiffBase(lastGen, commitKnownMissing);
570977
571044
  if (!diffBase) {
571045
+ this.warnings.push(
571046
+ `Neither the previous generation's commit (${lastGen.commit_sha.slice(0, 7)}) nor its recorded tree was reachable during this run. Falling back to a bounded commit scan, which cannot see customizations older than the clone window. Widen the clone window (e.g. \`git fetch --unshallow\`) and re-run replay to verify customizations were carried forward.`
571047
+ );
571048
+ this.degradedReasons.push({
571049
+ code: "generation-tree-unreachable",
571050
+ message: `The previous generation (${lastGen.commit_sha.slice(0, 7)}) was not reachable during this run \u2014 neither its commit nor its recorded tree. Customizations may not have been carried forward \u2014 review the result before merging, or widen the clone window and re-run replay.`
571051
+ });
570978
571052
  return this.detectPatchesViaCommitScan();
570979
571053
  }
570980
571054
  const lockForGuard = this.lockManager.read();
@@ -571087,10 +571161,7 @@ var require_dist24 = __commonJS({
571087
571161
  continue;
571088
571162
  }
571089
571163
  const filesOutput = await this.git.exec(["diff-tree", "--no-commit-id", "--name-only", "-r", commit.sha]);
571090
- const { files, sensitiveFiles, fernignoredFiles } = filterInfrastructureAndSensitiveFiles(
571091
- filesOutput,
571092
- this.fernignorePatterns
571093
- );
571164
+ const { files, sensitiveFiles, fernignoredFiles, infrastructureFiles } = filterInfrastructureAndSensitiveFiles(filesOutput, this.fernignorePatterns);
571094
571165
  if (sensitiveFiles.length > 0) {
571095
571166
  this.warnings.push(
571096
571167
  `Sensitive file(s) excluded from patch for commit ${commit.sha.slice(0, 7)}: ${sensitiveFiles.join(", ")}. These files will not be tracked by replay.`
@@ -571101,7 +571172,8 @@ var require_dist24 = __commonJS({
571101
571172
  `.fernignore-protected file(s) excluded from patch for commit ${commit.sha.slice(0, 7)}: ${fernignoredFiles.join(", ")}. These files are managed by .fernignore, not Replay.`
571102
571173
  );
571103
571174
  }
571104
- if ((sensitiveFiles.length > 0 || fernignoredFiles.length > 0) && files.length > 0) {
571175
+ const wasFiltered = sensitiveFiles.length > 0 || fernignoredFiles.length > 0 || infrastructureFiles.length > 0;
571176
+ if (wasFiltered && files.length > 0) {
571105
571177
  if (parents.length === 0) {
571106
571178
  continue;
571107
571179
  }
@@ -572351,14 +572423,21 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
572351
572423
  return this.git.getTreeHash(commitSha);
572352
572424
  }
572353
572425
  };
572354
- var import_node_fs210 = require("fs");
572426
+ var import_node_fs36 = require("fs");
572355
572427
  var import_promises610 = require("fs/promises");
572356
- var import_node_path82 = require("path");
572428
+ var import_node_path92 = require("path");
572357
572429
  var import_minimatch52 = require_commonjs3();
572358
572430
  init_GitClient();
572431
+ var import_node_fs210 = require("fs");
572432
+ var import_node_path62 = require("path");
572433
+ function readFernignorePatterns(outputDir) {
572434
+ const fernignorePath = (0, import_node_path62.join)(outputDir, ".fernignore");
572435
+ if (!(0, import_node_fs210.existsSync)(fernignorePath)) return [];
572436
+ return (0, import_node_fs210.readFileSync)(fernignorePath, "utf-8").split("\n").map((line) => line.trim()).filter((line) => line && !line.startsWith("#"));
572437
+ }
572359
572438
  var import_promises410 = require("fs/promises");
572360
572439
  var import_node_os22 = require("os");
572361
- var import_node_path62 = require("path");
572440
+ var import_node_path72 = require("path");
572362
572441
  var import_node_child_process15 = require("child_process");
572363
572442
  init_HybridReconstruction();
572364
572443
  function normalizeHunkBody(hunk) {
@@ -572576,10 +572655,10 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
572576
572655
  }
572577
572656
  async function unifiedDiff(beforeContent, afterContent, filePath) {
572578
572657
  if (beforeContent === afterContent) return "";
572579
- const tmp20 = await (0, import_promises410.mkdtemp)((0, import_node_path62.join)((0, import_node_os22.tmpdir)(), "fern-replay-pp-"));
572658
+ const tmp20 = await (0, import_promises410.mkdtemp)((0, import_node_path72.join)((0, import_node_os22.tmpdir)(), "fern-replay-pp-"));
572580
572659
  try {
572581
- const beforePath = (0, import_node_path62.join)(tmp20, "before");
572582
- const afterPath = (0, import_node_path62.join)(tmp20, "after");
572660
+ const beforePath = (0, import_node_path72.join)(tmp20, "before");
572661
+ const afterPath = (0, import_node_path72.join)(tmp20, "after");
572583
572662
  await (0, import_promises410.writeFile)(beforePath, beforeContent, "utf-8");
572584
572663
  await (0, import_promises410.writeFile)(afterPath, afterContent, "utf-8");
572585
572664
  const raw = await runGitDiffNoIndex(beforePath, afterPath);
@@ -572751,6 +572830,7 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
572751
572830
  patchesToApply: [],
572752
572831
  newPatchIds: /* @__PURE__ */ new Set(),
572753
572832
  detectorWarnings: [],
572833
+ detectorDegraded: [],
572754
572834
  revertedCount: 0,
572755
572835
  genSha: genRecord.commit_sha,
572756
572836
  optionsSnapshot: options2
@@ -572808,6 +572888,7 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
572808
572888
  patchesToApply: [],
572809
572889
  newPatchIds: /* @__PURE__ */ new Set(),
572810
572890
  detectorWarnings: [],
572891
+ detectorDegraded: [],
572811
572892
  revertedCount: 0,
572812
572893
  genSha: genRecord.commit_sha,
572813
572894
  optionsSnapshot: options2
@@ -572843,8 +572924,10 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
572843
572924
  const previousGenerationSha = preLock.current_generation ?? null;
572844
572925
  let { patches: newPatches, revertedPatchIds } = await this.service.detector.detectNewPatches();
572845
572926
  const detectorWarnings = [...this.service.detector.warnings];
572927
+ const detectorDegraded = [...this.service.detector.degradedReasons];
572846
572928
  if (options2?.dryRun) {
572847
572929
  const dryRunWarnings = [...detectorWarnings, ...this.service.warnings];
572930
+ const dryRunDegraded = dedupeDegradedReasons([...detectorDegraded, ...this.service.degradedReasons]);
572848
572931
  const preparedReport = {
572849
572932
  flow: "no-patches",
572850
572933
  patchesDetected: newPatches.length,
@@ -572854,7 +572937,9 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
572854
572937
  patchesReverted: revertedPatchIds.length,
572855
572938
  conflicts: [],
572856
572939
  wouldApply: newPatches,
572857
- warnings: dryRunWarnings.length > 0 ? dryRunWarnings : void 0
572940
+ warnings: dryRunWarnings.length > 0 ? dryRunWarnings : void 0,
572941
+ degraded: dryRunDegraded.length > 0 ? true : void 0,
572942
+ degradedReasons: dryRunDegraded.length > 0 ? dryRunDegraded : void 0
572858
572943
  };
572859
572944
  const headSha = await this.service.git.exec(["rev-parse", "HEAD"]).then((s5) => s5.trim()).catch(() => "");
572860
572945
  return {
@@ -572908,6 +572993,7 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
572908
572993
  patchesToApply: newPatches,
572909
572994
  newPatchIds: new Set(newPatches.map((p6) => p6.id)),
572910
572995
  detectorWarnings,
572996
+ detectorDegraded,
572911
572997
  revertedCount: revertedPatchIds.length,
572912
572998
  genSha: genRecord.commit_sha,
572913
572999
  optionsSnapshot: options2
@@ -572951,6 +573037,7 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
572951
573037
  }
572952
573038
  }
572953
573039
  const warnings = [...detectorWarnings, ...this.service.warnings];
573040
+ const degradedReasons = [...prep._prepared.detectorDegraded, ...this.service.degradedReasons];
572954
573041
  return this.service.buildReport(
572955
573042
  "no-patches",
572956
573043
  newPatches,
@@ -572959,7 +573046,8 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
572959
573046
  warnings,
572960
573047
  rebaseCounts,
572961
573048
  void 0,
572962
- prep._prepared.revertedCount
573049
+ prep._prepared.revertedCount,
573050
+ degradedReasons
572963
573051
  );
572964
573052
  }
572965
573053
  };
@@ -572974,6 +573062,10 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
572974
573062
  const existingPatches2 = this.service.lockManager.getPatches();
572975
573063
  const { patches: newPatches2, revertedPatchIds: dryRunReverted } = await this.service.detector.detectNewPatches();
572976
573064
  const warnings = [...this.service.detector.warnings, ...this.service.warnings];
573065
+ const dryRunDegraded = dedupeDegradedReasons([
573066
+ ...this.service.detector.degradedReasons,
573067
+ ...this.service.degradedReasons
573068
+ ]);
572977
573069
  const allPatches2 = [...existingPatches2, ...newPatches2];
572978
573070
  const preparedReport = {
572979
573071
  flow: "normal-regeneration",
@@ -572984,7 +573076,9 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
572984
573076
  patchesReverted: dryRunReverted.length,
572985
573077
  conflicts: [],
572986
573078
  wouldApply: allPatches2,
572987
- warnings: warnings.length > 0 ? warnings : void 0
573079
+ warnings: warnings.length > 0 ? warnings : void 0,
573080
+ degraded: dryRunDegraded.length > 0 ? true : void 0,
573081
+ degradedReasons: dryRunDegraded.length > 0 ? dryRunDegraded : void 0
572988
573082
  };
572989
573083
  const headSha = await this.service.git.exec(["rev-parse", "HEAD"]).then((s5) => s5.trim()).catch(() => "");
572990
573084
  return {
@@ -573023,6 +573117,7 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
573023
573117
  existingPatches = this.service.lockManager.getPatches();
573024
573118
  let { patches: newPatches, revertedPatchIds } = await this.service.detector.detectNewPatches();
573025
573119
  const detectorWarnings = [...this.service.detector.warnings];
573120
+ const detectorDegraded = [...this.service.detector.degradedReasons];
573026
573121
  if (removedByPreRebase.length > 0) {
573027
573122
  const removedOriginalCommits = new Set(removedByPreRebase.map((p6) => p6.original_commit));
573028
573123
  const removedOriginalMessages = new Set(removedByPreRebase.map((p6) => p6.original_message));
@@ -573063,6 +573158,7 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
573063
573158
  newPatchIds: new Set(newPatches.map((p6) => p6.id)),
573064
573159
  preRebaseCounts,
573065
573160
  detectorWarnings,
573161
+ detectorDegraded,
573066
573162
  revertedCount: revertedPatchIds.length,
573067
573163
  genSha: genRecord.commit_sha,
573068
573164
  optionsSnapshot: options2
@@ -573111,6 +573207,7 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
573111
573207
  await this.service.committer.commitReplay(appliedCount, allPatches, void 0, { buckets });
573112
573208
  }
573113
573209
  const warnings = [...detectorWarnings, ...this.service.warnings];
573210
+ const degradedReasons = [...prep._prepared.detectorDegraded, ...this.service.degradedReasons];
573114
573211
  return this.service.buildReport(
573115
573212
  "normal-regeneration",
573116
573213
  allPatches,
@@ -573119,7 +573216,8 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
573119
573216
  warnings,
573120
573217
  rebaseCounts,
573121
573218
  prep._prepared.preRebaseCounts,
573122
- prep._prepared.revertedCount
573219
+ prep._prepared.revertedCount,
573220
+ degradedReasons
573123
573221
  );
573124
573222
  }
573125
573223
  };
@@ -573157,6 +573255,16 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
573157
573255
  * @internal Used by Flow implementations in `src/flows/`.
573158
573256
  */
573159
573257
  warnings = [];
573258
+ /**
573259
+ * Service-level structured degraded reasons, mirroring `warnings`.
573260
+ * Populated at apply time (e.g. `recordUnreachableBaseWarnings` when a
573261
+ * patch's base-generation tree is unreachable). Merged with
573262
+ * `detector.degradedReasons` into `ReplayReport.degradedReasons` by each
573263
+ * flow handler. Cleared at the top of `prepareReplay()`.
573264
+ *
573265
+ * @internal Used by Flow implementations in `src/flows/`.
573266
+ */
573267
+ degradedReasons = [];
573160
573268
  /**
573161
573269
  * @internal Test-only accessor.
573162
573270
  * Returns the ReplayApplicator instance used for the last run. Tests use this
@@ -573200,7 +573308,10 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
573200
573308
  async prepareReplay(options2) {
573201
573309
  this.warnings = [];
573202
573310
  this.detector.warnings.length = 0;
573311
+ this.degradedReasons = [];
573312
+ this.detector.degradedReasons.length = 0;
573203
573313
  this.cleanseLegacyFernignoreEntries();
573314
+ this.cleanseInfrastructureFileEntries();
573204
573315
  return this.selectFlow(options2).prepare(options2);
573205
573316
  }
573206
573317
  /**
@@ -573254,6 +573365,50 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
573254
573365
  this.lockManager.save();
573255
573366
  }
573256
573367
  }
573368
+ /**
573369
+ * Migration step: strips infrastructure file diffs (`.fernignore`, `.fern/*`)
573370
+ * from existing patches' `patch_content`. These diffs were bundled by older
573371
+ * detection logic that did not scope `git diff` when only infrastructure files
573372
+ * were stripped. Patches that end up with no surviving diff sections are
573373
+ * removed entirely.
573374
+ *
573375
+ * Idempotent: a second run is a no-op because the first run already removed
573376
+ * all infrastructure sections. Per-patch try/catch ensures isolation.
573377
+ */
573378
+ cleanseInfrastructureFileEntries() {
573379
+ if (!this.lockManager.exists()) return;
573380
+ this.lockManager.read();
573381
+ let lockfileMutated = false;
573382
+ const patches = this.lockManager.getPatches();
573383
+ for (const patch5 of patches) {
573384
+ try {
573385
+ const result = computeInfrastructureFileCleanse(
573386
+ patch5,
573387
+ (content5) => this.detector.computeContentHash(content5)
573388
+ );
573389
+ if (result.unchanged) continue;
573390
+ if (result.remove) {
573391
+ this.lockManager.removePatch(patch5.id);
573392
+ this.warnings.push(
573393
+ `Stripped infrastructure file diffs from patch ${patch5.id}: patch contained only infrastructure file changes (${result.strippedPaths.join(", ")}) \u2014 removed.`
573394
+ );
573395
+ } else {
573396
+ this.lockManager.updatePatch(patch5.id, {
573397
+ patch_content: result.patch_content,
573398
+ content_hash: result.content_hash
573399
+ });
573400
+ this.warnings.push(
573401
+ `Stripped infrastructure file diffs from patch ${patch5.id}: removed bundled diffs for ${result.strippedPaths.join(", ")}.`
573402
+ );
573403
+ }
573404
+ lockfileMutated = true;
573405
+ } catch {
573406
+ }
573407
+ }
573408
+ if (lockfileMutated) {
573409
+ this.lockManager.save();
573410
+ }
573411
+ }
573257
573412
  /**
573258
573413
  * Phase 2 of the two-phase replay flow. Applies patches, rebases them against
573259
573414
  * the generation, saves the lockfile, and commits `[fern-replay]` (or stages
@@ -573509,7 +573664,7 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
573509
573664
  for (const file4 of files) {
573510
573665
  if (isBinaryFile(file4)) continue;
573511
573666
  try {
573512
- const content5 = await (0, import_promises610.readFile)((0, import_node_path82.join)(this.outputDir, file4), "utf-8");
573667
+ const content5 = await (0, import_promises610.readFile)((0, import_node_path92.join)(this.outputDir, file4), "utf-8");
573513
573668
  snapshot[file4] = content5;
573514
573669
  } catch {
573515
573670
  }
@@ -574026,6 +574181,12 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
574026
574181
  );
574027
574182
  }
574028
574183
  }
574184
+ if (warned2.size > 0) {
574185
+ this.degradedReasons.push({
574186
+ code: "patch-base-unreachable",
574187
+ message: `${warned2.size} customization file(s) could not be checked against the generation they were made on because that generation's tree was not reachable during this run. Customizations may not have been carried forward \u2014 review the result before merging, or widen the clone window and re-run replay.`
574188
+ });
574189
+ }
574029
574190
  }
574030
574191
  /**
574031
574192
  * After applyPatches(), strip conflict markers from conflicting files
@@ -574037,11 +574198,11 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
574037
574198
  if (result.status !== "conflict" || !result.fileResults) continue;
574038
574199
  for (const fileResult of result.fileResults) {
574039
574200
  if (fileResult.status !== "conflict") continue;
574040
- const filePath = (0, import_node_path82.join)(this.outputDir, fileResult.file);
574201
+ const filePath = (0, import_node_path92.join)(this.outputDir, fileResult.file);
574041
574202
  try {
574042
- const content5 = (0, import_node_fs210.readFileSync)(filePath, "utf-8");
574203
+ const content5 = (0, import_node_fs36.readFileSync)(filePath, "utf-8");
574043
574204
  const stripped = stripConflictMarkers(content5);
574044
- (0, import_node_fs210.writeFileSync)(filePath, stripped);
574205
+ (0, import_node_fs36.writeFileSync)(filePath, stripped);
574045
574206
  } catch {
574046
574207
  }
574047
574208
  }
@@ -574069,12 +574230,11 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
574069
574230
  }
574070
574231
  /** @internal Used by Flow implementations in `src/flows/`. */
574071
574232
  readFernignorePatterns() {
574072
- const fernignorePath = (0, import_node_path82.join)(this.outputDir, ".fernignore");
574073
- if (!(0, import_node_fs210.existsSync)(fernignorePath)) return [];
574074
- return (0, import_node_fs210.readFileSync)(fernignorePath, "utf-8").split("\n").map((line) => line.trim()).filter((line) => line && !line.startsWith("#"));
574233
+ return readFernignorePatterns(this.outputDir);
574075
574234
  }
574076
574235
  /** @internal Used by Flow implementations in `src/flows/`. */
574077
- buildReport(flow4, patches, results, options2, warnings, rebaseCounts, preRebaseCounts, patchesReverted) {
574236
+ buildReport(flow4, patches, results, options2, warnings, rebaseCounts, preRebaseCounts, patchesReverted, degradedReasons) {
574237
+ const degraded = dedupeDegradedReasons(degradedReasons ?? []);
574078
574238
  const conflictResults = results.filter((r4) => r4.status === "conflict");
574079
574239
  const conflictDetails = conflictResults.map((r4) => {
574080
574240
  const conflictFiles = r4.fileResults?.filter((f3) => f3.status === "conflict") ?? [];
@@ -574114,10 +574274,23 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
574114
574274
  conflictDetails: r4.fileResults?.filter((f3) => f3.status === "conflict") ?? []
574115
574275
  })) : void 0,
574116
574276
  wouldApply: options2?.dryRun ? patches : void 0,
574117
- warnings: warnings && warnings.length > 0 ? warnings : void 0
574277
+ warnings: warnings && warnings.length > 0 ? warnings : void 0,
574278
+ degraded: degraded.length > 0 ? true : void 0,
574279
+ degradedReasons: degraded.length > 0 ? degraded : void 0
574118
574280
  };
574119
574281
  }
574120
574282
  };
574283
+ function dedupeDegradedReasons(reasons) {
574284
+ const seen = /* @__PURE__ */ new Set();
574285
+ const out = [];
574286
+ for (const reason of reasons) {
574287
+ const key2 = `${reason.code}\0${reason.message}`;
574288
+ if (seen.has(key2)) continue;
574289
+ seen.add(key2);
574290
+ out.push(reason);
574291
+ }
574292
+ return out;
574293
+ }
574121
574294
  function computeCommitBuckets(results, absorbedPatchIds, patchesPassedToCommit) {
574122
574295
  const resultByPatchId = /* @__PURE__ */ new Map();
574123
574296
  for (const r4 of results) resultByPatchId.set(r4.patch.id, r4);
@@ -574166,7 +574339,7 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
574166
574339
  }
574167
574340
  }
574168
574341
  }
574169
- const { stripped: patchContentStripped, content: newPatchContent, strippedSectionPaths } = stripFernignoreDiffSections(patch5.patch_content, matchesFernignore);
574342
+ const { stripped: patchContentStripped, content: newPatchContent, strippedSectionPaths } = stripDiffSectionsByPredicate(patch5.patch_content, matchesFernignore);
574170
574343
  const unchanged = strippedFromFiles.length === 0 && !snapshotChanged && !patchContentStripped;
574171
574344
  if (unchanged) {
574172
574345
  return {
@@ -574201,7 +574374,38 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
574201
574374
  strippedPaths
574202
574375
  };
574203
574376
  }
574204
- function stripFernignoreDiffSections(patchContent, matchesFernignore) {
574377
+ function isInfrastructureFile(filePath) {
574378
+ return filePath === ".fernignore" || filePath.startsWith(".fern/");
574379
+ }
574380
+ function computeInfrastructureFileCleanse(patch5, computeContentHash2) {
574381
+ const { stripped, content: newPatchContent, strippedSectionPaths } = stripDiffSectionsByPredicate(patch5.patch_content, isInfrastructureFile);
574382
+ if (!stripped) {
574383
+ return {
574384
+ unchanged: true,
574385
+ remove: false,
574386
+ patch_content: patch5.patch_content,
574387
+ content_hash: patch5.content_hash,
574388
+ strippedPaths: []
574389
+ };
574390
+ }
574391
+ if (newPatchContent.trim() === "" || patch5.files.length === 0) {
574392
+ return {
574393
+ unchanged: false,
574394
+ remove: true,
574395
+ patch_content: "",
574396
+ content_hash: "",
574397
+ strippedPaths: strippedSectionPaths
574398
+ };
574399
+ }
574400
+ return {
574401
+ unchanged: false,
574402
+ remove: false,
574403
+ patch_content: newPatchContent,
574404
+ content_hash: computeContentHash2(newPatchContent),
574405
+ strippedPaths: strippedSectionPaths
574406
+ };
574407
+ }
574408
+ function stripDiffSectionsByPredicate(patchContent, shouldStrip) {
574205
574409
  const lines = patchContent.split("\n");
574206
574410
  const sectionStarts = [];
574207
574411
  for (let i5 = 0; i5 < lines.length; i5++) {
@@ -574223,7 +574427,7 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
574223
574427
  const section = sectionStarts[i5];
574224
574428
  const next2 = sectionStarts[i5 + 1];
574225
574429
  const endIdx = next2 ? next2.idx : lines.length;
574226
- if (section.path !== null && matchesFernignore(section.path)) {
574430
+ if (section.path !== null && shouldStrip(section.path)) {
574227
574431
  stripped = true;
574228
574432
  strippedSectionPaths.push(section.path);
574229
574433
  continue;
@@ -574233,8 +574437,8 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
574233
574437
  return { stripped, content: newLines.join("\n"), strippedSectionPaths };
574234
574438
  }
574235
574439
  var import_node_crypto22 = require("crypto");
574236
- var import_node_fs36 = require("fs");
574237
- var import_node_path92 = require("path");
574440
+ var import_node_fs42 = require("fs");
574441
+ var import_node_path102 = require("path");
574238
574442
  var import_minimatch62 = require_commonjs3();
574239
574443
  var import_yaml22 = require_dist11();
574240
574444
  var FernignoreMigrator = class {
@@ -574247,14 +574451,14 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
574247
574451
  this.outputDir = outputDir;
574248
574452
  }
574249
574453
  fernignoreExists() {
574250
- return (0, import_node_fs36.existsSync)((0, import_node_path92.join)(this.outputDir, ".fernignore"));
574454
+ return (0, import_node_fs42.existsSync)((0, import_node_path102.join)(this.outputDir, ".fernignore"));
574251
574455
  }
574252
574456
  readFernignorePatterns() {
574253
- const fernignorePath = (0, import_node_path92.join)(this.outputDir, ".fernignore");
574254
- if (!(0, import_node_fs36.existsSync)(fernignorePath)) {
574457
+ const fernignorePath = (0, import_node_path102.join)(this.outputDir, ".fernignore");
574458
+ if (!(0, import_node_fs42.existsSync)(fernignorePath)) {
574255
574459
  return [];
574256
574460
  }
574257
- const content5 = (0, import_node_fs36.readFileSync)(fernignorePath, "utf-8");
574461
+ const content5 = (0, import_node_fs42.readFileSync)(fernignorePath, "utf-8");
574258
574462
  return content5.split("\n").map((line) => line.trim()).filter((line) => line && !line.startsWith("#"));
574259
574463
  }
574260
574464
  /** Analyze .fernignore patterns vs git history. Creates synthetic patches for files differing from pristine generation. */
@@ -574359,16 +574563,16 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
574359
574563
  return results;
574360
574564
  }
574361
574565
  readFileContent(filePath) {
574362
- const fullPath = (0, import_node_path92.join)(this.outputDir, filePath);
574363
- if (!(0, import_node_fs36.existsSync)(fullPath)) {
574566
+ const fullPath = (0, import_node_path102.join)(this.outputDir, filePath);
574567
+ if (!(0, import_node_fs42.existsSync)(fullPath)) {
574364
574568
  return null;
574365
574569
  }
574366
574570
  try {
574367
- const stat7 = (0, import_node_fs36.statSync)(fullPath);
574571
+ const stat7 = (0, import_node_fs42.statSync)(fullPath);
574368
574572
  if (stat7.isDirectory()) {
574369
574573
  return null;
574370
574574
  }
574371
- return (0, import_node_fs36.readFileSync)(fullPath, "utf-8");
574575
+ return (0, import_node_fs42.readFileSync)(fullPath, "utf-8");
574372
574576
  } catch {
574373
574577
  return null;
574374
574578
  }
@@ -574424,25 +574628,25 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
574424
574628
  };
574425
574629
  }
574426
574630
  movePatternsToReplayYml(patterns) {
574427
- const replayYmlPath = (0, import_node_path92.join)(this.outputDir, ".fern", "replay.yml");
574631
+ const replayYmlPath = (0, import_node_path102.join)(this.outputDir, ".fern", "replay.yml");
574428
574632
  let config5 = {};
574429
- if ((0, import_node_fs36.existsSync)(replayYmlPath)) {
574430
- const content5 = (0, import_node_fs36.readFileSync)(replayYmlPath, "utf-8");
574633
+ if ((0, import_node_fs42.existsSync)(replayYmlPath)) {
574634
+ const content5 = (0, import_node_fs42.readFileSync)(replayYmlPath, "utf-8");
574431
574635
  config5 = (0, import_yaml22.parse)(content5) ?? {};
574432
574636
  }
574433
574637
  const existing = config5.exclude ?? [];
574434
574638
  const merged = [.../* @__PURE__ */ new Set([...existing, ...patterns])];
574435
574639
  config5.exclude = merged;
574436
- const dir = (0, import_node_path92.dirname)(replayYmlPath);
574437
- if (!(0, import_node_fs36.existsSync)(dir)) {
574438
- (0, import_node_fs36.mkdirSync)(dir, { recursive: true });
574640
+ const dir = (0, import_node_path102.dirname)(replayYmlPath);
574641
+ if (!(0, import_node_fs42.existsSync)(dir)) {
574642
+ (0, import_node_fs42.mkdirSync)(dir, { recursive: true });
574439
574643
  }
574440
- (0, import_node_fs36.writeFileSync)(replayYmlPath, (0, import_yaml22.stringify)(config5, { lineWidth: 0 }), "utf-8");
574644
+ (0, import_node_fs42.writeFileSync)(replayYmlPath, (0, import_yaml22.stringify)(config5, { lineWidth: 0 }), "utf-8");
574441
574645
  }
574442
574646
  };
574443
574647
  var import_node_crypto32 = require("crypto");
574444
- var import_node_fs42 = require("fs");
574445
- var import_node_path102 = require("path");
574648
+ var import_node_fs52 = require("fs");
574649
+ var import_node_path112 = require("path");
574446
574650
  init_GitClient();
574447
574651
  async function bootstrap(outputDir, options2) {
574448
574652
  const git = new GitClient(outputDir);
@@ -574609,10 +574813,10 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
574609
574813
  }
574610
574814
  var REPLAY_FERNIGNORE_ENTRIES = [".fern/replay.lock", ".fern/replay.yml", ".gitattributes"];
574611
574815
  function ensureFernignoreEntries(outputDir) {
574612
- const fernignorePath = (0, import_node_path102.join)(outputDir, ".fernignore");
574816
+ const fernignorePath = (0, import_node_path112.join)(outputDir, ".fernignore");
574613
574817
  let content5 = "";
574614
- if ((0, import_node_fs42.existsSync)(fernignorePath)) {
574615
- content5 = (0, import_node_fs42.readFileSync)(fernignorePath, "utf-8");
574818
+ if ((0, import_node_fs52.existsSync)(fernignorePath)) {
574819
+ content5 = (0, import_node_fs52.readFileSync)(fernignorePath, "utf-8");
574616
574820
  }
574617
574821
  const lines = content5.split("\n");
574618
574822
  const toAdd = [];
@@ -574628,15 +574832,15 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
574628
574832
  content5 += "\n";
574629
574833
  }
574630
574834
  content5 += toAdd.join("\n") + "\n";
574631
- (0, import_node_fs42.writeFileSync)(fernignorePath, content5, "utf-8");
574835
+ (0, import_node_fs52.writeFileSync)(fernignorePath, content5, "utf-8");
574632
574836
  return true;
574633
574837
  }
574634
574838
  var GITATTRIBUTES_ENTRIES = [".fern/replay.lock linguist-generated=true"];
574635
574839
  function ensureGitattributesEntries(outputDir) {
574636
- const gitattributesPath = (0, import_node_path102.join)(outputDir, ".gitattributes");
574840
+ const gitattributesPath = (0, import_node_path112.join)(outputDir, ".gitattributes");
574637
574841
  let content5 = "";
574638
- if ((0, import_node_fs42.existsSync)(gitattributesPath)) {
574639
- content5 = (0, import_node_fs42.readFileSync)(gitattributesPath, "utf-8");
574842
+ if ((0, import_node_fs52.existsSync)(gitattributesPath)) {
574843
+ content5 = (0, import_node_fs52.readFileSync)(gitattributesPath, "utf-8");
574640
574844
  }
574641
574845
  const lines = content5.split("\n");
574642
574846
  const toAdd = [];
@@ -574652,7 +574856,7 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
574652
574856
  content5 += "\n";
574653
574857
  }
574654
574858
  content5 += toAdd.join("\n") + "\n";
574655
- (0, import_node_fs42.writeFileSync)(gitattributesPath, content5, "utf-8");
574859
+ (0, import_node_fs52.writeFileSync)(gitattributesPath, content5, "utf-8");
574656
574860
  }
574657
574861
  function computeContentHash(patchContent) {
574658
574862
  const lines = patchContent.split("\n");
@@ -574662,6 +574866,7 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
574662
574866
  return `sha256:${(0, import_node_crypto32.createHash)("sha256").update(normalized).digest("hex")}`;
574663
574867
  }
574664
574868
  var import_minimatch72 = require_commonjs3();
574869
+ init_GitClient();
574665
574870
  function parseDiffStat(patchContent) {
574666
574871
  let additions = 0;
574667
574872
  let deletions = 0;
@@ -574720,7 +574925,28 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
574720
574925
  totalPatches: 0,
574721
574926
  warnings: []
574722
574927
  };
574723
- function forget(outputDir, options2) {
574928
+ async function dismissPatches(outputDir, lockManager, patches) {
574929
+ const git = new GitClient(outputDir);
574930
+ const detector = new ReplayDetector(git, lockManager, outputDir, readFernignorePatterns(outputDir));
574931
+ for (const patch5 of patches) {
574932
+ const hashes = [patch5.content_hash];
574933
+ if (patch5.original_commit) {
574934
+ const materialized = await detector.materializeCommitPatch(patch5.original_commit);
574935
+ if (materialized.status === "ok" && !hashes.includes(materialized.contentHash)) {
574936
+ hashes.push(materialized.contentHash);
574937
+ }
574938
+ }
574939
+ lockManager.addDismissal({
574940
+ patch_id: patch5.id,
574941
+ original_commit: patch5.original_commit,
574942
+ original_message: patch5.original_message,
574943
+ content_hashes: hashes,
574944
+ dismissed_at: (/* @__PURE__ */ new Date()).toISOString(),
574945
+ via: "forget"
574946
+ });
574947
+ }
574948
+ }
574949
+ async function forget(outputDir, options2) {
574724
574950
  const lockManager = new LockfileManager(outputDir);
574725
574951
  if (!lockManager.exists()) {
574726
574952
  return { ...EMPTY_RESULT };
@@ -574731,9 +574957,7 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
574731
574957
  const removed = lock.patches.map(toMatchedPatch);
574732
574958
  const warnings = buildWarnings(lock.patches);
574733
574959
  if (!options2.dryRun) {
574734
- for (const patch5 of lock.patches) {
574735
- lockManager.addForgottenHash(patch5.content_hash);
574736
- }
574960
+ await dismissPatches(outputDir, lockManager, lock.patches);
574737
574961
  lockManager.clearPatches();
574738
574962
  lockManager.save();
574739
574963
  }
@@ -574762,8 +574986,8 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
574762
574986
  }
574763
574987
  const warnings = buildWarnings(patchesToRemove);
574764
574988
  if (!options2.dryRun) {
574989
+ await dismissPatches(outputDir, lockManager, patchesToRemove);
574765
574990
  for (const patch5 of patchesToRemove) {
574766
- lockManager.addForgottenHash(patch5.content_hash);
574767
574991
  lockManager.removePatch(patch5.id);
574768
574992
  }
574769
574993
  lockManager.save();
@@ -574802,7 +575026,7 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
574802
575026
  matched: lock.patches.map(toMatchedPatch)
574803
575027
  };
574804
575028
  }
574805
- var import_node_fs52 = require("fs");
575029
+ var import_node_fs62 = require("fs");
574806
575030
  function reset2(outputDir, options2) {
574807
575031
  const lockManager = new LockfileManager(outputDir);
574808
575032
  if (!lockManager.exists()) {
@@ -574816,7 +575040,7 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
574816
575040
  const lock = lockManager.read();
574817
575041
  const patchCount = lock.patches.length;
574818
575042
  if (!options2?.dryRun) {
574819
- (0, import_node_fs52.unlinkSync)(lockManager.lockfilePath);
575043
+ (0, import_node_fs62.unlinkSync)(lockManager.lockfilePath);
574820
575044
  }
574821
575045
  return {
574822
575046
  success: true,
@@ -574826,7 +575050,7 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
574826
575050
  };
574827
575051
  }
574828
575052
  var import_promises710 = require("fs/promises");
574829
- var import_node_path112 = require("path");
575053
+ var import_node_path122 = require("path");
574830
575054
  init_GitClient();
574831
575055
  async function resolve15(outputDir, options2) {
574832
575056
  const lockManager = new LockfileManager(outputDir);
@@ -574867,15 +575091,16 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
574867
575091
  const patchesToCommit = [...resolvingPatches, ...unresolvedPatches];
574868
575092
  if (patchesToCommit.length > 0) {
574869
575093
  const currentGen = lock.current_generation;
574870
- const detector = new ReplayDetector(git, lockManager, outputDir);
575094
+ const detector = new ReplayDetector(git, lockManager, outputDir, readFernignorePatterns(outputDir));
574871
575095
  const warnings = [];
575096
+ const patchesDismissed = [];
574872
575097
  let patchesResolved = 0;
574873
575098
  const pass1 = [];
574874
575099
  for (const patch5 of patchesToCommit) {
574875
575100
  const result = await computePerPatchDiffWithFallback(
574876
575101
  patch5,
574877
575102
  (f3) => git.showFile(currentGen, f3),
574878
- (f3) => (0, import_promises710.readFile)((0, import_node_path112.join)(outputDir, f3), "utf-8").catch(() => null),
575103
+ (f3) => (0, import_promises710.readFile)((0, import_node_path122.join)(outputDir, f3), "utf-8").catch(() => null),
574879
575104
  (files) => git.exec(["diff", currentGen, "--", ...files]).catch(() => null)
574880
575105
  );
574881
575106
  if (result === null) {
@@ -574909,7 +575134,27 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
574909
575134
  const r4 = pass1[i5];
574910
575135
  if (r4.kind === "skip") continue;
574911
575136
  if (r4.kind === "revert") {
575137
+ const hashes = [patch5.content_hash];
575138
+ if (patch5.original_commit) {
575139
+ const materialized = await detector.materializeCommitPatch(patch5.original_commit);
575140
+ if (materialized.status === "ok" && !hashes.includes(materialized.contentHash)) {
575141
+ hashes.push(materialized.contentHash);
575142
+ } else if (materialized.status === "unreachable") {
575143
+ warnings.push(
575144
+ `Patch ${patch5.id}: original commit ${patch5.original_commit.slice(0, 7)} is unreachable; dismissed using the stored content hash only. If this commit reappears in history, the customization may be re-detected once.`
575145
+ );
575146
+ }
575147
+ }
575148
+ lockManager.addDismissal({
575149
+ patch_id: patch5.id,
575150
+ original_commit: patch5.original_commit,
575151
+ original_message: patch5.original_message,
575152
+ content_hashes: hashes,
575153
+ dismissed_at: (/* @__PURE__ */ new Date()).toISOString(),
575154
+ via: "resolve"
575155
+ });
574912
575156
  lockManager.removePatch(patch5.id);
575157
+ patchesDismissed.push({ id: patch5.id, message: patch5.original_message });
574913
575158
  continue;
574914
575159
  }
574915
575160
  if (lastIndexByHash.get(r4.hash) !== i5) {
@@ -574931,7 +575176,7 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
574931
575176
  const theirsSnapshot = {};
574932
575177
  for (const file4 of filesForSnapshot) {
574933
575178
  if (isBinaryFile(file4)) continue;
574934
- const content5 = await (0, import_promises710.readFile)((0, import_node_path112.join)(outputDir, file4), "utf-8").catch(() => null);
575179
+ const content5 = await (0, import_promises710.readFile)((0, import_node_path122.join)(outputDir, file4), "utf-8").catch(() => null);
574935
575180
  if (content5 != null) {
574936
575181
  theirsSnapshot[file4] = content5;
574937
575182
  }
@@ -574958,6 +575203,7 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
574958
575203
  commitSha: commitSha2,
574959
575204
  phase: "committed",
574960
575205
  patchesResolved,
575206
+ patchesDismissed: patchesDismissed.length > 0 ? patchesDismissed : void 0,
574961
575207
  warnings: warnings.length > 0 ? warnings : void 0
574962
575208
  };
574963
575209
  }
@@ -575003,7 +575249,9 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
575003
575249
  lastGeneration: void 0,
575004
575250
  patches: [],
575005
575251
  unresolvedCount: 0,
575006
- excludePatterns: []
575252
+ excludePatterns: [],
575253
+ dismissed: [],
575254
+ legacyDismissedCount: 0
575007
575255
  };
575008
575256
  }
575009
575257
  const lock = lockManager.read();
@@ -575032,13 +575280,24 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
575032
575280
  }
575033
575281
  const config5 = lockManager.getCustomizationsConfig();
575034
575282
  const excludePatterns = config5.exclude ?? [];
575283
+ const dismissals = lock.dismissals ?? [];
575284
+ const dismissed = dismissals.map((d2) => ({
575285
+ message: d2.original_message,
575286
+ sha: d2.original_commit.slice(0, 7),
575287
+ via: d2.via,
575288
+ dismissedAt: d2.dismissed_at
575289
+ }));
575290
+ const coveredHashes = new Set(dismissals.flatMap((d2) => d2.content_hashes));
575291
+ const legacyDismissedCount = (lock.forgotten_hashes ?? []).filter((h4) => !coveredHashes.has(h4)).length;
575035
575292
  return {
575036
575293
  initialized: true,
575037
575294
  generationCount: lock.generations.length,
575038
575295
  lastGeneration,
575039
575296
  patches,
575040
575297
  unresolvedCount,
575041
- excludePatterns
575298
+ excludePatterns,
575299
+ dismissed,
575300
+ legacyDismissedCount
575042
575301
  };
575043
575302
  }
575044
575303
  }
@@ -608315,7 +608574,7 @@ var require_replay_forget = __commonJS({
608315
608574
  Object.defineProperty(exports2, "__esModule", { value: true });
608316
608575
  exports2.replayForget = replayForget3;
608317
608576
  var replay_1 = require_dist24();
608318
- function replayForget3(params2) {
608577
+ async function replayForget3(params2) {
608319
608578
  return (0, replay_1.forget)(params2.outputDir, params2.options);
608320
608579
  }
608321
608580
  }
@@ -686384,7 +686643,7 @@ var AccessTokenPosthogManager = class {
686384
686643
  properties: {
686385
686644
  ...event,
686386
686645
  ...event.properties,
686387
- version: "5.23.3-941-g239fc7b1855",
686646
+ version: "5.23.3-947-ga9032a3d959",
686388
686647
  usingAccessToken: true,
686389
686648
  ...getRunIdProperties()
686390
686649
  }
@@ -686450,7 +686709,7 @@ var UserPosthogManager = class {
686450
686709
  distinctId: this.userId ?? await this.getPersistedDistinctId(),
686451
686710
  event: "CLI",
686452
686711
  properties: {
686453
- version: "5.23.3-941-g239fc7b1855",
686712
+ version: "5.23.3-947-ga9032a3d959",
686454
686713
  ...event,
686455
686714
  ...event.properties,
686456
686715
  usingAccessToken: false,
@@ -867512,7 +867771,7 @@ var LOCAL_STORAGE_FOLDER4 = ".fern-dev";
867512
867771
  var LOGS_FOLDER_NAME = "logs";
867513
867772
  var MAX_LOGS_DIR_SIZE_BYTES = 100 * 1024 * 1024;
867514
867773
  function getCliSource() {
867515
- const version7 = "5.23.3-941-g239fc7b1855";
867774
+ const version7 = "5.23.3-947-ga9032a3d959";
867516
867775
  return `cli@${version7}`;
867517
867776
  }
867518
867777
  var DebugLogger = class {
@@ -899700,7 +899959,7 @@ var LegacyDocsPublisher = class {
899700
899959
  previewId,
899701
899960
  disableTemplates: void 0,
899702
899961
  skipUpload,
899703
- cliVersion: "5.23.3-941-g239fc7b1855",
899962
+ cliVersion: "5.23.3-947-ga9032a3d959",
899704
899963
  loginCommand: "fern auth login"
899705
899964
  });
899706
899965
  if (taskContext.getResult() === TaskResult.Failure) {
@@ -902297,7 +902556,7 @@ var ForgetCommand = class {
902297
902556
  const dryRun = args["dry-run"];
902298
902557
  try {
902299
902558
  if (args.all) {
902300
- const result2 = (0, import_generator_cli.replayForget)({ outputDir, options: { all: true, dryRun } });
902559
+ const result2 = await (0, import_generator_cli.replayForget)({ outputDir, options: { all: true, dryRun } });
902301
902560
  if (!result2.initialized) {
902302
902561
  context3.stderr.info("Replay is not initialized. Nothing to forget.");
902303
902562
  return;
@@ -902320,7 +902579,7 @@ var ForgetCommand = class {
902320
902579
  return;
902321
902580
  }
902322
902581
  if (positionalArgs.length > 0 && positionalArgs.every((a3) => a3.startsWith("patch-"))) {
902323
- const result2 = (0, import_generator_cli.replayForget)({ outputDir, options: { patchIds: positionalArgs, dryRun } });
902582
+ const result2 = await (0, import_generator_cli.replayForget)({ outputDir, options: { patchIds: positionalArgs, dryRun } });
902324
902583
  if (!result2.initialized) {
902325
902584
  context3.stderr.info("Replay is not initialized. Nothing to forget.");
902326
902585
  return;
@@ -902346,7 +902605,7 @@ var ForgetCommand = class {
902346
902605
  });
902347
902606
  }
902348
902607
  const pattern = positionalArgs.length === 1 ? positionalArgs[0] : void 0;
902349
- const result = (0, import_generator_cli.replayForget)({ outputDir, options: { pattern } });
902608
+ const result = await (0, import_generator_cli.replayForget)({ outputDir, options: { pattern } });
902350
902609
  if (!result.initialized) {
902351
902610
  context3.stderr.info("Replay is not initialized. Nothing to forget.");
902352
902611
  return;
@@ -902389,7 +902648,7 @@ Remove ${matched.length} patch(es)? [y/N] `, resolvePrompt);
902389
902648
  }
902390
902649
  }
902391
902650
  const patchIds = matched.map((p6) => p6.id);
902392
- const removeResult = (0, import_generator_cli.replayForget)({ outputDir, options: { patchIds, dryRun: false } });
902651
+ const removeResult = await (0, import_generator_cli.replayForget)({ outputDir, options: { patchIds, dryRun: false } });
902393
902652
  context3.stderr.info(`Removed ${removeResult.removed.length} patch(es). ${removeResult.remaining} remaining.`);
902394
902653
  for (const warning of removeResult.warnings) {
902395
902654
  context3.stderr.warn(warning);
@@ -965891,7 +966150,7 @@ function getAutomationContextFromEnv() {
965891
966150
  config_branch: process.env.FERN_CONFIG_BRANCH,
965892
966151
  config_pr_number: process.env.FERN_CONFIG_PR_NUMBER,
965893
966152
  trigger: process.env.GITHUB_EVENT_NAME,
965894
- cli_version: "5.23.3-941-g239fc7b1855"
966153
+ cli_version: "5.23.3-947-ga9032a3d959"
965895
966154
  };
965896
966155
  }
965897
966156
  function isAutomationMode() {
@@ -966723,7 +966982,7 @@ var CliContext = class _CliContext {
966723
966982
  if (false) {
966724
966983
  this.logger.error("CLI_VERSION is not defined");
966725
966984
  }
966726
- return "5.23.3-941-g239fc7b1855";
966985
+ return "5.23.3-947-ga9032a3d959";
966727
966986
  }
966728
966987
  getCliName() {
966729
966988
  if (false) {
@@ -984893,7 +985152,7 @@ function addReplayForgetCommand(cli, cliContext) {
984893
985152
  const dryRun = argv.dryRun;
984894
985153
  try {
984895
985154
  if (argv.all) {
984896
- const result2 = (0, import_generator_cli5.replayForget)({ outputDir, options: { all: true, dryRun } });
985155
+ const result2 = await (0, import_generator_cli5.replayForget)({ outputDir, options: { all: true, dryRun } });
984897
985156
  if (!result2.initialized) {
984898
985157
  cliContext.logger.info("Replay is not initialized. Nothing to forget.");
984899
985158
  return;
@@ -984916,7 +985175,7 @@ function addReplayForgetCommand(cli, cliContext) {
984916
985175
  return;
984917
985176
  }
984918
985177
  if (args.length > 0 && args.every((a3) => a3.startsWith("patch-"))) {
984919
- const result2 = (0, import_generator_cli5.replayForget)({ outputDir, options: { patchIds: args, dryRun } });
985178
+ const result2 = await (0, import_generator_cli5.replayForget)({ outputDir, options: { patchIds: args, dryRun } });
984920
985179
  if (!result2.initialized) {
984921
985180
  cliContext.logger.info("Replay is not initialized. Nothing to forget.");
984922
985181
  return;
@@ -984936,7 +985195,7 @@ function addReplayForgetCommand(cli, cliContext) {
984936
985195
  return;
984937
985196
  }
984938
985197
  const pattern = args.length === 1 ? args[0] : void 0;
984939
- const result = (0, import_generator_cli5.replayForget)({ outputDir, options: { pattern } });
985198
+ const result = await (0, import_generator_cli5.replayForget)({ outputDir, options: { pattern } });
984940
985199
  if (!result.initialized) {
984941
985200
  cliContext.logger.info("Replay is not initialized. Nothing to forget.");
984942
985201
  return;
@@ -984979,7 +985238,7 @@ Remove ${matched.length} patch(es)? [y/N] `, resolvePrompt);
984979
985238
  }
984980
985239
  }
984981
985240
  const patchIds = matched.map((p6) => p6.id);
984982
- const removeResult = (0, import_generator_cli5.replayForget)({ outputDir, options: { patchIds, dryRun: false } });
985241
+ const removeResult = await (0, import_generator_cli5.replayForget)({ outputDir, options: { patchIds, dryRun: false } });
984983
985242
  cliContext.logger.info(
984984
985243
  `Removed ${removeResult.removed.length} patch(es). ${removeResult.remaining} remaining.`
984985
985244
  );
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "5.23.3-941-g239fc7b1855",
2
+ "version": "5.23.3-947-ga9032a3d959",
3
3
  "repository": {
4
4
  "type": "git",
5
5
  "url": "git+https://github.com/fern-api/fern.git",