@savvy-web/silk 2.2.2 → 2.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -380,10 +380,27 @@ const ChangesetConfigLive = effect.Layer.effect(ChangesetConfig, effect.Effect.g
380
380
  fixed: (root) => read(root).pipe(effect.Effect.map(effect.Option.match({
381
381
  onNone: () => [],
382
382
  onSome: (cfg) => cfg.fixed ?? []
383
- })))
383
+ }))),
384
+ refresh: () => effect.Effect.sync(() => cache.clear())
384
385
  };
385
386
  }));
386
387
  //#endregion
388
+ //#region ../silk-effects/dist/dev/pkg/utils/TrailingSlash.js
389
+ /**
390
+ * Trim trailing slashes from a string.
391
+ *
392
+ * @remarks
393
+ * Trims trailing slashes with an index scan rather than `/\/+$/`. That regex is
394
+ * unanchored at the start, so the engine retries the match from every position
395
+ * and degrades to O(n²) on a string of many slashes (CodeQL `js/polynomial-redos`).
396
+ * Only a trailing run of slashes is removed; interior slash runs are untouched.
397
+ */
398
+ const trimTrailingSlashes = (s) => {
399
+ let end = s.length;
400
+ while (end > 0 && s[end - 1] === "/") end -= 1;
401
+ return s.slice(0, end);
402
+ };
403
+ //#endregion
387
404
  //#region ../../node_modules/.pnpm/workspaces-effect@2.0.3_@effect+platform@0.96.2_effect@3.21.4__effect@3.21.4/node_modules/workspaces-effect/errors/CatalogAssemblyError.js
388
405
  /**
389
406
  * Base constant for {@link CatalogAssemblyError}.
@@ -10868,18 +10885,16 @@ var SilkPublishability = class {
10868
10885
  * normalizes to `.`, matching how a root-directory target is recorded.
10869
10886
  *
10870
10887
  * @remarks
10871
- * Trailing slashes are trimmed with an index scan rather than `/\/+$/`. That
10872
- * regex is unanchored at the start, so the engine retries the match from every
10873
- * position and degrades to O(n²) on a path of many slashes (CodeQL
10874
- * `js/polynomial-redos`). `dir` reaches here from `dist/prod/targets.json`, which
10875
- * the bundler writes but a consumer repo could hand-edit.
10888
+ * Trailing-slash trimming is delegated to the shared `trimTrailingSlashes`
10889
+ * index-scan helper rather than `/\/+$/`: that regex is unanchored at the
10890
+ * start, so the engine retries the match from every position and degrades to
10891
+ * O(n²) on a path of many slashes (CodeQL `js/polynomial-redos`). `dir` reaches
10892
+ * here from `dist/prod/targets.json`, which the bundler writes but a consumer
10893
+ * repo could hand-edit.
10876
10894
  */
10877
10895
  const normalizeDir = (dir) => {
10878
10896
  const slashed = dir.replaceAll("\\", "/");
10879
- const withoutPrefix = slashed.startsWith("./") ? slashed.slice(2) : slashed;
10880
- let end = withoutPrefix.length;
10881
- while (end > 0 && withoutPrefix[end - 1] === "/") end -= 1;
10882
- const normalized = withoutPrefix.slice(0, end);
10897
+ const normalized = trimTrailingSlashes(slashed.startsWith("./") ? slashed.slice(2) : slashed);
10883
10898
  return normalized === "" ? "." : normalized;
10884
10899
  };
10885
10900
  /** True when a built target directory's package.json is `private: true`. Missing/unreadable/malformed → false. */
@@ -38499,9 +38514,14 @@ function makeShape$3(reader, discovery, fs) {
38499
38514
  const inspected = yield* inspect(cwd);
38500
38515
  return paths.map((p) => classifyOne(inspected, p));
38501
38516
  });
38517
+ const refresh = () => effect.Effect.gen(function* () {
38518
+ cache.clear();
38519
+ yield* discovery.refresh();
38520
+ });
38502
38521
  return {
38503
38522
  inspect,
38504
- classify
38523
+ classify,
38524
+ refresh
38505
38525
  };
38506
38526
  }
