@savvy-web/silk-effects 4.1.0 → 4.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,7 +5,7 @@ import { SilkPublishability, readTargetsBinding } from "../../services/SilkPubli
5
5
  import { Context, Effect, FileSystem, Layer, Path, Result, Schema } from "effect";
6
6
  import { isAbsolute, join, relative, resolve } from "node:path";
7
7
  import { GlobPattern, GlobPatternOptions } from "@effected/glob";
8
- import { descend } from "@effected/walker";
8
+ import { compileAndExpand } from "@effected/walker";
9
9
  import { WorkspaceDiscovery } from "@effected/workspaces";
10
10
 
11
11
  //#region src/changesets/services/config-inspector.ts
@@ -172,23 +172,13 @@ function normalizeLegacyOptions(options, configPath) {
172
172
  /** Match dotfiles, mirroring the former tinyglobby `dot: true` behavior. */
173
173
  const GLOB_OPTIONS = GlobPatternOptions.make({ dot: true });
174
174
  /**
175
- * Compile `glob` via `@effected/glob`, folding a compile-guard trip
176
- * (over-length pattern, brace-expansion budget, nesting depth) into a
177
- * {@link ConfigurationError} naming the offending glob on the typed channel.
178
- */
179
- function compileGlob(glob) {
180
- return GlobPattern.compile(glob, GLOB_OPTIONS).pipe(Effect.mapError((error) => new ConfigurationError({
181
- field: "glob",
182
- reason: `Invalid glob pattern ${JSON.stringify(glob)}: ${error.message}`
183
- })));
184
- }
185
175
  /**
186
176
  * Pure attribution helper: does `glob` (under this service's `dot: true`
187
177
  * semantics) match the repo-relative POSIX path `rel`? An uncompilable
188
178
  * pattern matches nothing.
189
179
  */
190
180
  function globMatchesRel(glob, rel) {
191
- const result = Effect.runSync(Effect.result(GlobPattern.compile(glob, GLOB_OPTIONS)));
181
+ const result = GlobPattern.compileResult(glob, GLOB_OPTIONS);
192
182
  return Result.isSuccess(result) && result.success.matches(rel);
193
183
  }
194
184
  /**
@@ -204,13 +194,14 @@ function globMatchesRel(glob, rel) {
204
194
  * module already speaks POSIX-relative match paths).
205
195
  */
206
196
  function materializeGlob(glob, cwd) {
207
- return compileGlob(glob).pipe(Effect.flatMap((pattern) => descend(pattern, {
197
+ return compileAndExpand(glob, {
208
198
  cwd,
209
- onUnreadable: "skip"
199
+ onUnreadable: "skip",
200
+ glob: GLOB_OPTIONS
210
201
  }).pipe(Effect.mapError((error) => new ConfigurationError({
211
202
  field: "glob",
212
- reason: `Failed to materialize glob ${JSON.stringify(glob)}: ${error.message}`
213
- })), Effect.provide(Path.layer))));
203
+ reason: error.stage === "compile" ? `Invalid glob pattern ${JSON.stringify(glob)}: ${error.cause.message}` : `Failed to materialize glob ${JSON.stringify(glob)}: ${error.cause.message}`
204
+ })), Effect.provide(Path.layer));
214
205
  }