38507
38527
  /**
@@ -38571,7 +38591,8 @@ const ConfigInspectorLive = effect.Layer.effect(ConfigInspector, effect.Effect.g
38571
38591
  function makeConfigInspectorTest(fixed) {
38572
38592
  return effect.Layer.succeed(ConfigInspector, {
38573
38593
  inspect: () => effect.Effect.succeed(fixed),
38574
- classify: (_cwd, paths) => effect.Effect.succeed(paths.map((p) => classifyOne(fixed, p)))
38594
+ classify: (_cwd, paths) => effect.Effect.succeed(paths.map((p) => classifyOne(fixed, p))),
38595
+ refresh: () => effect.Effect.void
38575
38596
  });
38576
38597
  }
38577
38598
  //#endregion
@@ -39085,6 +39106,48 @@ function gitMergeBase(cwd, base) {
39085
39106
  }
39086
39107
  });
39087
39108
  }
39109
+ /**
39110
+ * List the basenames of `.changeset/*.md` files tracked at `ref` (e.g. the
39111
+ * merge base), via `git ls-tree -r --name-only`. Used by `DepsRegen.plan()`
39112
+ * to protect changesets authored by already-merged PRs from being deleted by
39113
+ * an unrelated branch's regen run (#258).
39114
+ *
39115
+ * @remarks
39116
+ * Deliberately tolerant, unlike {@link gitMergeBase}: `cwd` may not be a git
39117
+ * repository at all (many `DepsRegen` unit tests pass synthetic refs like
39118
+ * `"BEFORE"`/`"AFTER"` against a bare tmpdir), and an unresolvable ref is a
39119
+ * plausible caller mistake rather than a fatal condition. Either failure mode
39120
+ * resolves to an empty set — "nothing protected" — rather than propagating a
39121
+ * {@link GitError}, so a missing/invalid git context degrades the
39122
+ * authorship filter to a no-op instead of blocking the whole plan.
39123
+ *
39124
+ * @internal
39125
+ */
39126
+ function gitListChangesetFilesAtRef(cwd, ref) {
39127
+ return effect.Effect.sync(() => {
39128
+ try {
39129
+ const out = (0, node_child_process.execFileSync)("git", [
39130
+ "ls-tree",
39131
+ "-r",
39132
+ "--name-only",
39133
+ ref,
39134
+ "--",
39135
+ ".changeset"
39136
+ ], {
39137
+ cwd,
39138
+ encoding: "utf8",
39139
+ stdio: [
39140
+ "ignore",
39141
+ "pipe",
39142
+ "pipe"
39143
+ ]
39144
+ });
39145
+ return new Set(out.split(/\r?\n/).filter((line) => line.trim().length > 0).map((path) => (0, node_path.basename)(path)));
39146
+ } catch {
39147
+ return /* @__PURE__ */ new Set();
39148
+ }
39149
+ });
39150
+ }
39088
39151
  //#endregion
39089
39152
  //#region ../silk-effects/dist/dev/pkg/changesets/utils/publishability.js
39090
39153
  /**
@@ -39149,6 +39212,15 @@ function listPublishablePackageNames(packages, root) {
39149
39212
  * first, then deleting the stale pure-dependency ones (so an interrupted
39150
39213
  * run loses nothing and is safely re-runnable).
39151
39214
  *
39215
+ * A pure dependency changeset is only ever a delete candidate when BOTH:
39216
+ * its package is in scope AND actually produced a fresh diff this run
39217
+ * (present in {@link RegenPlan.toWrite}), and the file was authored on
39218
+ * this branch rather than already committed at the merge-base ref. Either
39219
+ * gap silently destroyed a still-relevant release note before this was
39220
+ * fixed (#258) — a devDependency-only diff drops all its rows and was
39221
+ * never rewritten, and a changeset an earlier, already-merged PR
39222
+ * committed was swept away by an unrelated branch's regen run.
39223
+ *
39152
39224
  * This is the single source of truth for regen/detect: the CLI commands
39153
39225
  * and MCP tools are thin adapters over this service.
39154
39226
  *
@@ -39368,7 +39440,8 @@ function makeShape$1(pit, inspector, discovery, detector, config, fs) {
39368
39440
  const plan = (options) => effect.Effect.gen(function* () {
39369
39441
  const resolvedCwd = (0, node_path.resolve)(options.cwd);
39370
39442
  const changesetDir = (0, node_path.join)(resolvedCwd, ".changeset");
39371
- yield* discovery.refresh();
39443
+ yield* inspector.refresh();
39444
+ yield* config.refresh();
39372
39445
  let fromRef = options.from;
39373
39446
  if (!fromRef) {
39374
39447
  let baseBranch = options.base;
@@ -39405,7 +39478,10 @@ function makeShape$1(pit, inspector, discovery, detector, config, fs) {
39405
39478
  }
39406
39479
  const existingPure = yield* findPureDependencyChangesets(fs, changesetDir);
39407
39480
  const skippedMixed = yield* findMixedDependencyChangesets(fs, changesetDir);
39408
- const toDelete = existingPure.filter((p) => inScopeFor(p.package));
39481
+ const rewrittenPackages = new Set(resolved.map((d) => d.package));
39482
+ const atMergeBase = yield* gitListChangesetFilesAtRef(resolvedCwd, fromRef);
39483
+ const authoredOnBranch = (file) => !atMergeBase.has((0, node_path.basename)(file));
39484
+ const toDelete = existingPure.filter((p) => inScopeFor(p.package) && rewrittenPackages.has(p.package) && authoredOnBranch(p.file));
39409
39485
  const chosenFilenames = /* @__PURE__ */ new Set();
39410
39486
  const toWrite = [];
39411
39487
  for (const diff of resolved) {
@@ -7333,6 +7333,17 @@ interface ConfigInspectorShape {
7333
7333
  * input path, in the same order
7334
7334
  */
7335
7335
  readonly classify: (cwd: string, paths: ReadonlyArray<string>) => Effect.Effect<ReadonlyArray<Classification>, ConfigurationError>;
7336
+ /**
7337
+ * Drop the cached {@link InspectedConfig} for every previously-inspected
7338
+ * root, and refresh the underlying `WorkspaceDiscovery` snapshot. Callers
7339
+ * that hold this service across multiple logical operations in a single
7340
+ * process (e.g. a long-lived MCP server) must call this before an
7341
+ * operation that needs to observe on-disk edits made since the last
7342
+ * `inspect`/`classify` call — the cache never expires on its own.
7343
+ *
7344
+ * @returns An Effect that clears the cache and succeeds with `void`.
7345
+ */
7346
+ readonly refresh: () => Effect.Effect<void>;
7336
7347
  }
7337
7348
  /**
7338
7349
  * Base class for {@link ConfigInspector}.
@@ -8028,6 +8039,14 @@ declare const ChangesetConfig_base: Context.TagClass<ChangesetConfig, "@savvy-we
8028
8039
  readonly ignorePatterns: (root: string) => Effect.Effect<ReadonlyArray<string>>;
8029
8040
  readonly isIgnored: (name: string, root: string) => Effect.Effect<boolean>;
8030
8041
  readonly fixed: (root: string) => Effect.Effect<ReadonlyArray<ReadonlyArray<string>>>;
8042
+ /**
8043
+ * Drop the cached read for every previously-read root. Callers that hold
8044
+ * this service across multiple logical operations in a single process
8045
+ * (e.g. a long-lived MCP server) must call this before an operation that
8046
+ * needs to observe an on-disk edit made since the last accessor call —
8047
+ * the cache never expires on its own.
8048
+ */
8049
+ readonly refresh: () => Effect.Effect<void>;
8031
8050
  }>;
8032
8051
  /**
8033
8052
  * Accessor service over a workspace root's `.changeset/config.json`.
@@ -7333,6 +7333,17 @@ interface ConfigInspectorShape {
7333
7333
  * input path, in the same order
7334
7334
  */
7335
7335
  readonly classify: (cwd: string, paths: ReadonlyArray<string>) => Effect.Effect<ReadonlyArray<Classification>, ConfigurationError>;
7336
+ /**
7337
+ * Drop the cached {@link InspectedConfig} for every previously-inspected
7338
+ * root, and refresh the underlying `WorkspaceDiscovery` snapshot. Callers
7339
+ * that hold this service across multiple logical operations in a single
7340
+ * process (e.g. a long-lived MCP server) must call this before an
7341
+ * operation that needs to observe on-disk edits made since the last
7342
+ * `inspect`/`classify` call — the cache never expires on its own.
7343
+ *
7344
+ * @returns An Effect that clears the cache and succeeds with `void`.
7345
+ */
7346
+ readonly refresh: () => Effect.Effect<void>;
7336
7347
  }
7337
7348
  /**
7338
7349
  * Base class for {@link ConfigInspector}.
@@ -8028,6 +8039,14 @@ declare const ChangesetConfig_base: Context.TagClass<ChangesetConfig, "@savvy-we
8028
8039
  readonly ignorePatterns: (root: string) => Effect.Effect<ReadonlyArray<string>>;
8029
8040
  readonly isIgnored: (name: string, root: string) => Effect.Effect<boolean>;
8030
8041
  readonly fixed: (root: string) => Effect.Effect<ReadonlyArray<ReadonlyArray<string>>>;
8042
+ /**
8043
+ * Drop the cached read for every previously-read root. Callers that hold
8044
+ * this service across multiple logical operations in a single process
8045
+ * (e.g. a long-lived MCP server) must call this before an operation that
8046
+ * needs to observe an on-disk edit made since the last accessor call —
8047
+ * the cache never expires on its own.
8048
+ */
8049
+ readonly refresh: () => Effect.Effect<void>;
8031
8050
  }>;