215
206
  /**
216
207
  * Determine whether `child` is the same directory as `parent` or sits inside
@@ -1,10 +1,10 @@
1
1
  import { LegacyVersionFilesSchema } from "../schemas/version-files.js";
2
2
  import { jsonPathGet, jsonPathResolve, parseJsonPath } from "./jsonpath.js";
3
- import { Effect, Option, Path, Schema } from "effect";
3
+ import { Effect, Path, Schema } from "effect";
4
4
  import { readFileSync, writeFileSync } from "node:fs";
5
5
  import { join, relative, resolve } from "node:path";
6
- import { GlobPattern } from "@effected/glob";
7
- import { descend } from "@effected/walker";
6
+ import { GlobPatternOptions } from "@effected/glob";
7
+ import { compileAndExpand } from "@effected/walker";
8
8
  import { Jsonc, JsoncEdit, JsoncFormattingOptions, JsoncModifier } from "@effected/jsonc";
9
9
 
10
10
  //#region src/changesets/utils/version-files.ts
@@ -405,19 +405,29 @@ var VersionFiles = class VersionFiles {
405
405
  }
406
406
  };
407
407
  /**
408
+ * Match dotfiles, matching `ConfigInspector`'s glob dialect.
409
+ *
410
+ * @remarks
411
+ * This module previously compiled with minimatch DEFAULTS while
412
+ * `ConfigInspector` compiled with `dot: true`, so a wildcard segment matching
413
+ * a dotted directory was attributed to a package but never materialized (or
414
+ * the reverse). The two paths must agree: attribution and materialization
415
+ * answer the same question about the same glob.
416
+ */
417
+ const GLOB_OPTIONS = GlobPatternOptions.make({ dot: true });
418
+ /**
408
419
  * Expand a glob pattern against the filesystem, returning matching FILE paths
409
420
  * relative to `cwd` (POSIX separators), sorted by relative path.
410
421
  *
411
422
  * @remarks
412
- * The walk is `@effected/walker`'s `descend` (literal fast path,
423
+ * The walk is `@effected/walker`'s `compileAndExpand` (literal fast path,
413
424
  * `enumerationPrefix` bounding, `node_modules`/`.git` pruning), with
414
425
  * `onUnreadable: "skip"` preserving this module's silent-skip policy for
415
- * unreadable directories. Matching semantics are minimatch DEFAULTS via the
416
- * compiled pattern notably, wildcards do not match dotfiles, same as the
417
- * previous `tinyglobby` defaults. An uncompilable pattern expands to no
418
- * matches. `descend`'s `Path` requirement is satisfied internally with the
419
- * core POSIX `Path.layer` this module already speaks POSIX relative match
420
- * paths and node-bound absolute paths throughout.
426
+ * unreadable directories. Matching semantics are {@link GLOB_OPTIONS}
427
+ * wildcards DO match dotfiles, aligning this module with `ConfigInspector`. An
428
+ * uncompilable pattern expands to no matches. The `Path` requirement is
429
+ * satisfied internally with the core POSIX `Path.layer` this module already
430
+ * speaks POSIX relative match paths and node-bound absolute paths throughout.
421
431
  *
422
432
  * @param source - The glob pattern, repo-relative
423
433
  * @param cwd - Absolute directory the pattern is resolved against
@@ -426,13 +436,11 @@ var VersionFiles = class VersionFiles {
426
436
  * @internal
427
437
  */
428
438
  function expandGlob(source, cwd) {
429
- return Effect.option(GlobPattern.compile(source)).pipe(Effect.flatMap(Option.match({
430
- onNone: () => Effect.succeed([]),
431
- onSome: (pattern) => descend(pattern, {
432
- cwd,
433
- onUnreadable: "skip"
434
- })
435
- })), Effect.provide(Path.layer));
439
+ return compileAndExpand(source, {
440
+ cwd,
441
+ onUnreadable: "skip",
442
+ glob: GLOB_OPTIONS
443
+ }).pipe(Effect.catch((error) => error.stage === "compile" ? Effect.succeed([]) : Effect.fail(error)), Effect.provide(Path.layer));
436
444
  }
437
445
  /**
438
446
  * Read the `version` field from a `package.json` in the given directory.
package/index.d.ts CHANGED
@@ -3,7 +3,7 @@ import { Plugin } from "unified";
3
3
  import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
4
4
  import { PackageManagerDetector, PublishConfig, PublishTarget, PublishabilityDetector, WorkspaceDiscovery, WorkspaceDiscoveryFailure, WorkspacePackage, WorkspaceRoot, WorkspaceSnapshotAtFailure, WorkspaceSnapshotWorktreeFailure, WorkspaceSnapshots, WorkspaceStateSnapshot, WorkspacesOptions } from "@effected/workspaces";
5
5
  import { Git } from "@effected/git";
6
- import { DescendError } from "@effected/walker";
6
+ import { GlobExpansionError } from "@effected/walker";
7
7
  //#endregion
8
8
  //#region src/changesets/categories/types.d.ts
9
9
  /**
@@ -4931,7 +4931,7 @@ declare class VersionFiles {
4931
4931
  * @param cwd - Project root directory
4932
4932
  * @returns Effect of `[filePath, config]` tuples
4933
4933
  */