8032
8051
  /**
8033
8052
  * Accessor service over a workspace root's `.changeset/config.json`.
@@ -394,10 +394,28 @@ const ChangesetConfigLive = Layer.effect(ChangesetConfig, Effect.gen(function* (
394
394
  fixed: (root) => read(root).pipe(Effect.map(Option.match({
395
395
  onNone: () => [],
396
396
  onSome: (cfg) => cfg.fixed ?? []
397
- })))
397
+ }))),
398
+ refresh: () => Effect.sync(() => cache.clear())
398
399
  };
399
400
  }));
400
401
 
402
+ //#endregion
403
+ //#region ../silk-effects/dist/dev/pkg/utils/TrailingSlash.js
404
+ /**
405
+ * Trim trailing slashes from a string.
406
+ *
407
+ * @remarks
408
+ * Trims trailing slashes with an index scan rather than `/\/+$/`. That regex is
409
+ * unanchored at the start, so the engine retries the match from every position
410
+ * and degrades to O(n²) on a string of many slashes (CodeQL `js/polynomial-redos`).
411
+ * Only a trailing run of slashes is removed; interior slash runs are untouched.
412
+ */
413
+ const trimTrailingSlashes = (s) => {
414
+ let end = s.length;
415
+ while (end > 0 && s[end - 1] === "/") end -= 1;
416
+ return s.slice(0, end);
417
+ };
418
+
401
419
  //#endregion
402
420
  //#region ../../node_modules/.pnpm/workspaces-effect@2.0.3_@effect+platform@0.96.2_effect@3.21.4__effect@3.21.4/node_modules/workspaces-effect/errors/CatalogAssemblyError.js
403
421
  /**
@@ -11064,18 +11082,16 @@ var SilkPublishability = class {
11064
11082
  * normalizes to `.`, matching how a root-directory target is recorded.
11065
11083
  *
11066
11084
  * @remarks
11067
- * Trailing slashes are trimmed with an index scan rather than `/\/+$/`. That
11068
- * regex is unanchored at the start, so the engine retries the match from every
11069
- * position and degrades to O(n²) on a path of many slashes (CodeQL
11070
- * `js/polynomial-redos`). `dir` reaches here from `dist/prod/targets.json`, which
11071
- * the bundler writes but a consumer repo could hand-edit.
11085
+ * Trailing-slash trimming is delegated to the shared `trimTrailingSlashes`
11086
+ * index-scan helper rather than `/\/+$/`: that regex is unanchored at the
11087
+ * start, so the engine retries the match from every position and degrades to
11088
+ * O(n²) on a path of many slashes (CodeQL `js/polynomial-redos`). `dir` reaches
11089
+ * here from `dist/prod/targets.json`, which the bundler writes but a consumer
11090
+ * repo could hand-edit.
11072
11091
  */
11073
11092
  const normalizeDir = (dir) => {
11074
11093
  const slashed = dir.replaceAll("\\", "/");
11075
- const withoutPrefix = slashed.startsWith("./") ? slashed.slice(2) : slashed;
11076
- let end = withoutPrefix.length;
11077
- while (end > 0 && withoutPrefix[end - 1] === "/") end -= 1;
11078
- const normalized = withoutPrefix.slice(0, end);
11094
+ const normalized = trimTrailingSlashes(slashed.startsWith("./") ? slashed.slice(2) : slashed);
11079
11095
  return normalized === "" ? "." : normalized;
11080
11096
  };
11081
11097
  /** True when a built target directory's package.json is `private: true`. Missing/unreadable/malformed → false. */
@@ -38943,9 +38959,14 @@ function makeShape$3(reader, discovery, fs) {
38943
38959
  const inspected = yield* inspect(cwd);
38944
38960
  return paths.map((p) => classifyOne(inspected, p));
38945
38961
  });
38962
+ const refresh = () => Effect.gen(function* () {
38963
+ cache.clear();
38964
+ yield* discovery.refresh();
38965
+ });
38946
38966
  return {
38947
38967
  inspect,
38948
- classify
38968
+ classify,
38969
+ refresh
38949
38970
  };
38950
38971
  }
38951
38972
  /**
@@ -39015,7 +39036,8 @@ const ConfigInspectorLive = Layer.effect(ConfigInspector, Effect.gen(function* (
39015
39036
  function makeConfigInspectorTest(fixed) {
39016
39037
  return Layer.succeed(ConfigInspector, {
39017
39038
  inspect: () => Effect.succeed(fixed),
39018
- classify: (_cwd, paths) => Effect.succeed(paths.map((p) => classifyOne(fixed, p)))
39039
+ classify: (_cwd, paths) => Effect.succeed(paths.map((p) => classifyOne(fixed, p))),
39040
+ refresh: () => Effect.void
39019
39041
  });
39020
39042
  }
39021
39043
 
@@ -39533,6 +39555,48 @@ function gitMergeBase(cwd, base) {
39533
39555
  }
39534
39556
  });
39535
39557
  }
39558
+ /**
39559
+ * List the basenames of `.changeset/*.md` files tracked at `ref` (e.g. the
39560
+ * merge base), via `git ls-tree -r --name-only`. Used by `DepsRegen.plan()`
39561
+ * to protect changesets authored by already-merged PRs from being deleted by
39562
+ * an unrelated branch's regen run (#258).
39563
+ *
39564
+ * @remarks
39565
+ * Deliberately tolerant, unlike {@link gitMergeBase}: `cwd` may not be a git
39566
+ * repository at all (many `DepsRegen` unit tests pass synthetic refs like
39567
+ * `"BEFORE"`/`"AFTER"` against a bare tmpdir), and an unresolvable ref is a
39568
+ * plausible caller mistake rather than a fatal condition. Either failure mode
39569
+ * resolves to an empty set — "nothing protected" — rather than propagating a
39570
+ * {@link GitError}, so a missing/invalid git context degrades the
39571
+ * authorship filter to a no-op instead of blocking the whole plan.
39572
+ *
39573
+ * @internal
39574
+ */
39575
+ function gitListChangesetFilesAtRef(cwd, ref) {
39576
+ return Effect.sync(() => {
39577
+ try {
39578
+ const out = execFileSync("git", [
39579
+ "ls-tree",
39580
+ "-r",
39581
+ "--name-only",
39582
+ ref,
39583
+ "--",
39584
+ ".changeset"
39585
+ ], {
39586
+ cwd,
39587
+ encoding: "utf8",
39588
+ stdio: [
39589
+ "ignore",
39590
+ "pipe",
39591
+ "pipe"
39592
+ ]
39593
+ });
39594
+ return new Set(out.split(/\r?\n/).filter((line) => line.trim().length > 0).map((path) => basename(path)));
39595
+ } catch {
39596
+ return /* @__PURE__ */ new Set();
39597
+ }
39598
+ });
39599
+ }
39536
39600
 
39537
39601
  //#endregion
39538
39602
  //#region ../silk-effects/dist/dev/pkg/changesets/utils/publishability.js
@@ -39599,6 +39663,15 @@ function listPublishablePackageNames(packages, root) {
39599
39663
  * first, then deleting the stale pure-dependency ones (so an interrupted
39600
39664
  * run loses nothing and is safely re-runnable).
39601
39665
  *
39666
+ * A pure dependency changeset is only ever a delete candidate when BOTH:
39667
+ * its package is in scope AND actually produced a fresh diff this run
39668
+ * (present in {@link RegenPlan.toWrite}), and the file was authored on
39669
+ * this branch rather than already committed at the merge-base ref. Either
39670
+ * gap silently destroyed a still-relevant release note before this was
39671
+ * fixed (#258) — a devDependency-only diff drops all its rows and was
39672
+ * never rewritten, and a changeset an earlier, already-merged PR
39673
+ * committed was swept away by an unrelated branch's regen run.
39674
+ *
39602
39675
  * This is the single source of truth for regen/detect: the CLI commands
39603
39676
  * and MCP tools are thin adapters over this service.
39604
39677
  *
@@ -39818,7 +39891,8 @@ function makeShape$1(pit, inspector, discovery, detector, config, fs) {
39818
39891
  const plan = (options) => Effect.gen(function* () {
39819
39892
  const resolvedCwd = resolve(options.cwd);
39820
39893
  const changesetDir = join(resolvedCwd, ".changeset");
39821
- yield* discovery.refresh();
39894
+ yield* inspector.refresh();
39895
+ yield* config.refresh();
39822
39896
  let fromRef = options.from;
39823
39897
  if (!fromRef) {
39824
39898
  let baseBranch = options.base;
@@ -39855,7 +39929,10 @@ function makeShape$1(pit, inspector, discovery, detector, config, fs) {
39855
39929
  }
39856
39930
  const existingPure = yield* findPureDependencyChangesets(fs, changesetDir);
39857
39931
  const skippedMixed = yield* findMixedDependencyChangesets(fs, changesetDir);
39858
- const toDelete = existingPure.filter((p) => inScopeFor(p.package));
39932
+ const rewrittenPackages = new Set(resolved.map((d) => d.package));
39933
+ const atMergeBase = yield* gitListChangesetFilesAtRef(resolvedCwd, fromRef);
39934
+ const authoredOnBranch = (file) => !atMergeBase.has(basename(file));
39935
+ const toDelete = existingPure.filter((p) => inScopeFor(p.package) && rewrittenPackages.has(p.package) && authoredOnBranch(p.file));
39859
39936
  const chosenFilenames = /* @__PURE__ */ new Set();
39860
39937
  const toWrite = [];
39861
39938
  for (const diff of resolved) {
@@ -384,10 +384,27 @@ const ChangesetConfigLive = effect.Layer.effect(ChangesetConfig, effect.Effect.g
384
384
  fixed: (root) => read(root).pipe(effect.Effect.map(effect.Option.match({
385
385
  onNone: () => [],
386
386
  onSome: (cfg) => cfg.fixed ?? []
387
- })))
387
+ }))),
388
+ refresh: () => effect.Effect.sync(() => cache.clear())
388
389
  };
389
390
  }));