4934
- static resolveGlobs(configs: readonly LegacyVersionFileConfig[], cwd: string): Effect.Effect<Array<[string, LegacyVersionFileConfig]>, DescendError, FileSystem.FileSystem>;
4934
+ static resolveGlobs(configs: readonly LegacyVersionFileConfig[], cwd: string): Effect.Effect<Array<[string, LegacyVersionFileConfig]>, GlobExpansionError, FileSystem.FileSystem>;
4935
4935
  /**
4936
4936
  * Detect indentation from file content.
4937
4937
  *
@@ -5033,7 +5033,7 @@ declare class VersionFiles {
5033
5033
  name: string;
5034
5034
  version: string;
5035
5035
  path: string;
5036
- }>): Effect.Effect<VersionFileUpdate[], DescendError, FileSystem.FileSystem>;
5036
+ }>): Effect.Effect<VersionFileUpdate[], GlobExpansionError, FileSystem.FileSystem>;
5037
5037
  /**
5038
5038
  * Apply version-file updates from the resolved (post-`ConfigInspector`)
5039
5039
  * representation. Each {@link ResolvedPackageScope} already names the
@@ -7284,7 +7284,7 @@ declare class Markdown {
7284
7284
  /**
7285
7285
  * Handler for package.json files.
7286
7286
  *
7287
- * Sorts fields with sort-package-json and formats with Biome.
7287
+ * Sorts fields with @effected/package-json and formats with Biome.
7288
7288
  *
7289
7289
  * @example
7290
7290
  * ```typescript
@@ -7353,12 +7353,20 @@ declare class PackageJson {
7353
7353
  * Sort the keys of a package.json document string.
7354
7354
  *
7355
7355
  * @remarks
7356
- * Pure transform over the file contents using `sort-package-json`. Used by
7357
- * the `savvy lint fmt package-json` subcommand so the CLI does not depend on
7358
- * `sort-package-json` directly.
7356
+ * Pure transform over the file contents using `@effected/package-json`'s
7357
+ * `PackageJsonFormat.formatToString` the tolerant text path, which sorts
7358
+ * canonically without decoding through the strict `Package` schema (so a
7359
+ * private or version-less root is sorted rather than rejected). Byte-verified
7360
+ * identical to the previous `sort-package-json` output on this repo's
7361
+ * manifests. Used by the `savvy lint fmt package-json` subcommand so the CLI
7362
+ * does not depend on a sorter directly.
7363
+ *
7364
+ * Unparseable content is returned UNCHANGED rather than throwing: a formatter
7365
+ * must not mangle a file it cannot read, and the surrounding Biome pass
7366
+ * reports the syntax error.
7359
7367
  *
7360
7368
  * @param content - The raw package.json file contents
7361
- * @returns The sorted package.json file contents
7369
+ * @returns The sorted package.json file contents, or `content` if unparseable
7362
7370
  */
7363
7371
  static sortContent(content: string): string;
7364
7372
  /**
@@ -7444,17 +7452,18 @@ declare class PnpmWorkspace {
7444
7452
  * {@link create} and the `savvy lint fmt pnpm-workspace` CLI subcommand route
7445
7453
  * through here so the two paths cannot drift.
7446
7454
  *
7447
- * `@effected/yaml` emits block sequences without the leading two-space
7448
- * indentation and prefers single-quoted scalars; Prettier's YAML printer
7449
- * normalizes both back to the v3 `yaml` posture (indented sequences,
7450
- * double-quoted scalars). Do NOT write `EffectedYaml.stringify` output
7451
- * directly — that regresses the file on every format pass.
7455
+ * {@link DEFAULT_STRINGIFY_OPTIONS} produces the repo's byte format directly.
7456
+ * The former Prettier post-process which existed only to re-indent block
7457
+ * sequences and re-quote scalars is gone: `indentSequences` and
7458
+ * `quoteStyle` now express both, so there is no second printer to drift from.
7459
+ *
7460
+ * Still `async` for source compatibility with existing callers; it performs
7461
+ * no asynchronous work and can become synchronous in the next major.
7452
7462
  *
7453
7463
  * @param content - Sorted pnpm-workspace.yaml content
7454
- * @param filepath - Path used to resolve Prettier config
7455
7464
  * @returns The formatted YAML source
7456
7465
  */