390
391
  //#endregion
392
+ //#region ../silk-effects/dist/dev/pkg/utils/TrailingSlash.js
393
+ /**
394
+ * Trim trailing slashes from a string.
395
+ *
396
+ * @remarks
397
+ * Trims trailing slashes with an index scan rather than `/\/+$/`. That regex is
398
+ * unanchored at the start, so the engine retries the match from every position
399
+ * and degrades to O(n²) on a string of many slashes (CodeQL `js/polynomial-redos`).
400
+ * Only a trailing run of slashes is removed; interior slash runs are untouched.
401
+ */
402
+ const trimTrailingSlashes = (s) => {
403
+ let end = s.length;
404
+ while (end > 0 && s[end - 1] === "/") end -= 1;
405
+ return s.slice(0, end);
406
+ };
407
+ //#endregion
391
408
  //#region ../../node_modules/.pnpm/workspaces-effect@2.0.3_@effect+platform@0.96.2_effect@3.21.4__effect@3.21.4/node_modules/workspaces-effect/errors/CatalogAssemblyError.js
392
409
  /**
393
410
  * Base constant for {@link CatalogAssemblyError}.
@@ -10872,18 +10889,16 @@ var SilkPublishability = class {
10872
10889
  * normalizes to `.`, matching how a root-directory target is recorded.
10873
10890
  *
10874
10891
  * @remarks
10875
- * Trailing slashes are trimmed with an index scan rather than `/\/+$/`. That
10876
- * regex is unanchored at the start, so the engine retries the match from every
10877
- * position and degrades to O(n²) on a path of many slashes (CodeQL
10878
- * `js/polynomial-redos`). `dir` reaches here from `dist/prod/targets.json`, which
10879
- * the bundler writes but a consumer repo could hand-edit.
10892
+ * Trailing-slash trimming is delegated to the shared `trimTrailingSlashes`
10893
+ * index-scan helper rather than `/\/+$/`: that regex is unanchored at the
10894
+ * start, so the engine retries the match from every position and degrades to
10895
+ * O(n²) on a path of many slashes (CodeQL `js/polynomial-redos`). `dir` reaches
10896
+ * here from `dist/prod/targets.json`, which the bundler writes but a consumer
10897
+ * repo could hand-edit.
10880
10898
  */
10881
10899
  const normalizeDir = (dir) => {
10882
10900
  const slashed = dir.replaceAll("\\", "/");
10883
- const withoutPrefix = slashed.startsWith("./") ? slashed.slice(2) : slashed;
10884
- let end = withoutPrefix.length;
10885
- while (end > 0 && withoutPrefix[end - 1] === "/") end -= 1;
10886
- const normalized = withoutPrefix.slice(0, end);
10901
+ const normalized = trimTrailingSlashes(slashed.startsWith("./") ? slashed.slice(2) : slashed);
10887
10902
  return normalized === "" ? "." : normalized;
10888
10903
  };
10889
10904
  /** True when a built target directory's package.json is `private: true`. Missing/unreadable/malformed → false. */
@@ -38503,9 +38518,14 @@ function makeShape$3(reader, discovery, fs) {
38503
38518
  const inspected = yield* inspect(cwd);
38504
38519
  return paths.map((p) => classifyOne(inspected, p));
38505
38520
  });
38521
+ const refresh = () => effect.Effect.gen(function* () {
38522
+ cache.clear();
38523
+ yield* discovery.refresh();
38524
+ });
38506
38525
  return {
38507
38526
  inspect,
38508
- classify
38527
+ classify,
38528
+ refresh
38509
38529
  };
38510
38530
  }