7457
- static formatContent(content: PnpmWorkspaceContent, filepath?: string): Promise<string>;
7466
+ static formatContent(content: PnpmWorkspaceContent): Promise<string>;
7458
7467
  /**
7459
7468
  * Create a handler that returns a CLI command to sort/format pnpm-workspace.yaml.
7460
7469
  *
@@ -10260,9 +10269,9 @@ declare const SilkPublishabilityDetectorLive: Layer.Layer<PublishabilityDetector
10260
10269
  *
10261
10270
  * @remarks Requires `FileSystem` and {@link ChangesetConfig} at build.
10262
10271
  * The kit's `detect` contract no longer receives the workspace root, so the changeset
10263
- * lookups derive it per package from the package's own discovery coordinates
10264
- * (`pkg.path` ascended by `pkg.relativePath` — never a filesystem marker walk,
10265
- * which could escape an unmarked root and read the wrong `.changeset/config.json`).
10272
+ * lookups read it from `pkg.workspaceRoot` the discovery root the package was found
10273
+ * against, never a filesystem marker walk, which could escape an unmarked root and read
10274
+ * the wrong `.changeset/config.json`.
10266
10275
  *
10267
10276
  * @since 0.4.0
10268
10277
  * @public
@@ -1,19 +1,20 @@
1
1
  import { Command } from "../utils/Command.js";
2
2
  import { Filter } from "../utils/Filter.js";
3
3
  import { isWorkspacePackagePath } from "../utils/Workspace.js";
4
+ import { Result } from "effect";
4
5
  import { readFileSync, writeFileSync } from "node:fs";
5
- import sortPackageJson from "sort-package-json";
6
+ import { PackageJsonFormat } from "@effected/package-json";
6
7
 
7
8
  //#region src/lint/handlers/PackageJson.ts
8
9
  /**
9
10
  * Handler for package.json files.
10
11
  *
11
- * Sorts fields with sort-package-json and formats with Biome.
12
+ * Sorts fields with @effected/package-json and formats with Biome.
12
13
  */
13
14
  /**
14
15
  * Handler for package.json files.
15
16
  *
16
- * Sorts fields with sort-package-json and formats with Biome.
17
+ * Sorts fields with @effected/package-json and formats with Biome.
17
18
  *
18
19
  * @example
19
20
  * ```typescript
@@ -91,15 +92,24 @@ var PackageJson = class PackageJson {
91
92
  * Sort the keys of a package.json document string.
92
93
  *
93
94
  * @remarks
94
- * Pure transform over the file contents using `sort-package-json`. Used by
95
- * the `savvy lint fmt package-json` subcommand so the CLI does not depend on
96
- * `sort-package-json` directly.
95
+ * Pure transform over the file contents using `@effected/package-json`'s
96
+ * `PackageJsonFormat.formatToString` the tolerant text path, which sorts
97
+ * canonically without decoding through the strict `Package` schema (so a
98
+ * private or version-less root is sorted rather than rejected). Byte-verified
99
+ * identical to the previous `sort-package-json` output on this repo's
100
+ * manifests. Used by the `savvy lint fmt package-json` subcommand so the CLI
101
+ * does not depend on a sorter directly.
102
+ *
103
+ * Unparseable content is returned UNCHANGED rather than throwing: a formatter
104
+ * must not mangle a file it cannot read, and the surrounding Biome pass
105
+ * reports the syntax error.
97
106
  *
98
107
  * @param content - The raw package.json file contents
99
- * @returns The sorted package.json file contents
108
+ * @returns The sorted package.json file contents, or `content` if unparseable
100
109
  */
101
110
  static sortContent(content) {
102
- return sortPackageJson(content);
111
+ const result = PackageJsonFormat.formatToString(content);
112
+ return Result.isSuccess(result) ? result.success : content;
103
113
  }
104
114
  /**
105
115
  * Create a handler with custom options.
@@ -116,7 +126,7 @@ var PackageJson = class PackageJson {
116
126
  if (filtered.length === 0) return [];
117
127
  if (!skipSort) for (const filepath of filtered) {
118
128
  const content = readFileSync(filepath, "utf-8");
119
- const sorted = sortPackageJson(content);
129
+ const sorted = PackageJson.sortContent(content);
120
130
  if (sorted !== content) writeFileSync(filepath, sorted, "utf-8");
121
131
  }
122
132
  if (skipFormat) return [];
@@ -2,7 +2,6 @@ import { Command } from "../utils/Command.js";
2
2
  import { Effect, Result } from "effect";
3
3
  import { existsSync, readFileSync, writeFileSync } from "node:fs";
4
4
  import { Yaml, YamlStringifyOptions } from "@effected/yaml";
5
- import { format, resolveConfig } from "prettier";
6
5
 
7
6
  //#region src/lint/handlers/PnpmWorkspace.ts
8
7
  /**
@@ -14,18 +13,21 @@ import { format, resolveConfig } from "prettier";
14
13
  * Default YAML stringify options for consistent formatting.
15
14
  *
16
15
  * @remarks
17
- * `lineWidth: 0` disables line wrapping, matching the v3 `yaml` package
18
- * behavior. `@effected/yaml` emits block sequences without the extra
19
- * two-space indentation the v3 `yaml` package used, so the stringified
20
- * output is normalized through Prettier's YAML printer below — probed
21
- * byte-identical to the v3 `yaml.stringify(..., { indent: 2, lineWidth: 0,
22
- * singleQuote: false })` output on this repo's real `pnpm-workspace.yaml`
23
- * and on hostile synthetic shapes (quoted keys, block scalars, nested
24
- * sequences of maps).
16
+ * `lineWidth: 0` disables line wrapping, `indentSequences: true` indents block
17
+ * sequences one level under their key, and `quoteStyle: "double"` selects the
18
+ * quote character for plain scalars that require quoting. Together these
19
+ * reproduce the v3 `yaml.stringify(..., { indent: 2, lineWidth: 0,
20
+ * singleQuote: false })` byte format directly probed byte-identical to the
21
+ * previous Prettier-normalized output on this repo's real
22
+ * `pnpm-workspace.yaml` and on hostile synthetic shapes (scoped package keys,
23
+ * block scalars, nested sequences of maps), which is what let the Prettier
24
+ * post-process be removed.
25
25
  */
26
26
  const DEFAULT_STRINGIFY_OPTIONS = new YamlStringifyOptions({
27
27
  indent: 2,
28
- lineWidth: 0
28
+ lineWidth: 0,
29
+ indentSequences: true,
30
+ quoteStyle: "double"
29
31
  });
30
32
  /**
31
33
  * Handler for pnpm-workspace.yaml.
@@ -108,22 +110,19 @@ var PnpmWorkspace = class PnpmWorkspace {
108
110
  * {@link create} and the `savvy lint fmt pnpm-workspace` CLI subcommand route
109
111
  * through here so the two paths cannot drift.
110
112
  *
111
- * `@effected/yaml` emits block sequences without the leading two-space
112
- * indentation and prefers single-quoted scalars; Prettier's YAML printer
113
- * normalizes both back to the v3 `yaml` posture (indented sequences,
114
- * double-quoted scalars). Do NOT write `EffectedYaml.stringify` output
115
- * directly — that regresses the file on every format pass.
113
+ * {@link DEFAULT_STRINGIFY_OPTIONS} produces the repo's byte format directly.
114
+ * The former Prettier post-process which existed only to re-indent block
115
+ * sequences and re-quote scalars is gone: `indentSequences` and
116
+ * `quoteStyle` now express both, so there is no second printer to drift from.
117
+ *
118
+ * Still `async` for source compatibility with existing callers; it performs
119
+ * no asynchronous work and can become synchronous in the next major.
116
120
  *
117
121
  * @param content - Sorted pnpm-workspace.yaml content
118
- * @param filepath - Path used to resolve Prettier config
119
122
  * @returns The formatted YAML source
120
123
  */