38511
38531
  /**
@@ -38575,7 +38595,8 @@ const ConfigInspectorLive = effect.Layer.effect(ConfigInspector, effect.Effect.g
38575
38595
  function makeConfigInspectorTest(fixed) {
38576
38596
  return effect.Layer.succeed(ConfigInspector, {
38577
38597
  inspect: () => effect.Effect.succeed(fixed),
38578
- classify: (_cwd, paths) => effect.Effect.succeed(paths.map((p) => classifyOne(fixed, p)))
38598
+ classify: (_cwd, paths) => effect.Effect.succeed(paths.map((p) => classifyOne(fixed, p))),
38599
+ refresh: () => effect.Effect.void
38579
38600
  });
38580
38601
  }
38581
38602
  //#endregion
@@ -39089,6 +39110,48 @@ function gitMergeBase(cwd, base) {
39089
39110
  }
39090
39111
  });
39091
39112
  }
39113
+ /**
39114
+ * List the basenames of `.changeset/*.md` files tracked at `ref` (e.g. the
39115
+ * merge base), via `git ls-tree -r --name-only`. Used by `DepsRegen.plan()`
39116
+ * to protect changesets authored by already-merged PRs from being deleted by
39117
+ * an unrelated branch's regen run (#258).
39118
+ *
39119
+ * @remarks
39120
+ * Deliberately tolerant, unlike {@link gitMergeBase}: `cwd` may not be a git
39121
+ * repository at all (many `DepsRegen` unit tests pass synthetic refs like
39122
+ * `"BEFORE"`/`"AFTER"` against a bare tmpdir), and an unresolvable ref is a
39123
+ * plausible caller mistake rather than a fatal condition. Either failure mode
39124
+ * resolves to an empty set — "nothing protected" — rather than propagating a
39125
+ * {@link GitError}, so a missing/invalid git context degrades the
39126
+ * authorship filter to a no-op instead of blocking the whole plan.
39127
+ *
39128
+ * @internal
39129
+ */
39130
+ function gitListChangesetFilesAtRef(cwd, ref) {
39131
+ return effect.Effect.sync(() => {
39132
+ try {
39133
+ const out = (0, node_child_process.execFileSync)("git", [
39134
+ "ls-tree",
39135
+ "-r",
39136
+ "--name-only",
39137
+ ref,
39138
+ "--",
39139
+ ".changeset"
39140
+ ], {
39141
+ cwd,
39142
+ encoding: "utf8",
39143
+ stdio: [
39144
+ "ignore",
39145
+ "pipe",
39146
+ "pipe"
39147
+ ]
39148
+ });
39149
+ return new Set(out.split(/\r?\n/).filter((line) => line.trim().length > 0).map((path) => (0, node_path.basename)(path)));
39150
+ } catch {
39151
+ return /* @__PURE__ */ new Set();
39152
+ }
39153
+ });
39154
+ }
39092
39155
  //#endregion
39093
39156
  //#region ../silk-effects/dist/dev/pkg/changesets/utils/publishability.js
39094
39157
  /**
@@ -39153,6 +39216,15 @@ function listPublishablePackageNames(packages, root) {
39153
39216
  * first, then deleting the stale pure-dependency ones (so an interrupted
39154
39217
  * run loses nothing and is safely re-runnable).
39155
39218
  *
39219
+ * A pure dependency changeset is only ever a delete candidate when BOTH:
39220
+ * its package is in scope AND actually produced a fresh diff this run
39221
+ * (present in {@link RegenPlan.toWrite}), and the file was authored on
39222
+ * this branch rather than already committed at the merge-base ref. Either
39223
+ * gap silently destroyed a still-relevant release note before this was
39224
+ * fixed (#258) — a devDependency-only diff drops all its rows and was
39225
+ * never rewritten, and a changeset an earlier, already-merged PR
39226
+ * committed was swept away by an unrelated branch's regen run.
39227
+ *
39156
39228
  * This is the single source of truth for regen/detect: the CLI commands
39157
39229
  * and MCP tools are thin adapters over this service.
39158
39230
  *
@@ -39372,7 +39444,8 @@ function makeShape$1(pit, inspector, discovery, detector, config, fs) {
39372
39444
  const plan = (options) => effect.Effect.gen(function* () {
39373
39445
  const resolvedCwd = (0, node_path.resolve)(options.cwd);
39374
39446
  const changesetDir = (0, node_path.join)(resolvedCwd, ".changeset");
39375
- yield* discovery.refresh();
39447
+ yield* inspector.refresh();
39448
+ yield* config.refresh();
39376
39449
  let fromRef = options.from;
39377
39450
  if (!fromRef) {
39378
39451
  let baseBranch = options.base;
@@ -39409,7 +39482,10 @@ function makeShape$1(pit, inspector, discovery, detector, config, fs) {
39409
39482
  }
39410
39483
  const existingPure = yield* findPureDependencyChangesets(fs, changesetDir);
39411
39484
  const skippedMixed = yield* findMixedDependencyChangesets(fs, changesetDir);
39412
- const toDelete = existingPure.filter((p) => inScopeFor(p.package));
39485
+ const rewrittenPackages = new Set(resolved.map((d) => d.package));
39486
+ const atMergeBase = yield* gitListChangesetFilesAtRef(resolvedCwd, fromRef);
39487
+ const authoredOnBranch = (file) => !atMergeBase.has((0, node_path.basename)(file));
39488
+ const toDelete = existingPure.filter((p) => inScopeFor(p.package) && rewrittenPackages.has(p.package) && authoredOnBranch(p.file));
39413
39489
  const chosenFilenames = /* @__PURE__ */ new Set();