121
- static async formatContent(content, filepath = PnpmWorkspace.glob) {
122
- return format(Effect.runSync(Yaml.stringify(content, DEFAULT_STRINGIFY_OPTIONS)), {
123
- ...await resolveConfig(filepath),
124
- filepath,
125
- parser: "yaml"
126
- });
124
+ static async formatContent(content) {
125
+ return Effect.runSync(Yaml.stringify(content, DEFAULT_STRINGIFY_OPTIONS));
127
126
  }
128
127
  /**
129
128
  * Create a handler that returns a CLI command to sort/format pnpm-workspace.yaml.
@@ -164,7 +163,7 @@ var PnpmWorkspace = class PnpmWorkspace {
164
163
  let parsed = parseResult.success;
165
164
  if (!skipSort) parsed = PnpmWorkspace.sortContent(parsed);
166
165
  if (!skipSort || !skipFormat) {
167
- writeFileSync(filepath, await PnpmWorkspace.formatContent(parsed, filepath), "utf-8");
166
+ writeFileSync(filepath, await PnpmWorkspace.formatContent(parsed), "utf-8");
168
167
  return [];
169
168
  }
170
169
  return [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@savvy-web/silk-effects",
3
- "version": "4.1.0",
3
+ "version": "4.2.0",
4
4
  "private": false,
5
5
  "description": "Shared Effect library for Silk Suite conventions",
6
6
  "homepage": "https://github.com/savvy-web/systems/tree/main/packages/silk-effects",
@@ -34,12 +34,12 @@
34
34
  "@changesets/get-github-info": "^1.0.0-next.3",
35
35
  "@changesets/get-release-plan": "^5.0.0-next.7",
36
36
  "@effected/git": "^0.4.1",
37
- "@effected/glob": "^0.1.2",
38
- "@effected/jsonc": "^0.4.0",
39
- "@effected/package-json": "^0.3.1",
40
- "@effected/walker": "^0.2.2",
41
- "@effected/workspaces": "^0.4.1",
42
- "@effected/yaml": "^0.4.0",
37
+ "@effected/glob": "^0.2.0",
38
+ "@effected/jsonc": "^0.5.0",
39
+ "@effected/package-json": "^0.4.1",
40
+ "@effected/walker": "^0.3.1",
41
+ "@effected/workspaces": "^0.5.2",
42
+ "@effected/yaml": "^0.5.0",
43
43
  "@manypkg/get-packages": "^3.1.0",
44
44
  "mdast-util-heading-range": "^4.0.0",
45
45
  "mdast-util-to-string": "^4.0.0",
@@ -48,7 +48,6 @@
48
48
  "remark-parse": "^11.0.0",
49
49
  "remark-stringify": "^11.0.0",
50
50
  "shell-quote": "^1.10.0",
51
- "sort-package-json": "^4.0.0",
52
51
  "unified": "^11.0.5",
53
52
  "unified-lint-rule": "^3.0.1",
54
53
  "unist-util-visit": "^5.1.0",
@@ -244,34 +244,15 @@ const SilkPublishabilityDetectorLive = Layer.effect(PublishabilityDetector, Effe
244
244
  }) };
245
245
  }));
246
246
  /**
247
- * The workspace root a package was discovered against, derived from its own
248
- * coordinates: `relativePath` is the package directory relative to the
249
- * discovery root (POSIX, `"."` for the root package), so ascending one level
250
- * per path segment from `pkg.path` lands exactly on that root.
251
- *
252
- * @remarks
253
- * Derivation is deliberate — probing the filesystem for workspace markers
254
- * (`WorkspaceRoot.find`) can walk PAST the intended root when that root
255
- * carries no marker (a fixture tree, a bare directory) and land on an
256
- * enclosing workspace, silently swapping in that outer root's
257
- * `.changeset/config.json` and dropping the ignore/mode gating (the #209
258
- * regression class). The discovery root needs no probing: whoever built the
259
- * `WorkspacePackage` already knew it.
260
- */
261
- const packageRoot = (pkg) => {
262
- const segments = pkg.relativePath.split("/").filter((segment) => segment !== "" && segment !== ".");
263
- return segments.length === 0 ? pkg.path : join(pkg.path, ...segments.map(() => ".."));
264
- };
265
- /**
266
247
  * Ignore-aware override of `PublishabilityDetector`. `detect` short-circuits to `[]`
267
248
  * for changeset-ignored packages, then dispatches on `ChangesetConfig.mode`:
268
249
  * `none` → `[]`; `silk` → `SilkPublishability.detect`; `vanilla` → the library default.
269
250
  *
270
251
  * @remarks Requires `FileSystem` and {@link ChangesetConfig} at build.
271
252
  * The kit's `detect` contract no longer receives the workspace root, so the changeset
272
- * lookups derive it per package from the package's own discovery coordinates
273
- * (`pkg.path` ascended by `pkg.relativePath` — never a filesystem marker walk,
274
- * which could escape an unmarked root and read the wrong `.changeset/config.json`).
253
+ * lookups read it from `pkg.workspaceRoot` the discovery root the package was found
254
+ * against, never a filesystem marker walk, which could escape an unmarked root and read
255
+ * the wrong `.changeset/config.json`.
275
256
  *
276
257
  * @since 0.4.0
277
258
  * @public
@@ -281,7 +262,7 @@ const PublishabilityDetectorAdaptiveLive = Layer.effect(PublishabilityDetector,
281
262
  const config = yield* ChangesetConfig;
282
263
  const vanilla = yield* Effect.provide(PublishabilityDetector, PublishabilityDetector.layer);
283
264
  return { detect: (pkg) => Effect.gen(function* () {
284
- const root = packageRoot(pkg);
265
+ const root = pkg.workspaceRoot;
285
266
  if (yield* config.isIgnored(pkg.name, root)) return [];
286
267
  const mode = yield* config.mode(root);
287
268
  if (mode === "none") return [];