39414
39490
  const toWrite = [];
39415
39491
  for (const diff of resolved) {
@@ -7333,6 +7333,17 @@ interface ConfigInspectorShape {
7333
7333
  * input path, in the same order
7334
7334
  */
7335
7335
  readonly classify: (cwd: string, paths: ReadonlyArray<string>) => Effect.Effect<ReadonlyArray<Classification>, ConfigurationError>;
7336
+ /**
7337
+ * Drop the cached {@link InspectedConfig} for every previously-inspected
7338
+ * root, and refresh the underlying `WorkspaceDiscovery` snapshot. Callers
7339
+ * that hold this service across multiple logical operations in a single
7340
+ * process (e.g. a long-lived MCP server) must call this before an
7341
+ * operation that needs to observe on-disk edits made since the last
7342
+ * `inspect`/`classify` call — the cache never expires on its own.
7343
+ *
7344
+ * @returns An Effect that clears the cache and succeeds with `void`.
7345
+ */
7346
+ readonly refresh: () => Effect.Effect<void>;
7336
7347
  }
7337
7348
  /**
7338
7349
  * Base class for {@link ConfigInspector}.
@@ -8028,6 +8039,14 @@ declare const ChangesetConfig_base: Context.TagClass<ChangesetConfig, "@savvy-we
8028
8039
  readonly ignorePatterns: (root: string) => Effect.Effect<ReadonlyArray<string>>;
8029
8040
  readonly isIgnored: (name: string, root: string) => Effect.Effect<boolean>;
8030
8041
  readonly fixed: (root: string) => Effect.Effect<ReadonlyArray<ReadonlyArray<string>>>;
8042
+ /**
8043
+ * Drop the cached read for every previously-read root. Callers that hold
8044
+ * this service across multiple logical operations in a single process
8045
+ * (e.g. a long-lived MCP server) must call this before an operation that
8046
+ * needs to observe an on-disk edit made since the last accessor call —
8047
+ * the cache never expires on its own.
8048
+ */
8049
+ readonly refresh: () => Effect.Effect<void>;
8031
8050
  }>;
8032
8051
  /**
8033
8052
  * Accessor service over a workspace root's `.changeset/config.json`.
@@ -7333,6 +7333,17 @@ interface ConfigInspectorShape {
7333
7333
  * input path, in the same order
7334
7334
  */
7335
7335
  readonly classify: (cwd: string, paths: ReadonlyArray<string>) => Effect.Effect<ReadonlyArray<Classification>, ConfigurationError>;
7336
+ /**
7337
+ * Drop the cached {@link InspectedConfig} for every previously-inspected
7338
+ * root, and refresh the underlying `WorkspaceDiscovery` snapshot. Callers
7339
+ * that hold this service across multiple logical operations in a single
7340
+ * process (e.g. a long-lived MCP server) must call this before an
7341
+ * operation that needs to observe on-disk edits made since the last
7342
+ * `inspect`/`classify` call — the cache never expires on its own.
7343
+ *
7344
+ * @returns An Effect that clears the cache and succeeds with `void`.
7345
+ */
7346
+ readonly refresh: () => Effect.Effect<void>;
7336
7347
  }
7337
7348
  /**
7338
7349
  * Base class for {@link ConfigInspector}.
@@ -8028,6 +8039,14 @@ declare const ChangesetConfig_base: Context.TagClass<ChangesetConfig, "@savvy-we
8028
8039
  readonly ignorePatterns: (root: string) => Effect.Effect<ReadonlyArray<string>>;
8029
8040
  readonly isIgnored: (name: string, root: string) => Effect.Effect<boolean>;
8030
8041
  readonly fixed: (root: string) => Effect.Effect<ReadonlyArray<ReadonlyArray<string>>>;
8042
+ /**
8043
+ * Drop the cached read for every previously-read root. Callers that hold
8044
+ * this service across multiple logical operations in a single process
8045
+ * (e.g. a long-lived MCP server) must call this before an operation that
8046
+ * needs to observe an on-disk edit made since the last accessor call —
8047
+ * the cache never expires on its own.
8048
+ */
8049
+ readonly refresh: () => Effect.Effect<void>;
8031
8050
  }>;
8032
8051
  /**
8033
8052
  * Accessor service over a workspace root's `.changeset/config.json`.