@effected/workspaces 0.28.0-next-20260927071821 → 0.29.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.d.ts CHANGED
@@ -560,6 +560,11 @@ interface WorkspaceDiscoveryOptions {
560
560
  * directory the caller names, which a single layer-level ceiling cannot
561
561
  * sensibly bound, so they ascend unbounded as before.
562
562
  *
563
+ * `LockfileReader`, `WorkspaceCatalogs` and `WorkspaceSnapshots` each
564
+ * take the same option; give them the same
565
+ * value when wiring by hand, or let a `Workspaces.*` composite forward one
566
+ * `stopAt` to all of them, so no service adopts a root another refused.
567
+ *
563
568
  * @defaultValue no ceiling — the ascent runs to the filesystem root.
564
569
  */
565
570
  readonly stopAt?: string | undefined;
@@ -1252,7 +1257,19 @@ export declare class ConfigDependencyHooks extends ConfigDependencyHooks_base {
1252
1257
  * with `reason: "integrityUnavailable"` — nothing is fetched in either case
1253
1258
  * — and a fetch that fails with `reason: "fetchFailed"`, keeping the
1254
1259
  * not-installed remediation. The fetch needs `pnpm` 11 or 12 on `PATH` and
1255
- * registry access, and is bounded at two minutes. An empty `configDependencies`, or one whose dependencies all
1260
+ * registry access, and is bounded at two minutes.
1261
+ *
1262
+ * The scratch workspace fetches through the same registries a
1263
+ * `pnpm install` in the workspace would: the workspace root's `.npmrc` (scoped
1264
+ * registries, mirrors, auth) is copied into it as-is, with `${VAR}`
1265
+ * references left for pnpm to expand, never read or logged, and removed
1266
+ * with the scratch; and the root `pnpm-workspace.yaml`'s `registry` and
1267
+ * `registries` keys are carried into the scratch's. That is the CURRENT
1268
+ * checkout's registry config for both sides of a diff — a base ref's
1269
+ * `.npmrc` is not read through git — and relative paths inside the `.npmrc`
1270
+ * (a `cafile=./ca.pem`) resolve against the scratch, not the root.
1271
+ *
1272
+ * An empty `configDependencies`, or one whose dependencies all
1256
1273
  * ship no pnpmfile, returns the seed without spawning anything; a `..` path
1257
1274
  * segment in a dependency name fails typed **before** any spawn; a
1258
1275
  * load failure in the child — a syntax error, a throwing top level, an
@@ -2054,6 +2071,21 @@ interface LockfileReaderOptions {
2054
2071
  * @defaultValue `process.cwd()`, read lazily on first use.
2055
2072
  */
2056
2073
  readonly cwd?: string;
2074
+ /**
2075
+ * A ceiling for the root ascent from `cwd`, passed straight through to the
2076
+ * `stopAt` of {@link WorkspaceRoot}'s `find`.
2077
+ *
2078
+ * @remarks
2079
+ * Inclusive, and resolved to an absolute path exactly as `cwd` is. When no
2080
+ * root is found at or below the ceiling, `read` fails with
2081
+ * {@link WorkspaceRootNotFoundError} carrying the resolved `stopAt` rather
2082
+ * than adopting an enclosing directory's workspace. Pass the same value as
2083
+ * {@link WorkspaceDiscoveryOptions.stopAt} so every service agrees on the
2084
+ * root; the `Workspaces.*` composites forward one `stopAt` to all of them.
2085
+ *
2086
+ * @defaultValue no ceiling — the ascent runs to the filesystem root.
2087
+ */
2088
+ readonly stopAt?: string | undefined;
2057
2089
  }
2058
2090
  declare const LockfileReader_base: Context.ServiceClass<LockfileReader, "@effected/workspaces/LockfileReader", LockfileReaderShape>;
2059
2091
  /**
@@ -2150,181 +2182,675 @@ export declare class LockfileReader extends LockfileReader_base {
2150
2182
  static readonly layerTest: (overrides?: Partial<LockfileReaderShape>) => Layer.Layer<LockfileReader>;
2151
2183
  }
2152
2184
  //#endregion
2153
- //#region src/PeerCheck.d.ts
2154
- /**
2155
- * Why a report is not a complete answer.
2156
- *
2157
- * @remarks
2158
- * Closed at exactly two, by measurement rather than by guess:
2159
- *
2160
- * - `"peerRulesNotApplied"` — the effective suppression policy was not applied,
2161
- * so pnpm's post-hoc suppression could not be replicated and some reported
2162
- * rows may be ones pnpm hides. **Omitting the option always produces this**,
2163
- * because "nobody looked" and "I looked and there are none" are different
2164
- * facts. Supplied rules never produce it: all three axes —
2165
- * `allowedVersions`, `ignoreMissing` and `allowAny` — are applied with
2166
- * pnpm's measured semantics.
2167
- * - `"unresolvedEdge"` — some instance records an edge this model could not
2168
- * name (`ResolvedPackage.unresolvedEdges`), so a peer that edge satisfies
2169
- * cannot be verified either way.
2170
- *
2171
- * Both mean **fail closed**: a gate should treat an unverified report as "not
2172
- * proven clean" rather than as a pass.
2173
- *
2174
- * @public
2175
- */
2176
- type UnverifiedReason = "peerRulesNotApplied" | "unresolvedEdge";
2185
+ //#region src/WorkspaceCatalogs.d.ts
2177
2186
  /**
2178
- * Options for {@link PeerCheck.run}.
2187
+ * Each importer's dependency-name → resolved-version map, keyed by importer path
2188
+ * (`"."` for the root package — the same keys `WorkspaceDiscovery.importerMap()`
2189
+ * uses, and the same value `PackageStateSnapshot.relativePath` carries).
2179
2190
  *
2180
2191
  * @remarks
2181
- * **Presence of `peerDependencyRules` is the assertion; its contents are the
2182
- * value.** Supplying `NoPeerDependencyRules` asserts the workspace has none and
2183
- * yields a verified report; omitting the key entirely says nothing was looked
2184
- * up and always yields `"peerRulesNotApplied"`. The two are deliberately
2185
- * different results, because a gate must be able to tell "clean" from
2186
- * "unchecked".
2187
- *
2188
- * **All three axes of the supplied rules are applied**, each with the
2189
- * semantics measured against pnpm (see {@link PeerCheck.run}): `allowedVersions`
2190
- * permits a resolved version by range, `ignoreMissing` hides a required peer
2191
- * nothing resolved for, and `allowAny` hides a peer that resolved outside its
2192
- * range. The two list axes are name patterns, never `parent>peer` keys.
2193
- *
2194
- * @public
2195
- */
2196
- interface PeerCheckOptions {
2197
- /** The workspace's effective rules, from `WorkspaceCatalogs.peerDependencyRules()`. */
2198
- readonly peerDependencyRules?: PeerDependencyRules;
2199
- }
2200
- declare const PeerParent_base: Schema.Class<PeerParent, Schema.Struct<{
2201
- /** The package name. */
2202
- readonly name: Schema.NonEmptyString;
2203
- /** The resolved version of that package. */
2204
- readonly version: Schema.String;
2205
- }>, {}>;
2206
- /**
2207
- * One link in the chain from an importer to the package that declared an
2208
- * unsatisfied peer.
2192
+ * What a manager's lockfile records as actually installed, as opposed to what a
2193
+ * manifest declares. It answers a `catalog:` specifier no committed catalog
2194
+ * source declares — the shape a pnpm config-dependency pnpmfile hook produces —
2195
+ * which would otherwise resolve to nothing on both sides of a diff and hide a
2196
+ * real version movement.
2209
2197
  *
2210
- * @remarks
2211
- * Mirrors the shape `pnpm peers check --json` reports, so the two can be
2212
- * compared directly.
2198
+ * Versions are normalized: pnpm's peer-disambiguation suffix
2199
+ * (`1.2.3(effect@4.0.0)`) is stripped, and `link:` / `file:` entries are omitted
2200
+ * because a filesystem edge is not a version. Only pnpm records importer
2201
+ * versions; bun and npm yield an empty index.
2213
2202
  *
2214
2203
  * @public
2215
2204
  */
2216
- export declare class PeerParent extends PeerParent_base {}
2217
- declare const UnsatisfiedPeer_base: Schema.Class<UnsatisfiedPeer, Schema.Struct<{
2218
- /** The importer path the problem belongs to (`"."` for the root). */
2219
- readonly importer: Schema.NonEmptyString;
2220
- /** The peer dependency's name. */
2221
- readonly dependency: Schema.NonEmptyString;
2222
- /** The range the declaring package asked for. */
2223
- readonly wanted: Schema.String;
2224
- /** The version that resolved, or `null` when nothing resolved. */
2225
- readonly found: Schema.NullOr<Schema.String>;
2226
- /** Whether the declaring package marked the peer optional. */
2227
- readonly optional: Schema.Boolean;
2228
- /** The path from the importer to the declaring package. */
2229
- readonly parents: Schema.$Array<typeof PeerParent>;
2205
+ type ImporterVersions = Readonly<Record<string, Readonly<Record<string, string>>>>;
2206
+ declare const CatalogSet_base: Schema.Class<CatalogSet, Schema.Struct<{
2207
+ /** Catalog name → dependency name → version range. */
2208
+ readonly entries: Schema.$Record<Schema.String, Schema.$Record<Schema.String, Schema.String>>;
2230
2209
  }>, {}>;
2231
2210
  /**
2232
- * One peer dependency that is declared but not satisfied.
2211
+ * An immutable, fully-normalized catalog collection — the one catalog
2212
+ * resolution semantic in the package.
2233
2213
  *
2234
2214
  * @remarks
2235
- * `found` carries the version that actually resolved, or `null` when nothing
2236
- * resolved at all. There is deliberately **no separate discriminant** between
2237
- * "missing" and "unmet": `found === null` is the whole distinction, and a
2238
- * renderer reads it directly.
2239
- *
2240
- * `optional` must be respected by any gate — an unsatisfied *optional* peer is
2241
- * normal and should not fail a build, which is why the flag travels with the
2242
- * row rather than being filtered out here. {@link PeerCheck.required} is the
2243
- * convenience for the common case.
2244
- *
2245
- * `parents` is the path from the importer down to the package that declared
2246
- * the peer, nearest-to-the-importer first. It is a path rather than a single
2247
- * package because a peer declared by a transitive dependency is still that
2248
- * importer's problem, and the chain is what makes the report actionable.
2249
- *
2250
- * A package an importer reaches by more than one path yields **one** row, not
2251
- * one per path, carrying the path the walk reached first — which is what
2252
- * `pnpm peers check` reports for the same graph. Read `parents` as *a* route to
2253
- * the declaring package, never as the complete set of them.
2215
+ * `entries` is catalog name → dependency → range, with pnpm's unnamed
2216
+ * top-level `catalog:` block normalized under the key `"default"`. Lockfile
2217
+ * catalog entries, which pnpm records as `{ specifier, version }`, are
2218
+ * normalized to their `specifier` — the declared range, which is what a catalog
2219
+ * resolves to.
2254
2220
  *
2255
2221
  * @public
2256
2222
  */
2257
- export declare class UnsatisfiedPeer extends UnsatisfiedPeer_base {}
2258
- declare const PeerCheck_base: Schema.Class<PeerCheck, Schema.Struct<{
2223
+ export declare class CatalogSet extends CatalogSet_base {
2224
+ /** The empty set — a workspace with no catalogs. */
2225
+ static empty(): CatalogSet;
2226
+ /** Wrap a pnpm `Catalogs` map, dropping unusable entries. */
2227
+ static fromCatalogs(catalogs: unknown): CatalogSet;
2259
2228
  /**
2260
- * Whether the lockfile's format records peer resolution at all.
2229
+ * The `catalog:` and `catalogs:` blocks of a `pnpm-workspace.yaml` document.
2261
2230
  *
2262
- * `false` for yarn: yarn resolves peers **virtually**, giving a
2263
- * peer-bearing package one `@virtual:` locator per consumer, and the
2264
- * lockfile does not record which virtual instance satisfied which peer.
2265
- * The answer is not recoverable, so it is not fabricated — `unsatisfied`
2266
- * is empty and this flag says why.
2231
+ * @param text - The raw YAML text.
2267
2232
  */
2268
- readonly supported: Schema.Boolean;
2269
- /** Every unsatisfied peer found, optional ones included. */
2270
- readonly unsatisfied: Schema.$Array<typeof UnsatisfiedPeer>;
2233
+ static readonly fromWorkspaceYaml: (text: string) => Effect.Effect<CatalogSet, CatalogAssemblyError, never>;
2271
2234
  /**
2272
- * Importers whose dependencies could not be resolved to instances, so no
2273
- * verdict was reached for them.
2235
+ * The `catalogs:` section of a pnpm lockfile, whose entries are either a bare
2236
+ * range or a `{ specifier, version }` pair.
2237
+ */
2238
+ static fromLockfileCatalogs(raw: unknown): CatalogSet;
2239
+ /**
2240
+ * The catalog set a parsed lockfile records, PM-aware.
2274
2241
  *
2275
2242
  * @remarks
2276
- * In practice this is the **root importer under npm and bun**: neither
2277
- * records a resolved version per importer dependency, and neither emits a
2278
- * package row for the root, so there is nothing to join on. pnpm records a
2279
- * version per importer dependency and is unaffected.
2243
+ * Both pnpm and bun record catalogs in the lockfile, in different shapes:
2244
+ * pnpm under `extension.catalogs`, bun under `extension.catalog` /
2245
+ * `extension.catalogs`. Assembly reads whichever the parsed lockfile carries
2246
+ * rather than assuming the pnpm extension. A lockfile with no extension, or a
2247
+ * pnpm one with no catalogs, yields the empty set.
2280
2248
  *
2281
- * Reported rather than silently skipped, for the same reason `supported`
2282
- * exists: a gate that sees no rows is entitled to know whether that means
2283
- * "clean" or "not looked at".
2249
+ * @param lockfile - A parsed lockfile from `@effected/lockfiles`.
2284
2250
  */
2285
- readonly unresolvedImporters: Schema.$Array<Schema.String>;
2251
+ static fromLockfile(lockfile: Lockfile): CatalogSet;
2286
2252
  /**
2287
- * Why this report is not a complete answer, or empty when it is.
2253
+ * A catalog set from bun's `{ catalog, catalogs }` blocks — used for the
2254
+ * `bun.lock` extension and for the root `package.json` `workspaces` block. The
2255
+ * unnamed default catalog normalizes under `"default"`. Tolerant: unusable
2256
+ * values are dropped, never fatal.
2257
+ *
2258
+ * @param blocks - The `catalog` (default) and `catalogs` (named) blocks.
2259
+ */
2260
+ static fromBunBlocks(blocks: {
2261
+ readonly catalog?: unknown;
2262
+ readonly catalogs?: unknown;
2263
+ }): CatalogSet;
2264
+ /**
2265
+ * The `catalog` / `catalogs` blocks of a root `package.json` `workspaces` field
2266
+ * — bun's package.json analogue of pnpm's `pnpm-workspace.yaml` blocks.
2288
2267
  *
2289
2268
  * @remarks
2290
- * See {@link UnverifiedReason}. A gate must treat a non-empty `unverified`
2291
- * as **not proven clean** rather than as a pass: both reasons mean some
2292
- * finding may be missing or spurious, and failing closed is the requirement.
2269
+ * **Hard-fail by design**, preserving the semantics catalog output is
2270
+ * load-bearing for: a present-but-malformed `workspaces` shape (a number, a
2271
+ * string, an object with a malformed `packages` / `catalog` / `catalogs`), or
2272
+ * the default catalog declared twice — once as `workspaces.catalog` and again
2273
+ * as `workspaces.catalogs.default` — fails with `CatalogAssemblyError`
2274
+ * naming what was wrong. An absent `workspaces` field, one explicitly `null`,
2275
+ * or the plain array form (npm/yarn patterns, which carry no catalogs) yields
2276
+ * the empty set. Presence is checked structurally, so an explicitly-declared
2277
+ * empty `catalog: {}` still counts as a declaration.
2278
+ *
2279
+ * @param text - The raw root `package.json` text.
2293
2280
  */
2294
- readonly unverified: Schema.$Array<Schema.Literals<readonly ["peerRulesNotApplied", "unresolvedEdge"]>>;
2295
- }>, {}>;
2281
+ static readonly fromManifestWorkspaces: (text: string) => Effect.Effect<CatalogSet, CatalogAssemblyError, never>;
2282
+ /** Merge sets. Later sets win per dependency within a catalog. */
2283
+ static merge(...sets: ReadonlyArray<CatalogSet>): CatalogSet;
2284
+ /** Whether any catalog declares anything. */
2285
+ get isEmpty(): boolean;
2286
+ /**
2287
+ * The range a `catalog:` specifier resolves to.
2288
+ *
2289
+ * @remarks
2290
+ * Total: an unmatched dependency, an unknown catalog name, or a non-catalog
2291
+ * specifier all yield `Option.none()`. A *malformed* catalog — one pnpm
2292
+ * itself rejects — also yields `Option.none()` here; the fallible surface is
2293
+ * {@link WorkspaceCatalogs}, which fails typed on assembly instead.
2294
+ *
2295
+ * @param dependency - The package name being resolved.
2296
+ * @param specifier - The declared specifier, e.g. `catalog:` or `catalog:build`.
2297
+ */
2298
+ resolveSpecifier(dependency: string, specifier: string): Option.Option<string>;
2299
+ /**
2300
+ * The range `dependency` carries in a named catalog, or in the default
2301
+ * catalog when `catalog` is `Option.none()`.
2302
+ *
2303
+ * @remarks
2304
+ * The shape `@effected/npm`'s `CatalogResolver` contract asks for.
2305
+ */
2306
+ rangeOf(dependency: string, catalog: Option.Option<string>): Option.Option<string>;
2307
+ }
2296
2308
  /**
2297
- * The result of checking a lockfile for unsatisfied peer dependencies.
2298
- *
2299
- * @remarks
2300
- * A report rather than a bare array, because an empty array is the most
2301
- * dangerous success shape this domain has: it is indistinguishable from "this
2302
- * format cannot be checked". `supported` and `unresolvedImporters` make the
2303
- * difference legible, so a consumer cannot read a limitation as a clean bill
2304
- * of health.
2305
- *
2306
- * Pure and total — construct it with {@link PeerCheck.run}, which never fails.
2307
- *
2308
- * @example
2309
- * ```ts
2310
- * import { PeerCheck } from "@effected/workspaces";
2311
- * import { Lockfile } from "@effected/lockfiles";
2312
- * import { Effect } from "effect";
2309
+ * Every failure catalog assembly can surface.
2313
2310
  *
2314
- * const program = Effect.gen(function* () {
2315
- * const lockfile = yield* Lockfile.parse(text, { format: "pnpm" });
2316
- * const report = PeerCheck.run(lockfile);
2317
- * return report.supported ? report.required : [];
2318
- * });
2319
- * ```
2311
+ * @public
2312
+ */
2313
+ type CatalogAssemblyFailure = CatalogAssemblyError | WorkspaceRootNotFoundError;
2314
+ /**
2315
+ * The {@link WorkspaceCatalogs} service shape.
2320
2316
  *
2321
2317
  * @public
2322
2318
  */
2323
- export declare class PeerCheck extends PeerCheck_base {
2324
- /** The unsatisfied peers a gate should act on — the non-optional ones. */
2325
- get required(): ReadonlyArray<UnsatisfiedPeer>;
2319
+ interface WorkspaceCatalogsShape {
2320
+ /** The assembled catalog set for the workspace. Memoized after the first call. */
2321
+ readonly set: () => Effect.Effect<CatalogSet, CatalogAssemblyFailure>;
2322
+ /** Resolve one `catalog:` specifier; `Option.none()` when it names nothing. */
2323
+ readonly resolveSpecifier: (dependency: string, specifier: string) => Effect.Effect<Option.Option<string>, CatalogAssemblyFailure>;
2326
2324
  /**
2327
- * Compute the unsatisfied peer dependencies of a parsed lockfile.
2325
+ * The effective pnpm release-age gate for the workspace, combined
2326
+ * strictest-wins from the inline `pnpm-workspace.yaml` keys
2327
+ * (`minimumReleaseAge` / `minimumReleaseAgeExclude`) and the replayed
2328
+ * config-dependency hooks. Assembled from the same single read and hook
2329
+ * replay as `set`, and memoized with it.
2330
+ *
2331
+ * @remarks
2332
+ * Under the default layer (no-op hooks) only inline values contribute; under
2333
+ * {@link WorkspaceCatalogs.layerWithConfigDependencies} the replayed hooks
2334
+ * contribute too. A workspace with no pnpm-workspace.yaml (a bun/npm
2335
+ * workspace) has no release-age keys, so the gate is the inert zero gate.
2336
+ */
2337
+ readonly releaseAgeGate: () => Effect.Effect<ReleaseAgeGate, CatalogAssemblyFailure>;
2338
+ /**
2339
+ * The workspace's **effective** `peerDependencyRules` — pnpm's post-hoc
2340
+ * suppression policy, which the lockfile does not record at all.
2341
+ *
2342
+ * @remarks
2343
+ * Assembled from the same single read and hook replay as `set`, and memoized
2344
+ * with it. The `pnpm-workspace.yaml` block (what `pnpm:export` materializes)
2345
+ * is **seeded** into the replayed config and the config-dependency hooks
2346
+ * merge onto it, so one object carries both sources — the kit never merges
2347
+ * them itself, because that would be a second implementation of a rule the
2348
+ * hook seam already enforces.
2349
+ *
2350
+ * A consumer needs these to avoid **reporting** what pnpm suppresses: pnpm
2351
+ * computes the same peer violations and then hides the ones a rule allows,
2352
+ * so a checker without them reports findings pnpm calls clean. `PeerCheck`
2353
+ * applies all three axes.
2354
+ */
2355
+ readonly peerDependencyRules: () => Effect.Effect<PeerDependencyRules, CatalogAssemblyFailure>;
2356
+ /**
2357
+ * Each importer's dependency-name → resolved-version map, as the manager's
2358
+ * lockfile records it. Read from the same single lockfile read as `set`, and
2359
+ * memoized with it.
2360
+ *
2361
+ * @remarks
2362
+ * Feeds `WorkspaceStateSnapshot`'s fallback for a `catalog:` specifier no
2363
+ * committed catalog source declares — the shape a config-dependency pnpmfile
2364
+ * hook produces. Only pnpm records importer versions; a bun or npm workspace
2365
+ * yields an empty index, and an absent or unreadable lockfile contributes
2366
+ * nothing rather than failing.
2367
+ */
2368
+ readonly importerVersions: () => Effect.Effect<ImporterVersions, CatalogAssemblyFailure>;
2369
+ /**
2370
+ * Which version each declared config dependency was replayed from, keyed
2371
+ * by name — off the same single memoized assemble pass as `set()`.
2372
+ *
2373
+ * @remarks
2374
+ * Every dependency the hooks layer resolved when a `pnpm-workspace.yaml`
2375
+ * was read — `{}` when it declares no config dependencies or under the
2376
+ * no-op layer, which resolves nothing. Config dependencies are a pnpm
2377
+ * feature, so the bun / `package.json` path yields `{}` too, exactly as
2378
+ * `importerVersions` does.
2379
+ */
2380
+ readonly hookReplays: () => Effect.Effect<Readonly<Record<string, HookReplay>>, CatalogAssemblyFailure>;
2381
+ /**
2382
+ * Discard the memoized assembly so the **next** read re-assembles — the same
2383
+ * single read and hook replay as the first, over the workspace as it stands
2384
+ * then.
2385
+ *
2386
+ * @remarks
2387
+ * The explicit memoization boundary for a tool that **mutates the workspace
2388
+ * mid-run** — installs, bumps a config dependency, regenerates the lockfile.
2389
+ * Release-age gating wants the before-state, a post-install peer check the
2390
+ * after-state, and one infinite memo cannot serve both without this call in
2391
+ * between; without it, `peerDependencyRules` keeps answering from the
2392
+ * pre-mutation hook replay and the checker reports findings the new rules
2393
+ * suppress.
2394
+ *
2395
+ * Unconditional and infallible: the memo is already success-only (a failed
2396
+ * or interrupted assembly retries by itself), so `refresh` exists solely to
2397
+ * discard a *successful* assembly that mutation has made stale. Calling it
2398
+ * before any read is harmless.
2399
+ */
2400
+ readonly refresh: () => Effect.Effect<void>;
2401
+ }
2402
+ /**
2403
+ * Options for the {@link WorkspaceCatalogs} layer.
2404
+ *
2405
+ * @public
2406
+ */
2407
+ interface WorkspaceCatalogsOptions {
2408
+ /**
2409
+ * The directory the workspace root is resolved from.
2410
+ *
2411
+ * @defaultValue `process.cwd()`, read lazily on first use.
2412
+ */
2413
+ readonly cwd?: string;
2414
+ /**
2415
+ * A ceiling for the root ascent from `cwd`, passed straight through to the
2416
+ * `stopAt` of {@link WorkspaceRoot}'s `find`.
2417
+ *
2418
+ * @remarks
2419
+ * Inclusive, and resolved to an absolute path exactly as `cwd` is. When no
2420
+ * root is found at or below the ceiling, catalog assembly fails with
2421
+ * {@link WorkspaceRootNotFoundError} carrying the resolved `stopAt` rather
2422
+ * than adopting an enclosing directory's workspace. Pass the same value as
2423
+ * {@link WorkspaceDiscoveryOptions.stopAt} so every service agrees on the
2424
+ * root; the `Workspaces.*` composites forward one `stopAt` to all of them.
2425
+ *
2426
+ * @defaultValue no ceiling — the ascent runs to the filesystem root.
2427
+ */
2428
+ readonly stopAt?: string | undefined;
2429
+ }
2430
+ declare const WorkspaceCatalogs_base: Context.ServiceClass<WorkspaceCatalogs, "@effected/workspaces/WorkspaceCatalogs", WorkspaceCatalogsShape>;
2431
+ /**
2432
+ * Assembles a workspace's catalogs, package-manager-aware.
2433
+ *
2434
+ * @remarks
2435
+ * The inline source is picked by **file presence**, the same rule
2436
+ * `internal/patterns.ts` uses for globs: a `pnpm-workspace.yaml` selects the pnpm
2437
+ * path (its `catalog:` / `catalogs:` blocks); its absence selects the root
2438
+ * `package.json` `workspaces.catalog` / `workspaces.catalogs` (bun's analogue).
2439
+ * The lockfile source is PM-aware too — whichever extension the parsed lockfile
2440
+ * carries (pnpm or bun) contributes its recorded catalogs.
2441
+ *
2442
+ * Precedence, lowest first: catalogs recorded in the lockfile, then the inline
2443
+ * declaration, then — only under {@link WorkspaceCatalogs.layerWithConfigDependencies}
2444
+ * — the config-dependency `pnpmfile.cjs` hooks, each layer winning per dependency
2445
+ * within a catalog. An unreadable or absent lockfile degrades to no lockfile
2446
+ * catalogs rather than failing (the inline declaration is the source of truth, the
2447
+ * lockfile a record of what was installed); a malformed inline source **fails
2448
+ * typed** with `CatalogAssemblyError`, because a silently-empty catalog read
2449
+ * is the "every dependency looks newly added" bug.
2450
+ *
2451
+ * Assembly is deferred to the first call and memoized success-only, matching
2452
+ * {@link WorkspaceDiscovery}.
2453
+ *
2454
+ * @public
2455
+ */
2456
+ export declare class WorkspaceCatalogs extends WorkspaceCatalogs_base {
2457
+ /** Builds the service. */
2458
+ static readonly make: (options?: WorkspaceCatalogsOptions) => Effect.Effect<WorkspaceCatalogsShape, never, WorkspaceRoot | LockfileReader | ConfigDependencyHooks | FileSystem.FileSystem | Path.Path>;
2459
+ /**
2460
+ * The service over an explicit {@link ConfigDependencyHooks} layer — the
2461
+ * one builder every `layer*` static and the `Workspaces` composites
2462
+ * delegate to, so the hooks layer is provided to {@link WorkspaceCatalogs.make}
2463
+ * in exactly one place.
2464
+ *
2465
+ * @remarks
2466
+ * Reach for it when the hooks layer is chosen elsewhere — a composite that
2467
+ * must hand ONE hooks reference to both this service and
2468
+ * `WorkspaceSnapshots` (layers memoize by reference, so two references
2469
+ * would replay twice and could run two policies), or a test wiring
2470
+ * {@link ConfigDependencyHooks.layerFrom}. The three `layer*` statics are
2471
+ * this with the hooks layer fixed. `R` is the hooks layer's own
2472
+ * requirement (the subprocess variant's `ChildProcessSpawner`), threaded
2473
+ * through. Parameterized, so bind it to a `const` and reuse it.
2474
+ *
2475
+ * @param hooks - The {@link ConfigDependencyHooks} layer to replay through.
2476
+ * @param options - The same options the other layer statics take.
2477
+ */
2478
+ static readonly layerWithHooks: <R = never>(hooks: Layer.Layer<ConfigDependencyHooks, never, R>, options?: WorkspaceCatalogsOptions) => Layer.Layer<WorkspaceCatalogs, never, WorkspaceRoot | LockfileReader | FileSystem.FileSystem | Path.Path | R>;
2479
+ /**
2480
+ * The live layer — the default, which **never executes config-dependency
2481
+ * code**: it wires {@link ConfigDependencyHooks.layerNoop}, whose hooks return
2482
+ * the inline-catalog seed untouched.
2483
+ *
2484
+ * @remarks
2485
+ * Parameterized, so it mints a fresh reference per call — bind it to a
2486
+ * `const` and reuse it, or layer memoization does not apply.
2487
+ */
2488
+ static readonly layer: (options?: WorkspaceCatalogsOptions) => Layer.Layer<WorkspaceCatalogs, never, WorkspaceRoot | LockfileReader | FileSystem.FileSystem | Path.Path>;
2489
+ /**
2490
+ * The opt-in live layer that **does** replay config-dependency `pnpmfile.cjs`
2491
+ * hooks: it wires {@link ConfigDependencyHooks.layerLive}, which dynamically
2492
+ * imports and runs each config dependency's `updateConfig` in process.
2493
+ *
2494
+ * @remarks
2495
+ * Same output and requirement set as {@link WorkspaceCatalogs.layer} — the only
2496
+ * difference is that config-dependency code is executed. Use it deliberately;
2497
+ * the default catalog path stays free of any config-dependency execution.
2498
+ * Parameterized, so bind it to a `const` and reuse it.
2499
+ */
2500
+ static readonly layerWithConfigDependencies: (options?: WorkspaceCatalogsOptions) => Layer.Layer<WorkspaceCatalogs, never, WorkspaceRoot | LockfileReader | FileSystem.FileSystem | Path.Path>;
2501
+ /**
2502
+ * The opt-in layer that replays config-dependency `pnpmfile` hooks in a
2503
+ * `node` **child process**: it wires
2504
+ * {@link ConfigDependencyHooks.layerSubprocess} in place of the in-process
2505
+ * `layerLive`.
2506
+ *
2507
+ * @remarks
2508
+ * Same typed semantics as {@link WorkspaceCatalogs.layerWithConfigDependencies}
2509
+ * — the two hook layers are drop-in interchangeable — but the replay's
2510
+ * computed dynamic `import()` runs in the subprocess, so it survives bundling
2511
+ * (a bundler compiles a computed in-process `import()` into a context module
2512
+ * that cannot resolve at runtime — every bundled GitHub Action hits this).
2513
+ * The cost is one extra requirement: core's `ChildProcessSpawner`, provided
2514
+ * once at the edge (`@effect/platform-node`'s `NodeServices.layer`) — the
2515
+ * same sanctioned R-widening as `Workspaces.layerWithGit`. Parameterized, so
2516
+ * bind it to a `const` and reuse it.
2517
+ */
2518
+ static readonly layerWithConfigDependenciesSubprocess: (options?: WorkspaceCatalogsOptions) => Layer.Layer<WorkspaceCatalogs, never, WorkspaceRoot | LockfileReader | FileSystem.FileSystem | Path.Path | ChildProcessSpawner.ChildProcessSpawner>;
2519
+ /**
2520
+ * A test double satisfying the full {@link WorkspaceCatalogsShape} with no
2521
+ * filesystem, lockfile read, or hook replay.
2522
+ *
2523
+ * @remarks
2524
+ * There is **no honest default catalog set**: an empty `CatalogSet` that
2525
+ * looks like a legitimate answer is the "every dependency looks newly added"
2526
+ * failure class the live assembler hard-fails to prevent, so every method
2527
+ * **dies** with an instructive defect until stubbed — a test-wiring mistake
2528
+ * fails loudly as a defect rather than succeeding with a lie or failing with
2529
+ * a dishonest typed error.
2530
+ *
2531
+ * The one derivation mirrors `WorkspaceDiscovery.makeTest`'s
2532
+ * derived-from-the-primary rule: when a `set` override is supplied,
2533
+ * `resolveSpecifier` answers from that `CatalogSet`'s own
2534
+ * {@link CatalogSet.resolveSpecifier} — exactly what the live service runs
2535
+ * over its assembled set — so the two stay consistent by construction.
2536
+ * `releaseAgeGate`, `importerVersions` and `hookReplays` are **not**
2537
+ * derivable from a catalog set (the gate comes from release-age keys and
2538
+ * hook contributions, the importer index from the lockfile's importer
2539
+ * blocks, the replay record from the hooks layer — none is in a
2540
+ * `CatalogSet`) and always die unless stubbed.
2541
+ *
2542
+ * `refresh` defaults to `Effect.void` honestly: the double holds no memo,
2543
+ * so "drop the memoized assembly" is genuinely a no-op — the stubs answer
2544
+ * fresh on every call already. This mirrors `WorkspaceDiscovery.makeTest`'s
2545
+ * `refresh`.
2546
+ *
2547
+ * @example
2548
+ * ```ts
2549
+ * import { CatalogSet, WorkspaceCatalogs } from "@effected/workspaces";
2550
+ * import { Effect } from "effect";
2551
+ *
2552
+ * const double = WorkspaceCatalogs.makeTest({
2553
+ * set: () => Effect.succeed(CatalogSet.fromCatalogs({ default: { effect: "4.0.0" } })),
2554
+ * });
2555
+ * // `resolveSpecifier` now answers consistently from that set.
2556
+ * ```
2557
+ */
2558
+ static readonly makeTest: (overrides?: Partial<WorkspaceCatalogsShape>) => WorkspaceCatalogsShape;
2559
+ /**
2560
+ * The test layer: {@link WorkspaceCatalogs.makeTest} behind `Layer.succeed`,
2561
+ * so a suite provides only the methods it exercises.
2562
+ *
2563
+ * @remarks
2564
+ * A parameterized layer factory mints a **fresh reference per call**, and
2565
+ * layers memoize by reference — bind the result to a `const` and reuse it
2566
+ * rather than calling `layerTest(...)` at each composition site.
2567
+ *
2568
+ * @example
2569
+ * ```ts
2570
+ * import { CatalogSet, WorkspaceCatalogs } from "@effected/workspaces";
2571
+ * import { Effect } from "effect";
2572
+ *
2573
+ * const TestCatalogs = WorkspaceCatalogs.layerTest({
2574
+ * set: () => Effect.succeed(CatalogSet.empty()),
2575
+ * });
2576
+ * // program.pipe(Effect.provide(TestCatalogs))
2577
+ * ```
2578
+ */
2579
+ static readonly layerTest: (overrides?: Partial<WorkspaceCatalogsShape>) => Layer.Layer<WorkspaceCatalogs>;
2580
+ /**
2581
+ * The real implementation of `@effected/npm`'s `CatalogResolver` contract —
2582
+ * the one `@effected/package-json` declares but cannot fill.
2583
+ *
2584
+ * @remarks
2585
+ * `rangeOf` returns `Option.none()` for a dependency no catalog declares, per
2586
+ * the contract's convention. A failed catalog *assembly* — an unreadable or
2587
+ * malformed `pnpm-workspace.yaml`, a broken config-dependency hook — passes
2588
+ * through **typed** as the contract's `CatalogAssemblyError` (it used to be
2589
+ * folded into `DependencyResolutionError`'s defect `cause`, which forced
2590
+ * consumers to `_tag`-sniff `unknown`); only the remaining mechanism failure,
2591
+ * an unfindable workspace root, is wrapped as `DependencyResolutionError`.
2592
+ */
2593
+ static readonly catalogResolver: Layer.Layer<CatalogResolver, never, WorkspaceCatalogs>;
2594
+ }
2595
+ //#endregion
2596
+ //#region src/PeerCheck.d.ts
2597
+ /**
2598
+ * Why a report is not a complete answer.
2599
+ *
2600
+ * @remarks
2601
+ * Closed at exactly four, by measurement rather than by guess:
2602
+ *
2603
+ * - `"peerRulesNotApplied"` — the effective suppression policy was not applied,
2604
+ * so pnpm's post-hoc suppression could not be replicated and some reported
2605
+ * rows may be ones pnpm hides. **Omitting the option always produces this**,
2606
+ * because "nobody looked" and "I looked and there are none" are different
2607
+ * facts. Supplied rules never produce it: all three axes —
2608
+ * `allowedVersions`, `ignoreMissing` and `allowAny` — are applied with
2609
+ * pnpm's measured semantics.
2610
+ * - `"unresolvedEdge"` — some instance records an edge this model could not
2611
+ * name (`ResolvedPackage.unresolvedEdges`), or an importer dependency
2612
+ * resolved through `link:` whose target the caller did not supply, so its
2613
+ * manifest peers are still invisible. Either way the peers that edge
2614
+ * involves are neither confirmed satisfied nor confirmed unmet.
2615
+ *
2616
+ * Supplying {@link PeerCheckOptions.workspacePackages} clears the second
2617
+ * trigger for every `link:` target the supplied set covers AND the walk
2618
+ * reaches: the linked manifest's peers are joined and judged for real, which
2619
+ * is what makes the report an answer rather than a refusal. Omitting the
2620
+ * option — or omitting a linked target from it — leaves the edge unverified,
2621
+ * because an unjoined parent's peers are recorded nowhere in the lockfile; so
2622
+ * does a covered target the lockfile records no workspace row for, since
2623
+ * there is nothing to walk and its peers were never judged.
2624
+ *
2625
+ * - `"peerRangeUnresolved"` — a peer declared by a joined `link:` manifest has
2626
+ * a range that is a protocol specifier this check could not turn into a
2627
+ * range, while something resolved for that peer, so the comparison was
2628
+ * never performed. The trigger is a `catalog:` specifier with
2629
+ * {@link PeerCheckOptions.catalogs} omitted, a `catalog:` specifier the
2630
+ * supplied set names nothing for, or any other protocol (`workspace:*` and
2631
+ * friends). A peer with NOTHING resolved never produces it: "no provider"
2632
+ * is reportable without the range, so that row is emitted as usual.
2633
+ * - `"peerVersionUnresolved"` — a peer resolved to a non-workspace provider
2634
+ * whose version is a protocol specifier rather than a version, so the
2635
+ * comparison was never performed. That is any `file:` provider — a
2636
+ * directory, a tarball, or either through a `file:` override
2637
+ * (`file:../x`) — and equally a git or remote-tarball provider, which
2638
+ * pnpm keys by its URL (`https://codeload.github.com/…`,
2639
+ * `git+https://…`), so the URL stands where a version belongs. The
2640
+ * lockfile records no version for a `file:` directory at all, and
2641
+ * `pnpm peers check` reports such a peer as a `bad` row carrying the
2642
+ * specifier as its found version even when the directory's own manifest
2643
+ * satisfies the range, so calling it satisfied would pass a workspace
2644
+ * pnpm rejects. Applies to a peer declared by a lockfile row and
2645
+ * by a joined `link:` manifest alike. A workspace-row provider never
2646
+ * produces it (it is accepted as always), and neither does a plain
2647
+ * unparseable version, which is skipped.
2648
+ *
2649
+ * All four mean **fail closed**: a gate should treat an unverified report as
2650
+ * "not proven clean" rather than as a pass.
2651
+ *
2652
+ * @public
2653
+ */
2654
+ type UnverifiedReason = "peerRulesNotApplied" | "unresolvedEdge" | "peerRangeUnresolved" | "peerVersionUnresolved";
2655
+ /**
2656
+ * Options for {@link PeerCheck.run}.
2657
+ *
2658
+ * @remarks
2659
+ * **Presence of `peerDependencyRules` is the assertion; its contents are the
2660
+ * value.** Supplying `NoPeerDependencyRules` asserts the workspace has none and
2661
+ * yields a verified report; omitting the key entirely says nothing was looked
2662
+ * up and always yields `"peerRulesNotApplied"`. The two are deliberately
2663
+ * different results, because a gate must be able to tell "clean" from
2664
+ * "unchecked".
2665
+ *
2666
+ * **All three axes of the supplied rules are applied**, each with the
2667
+ * semantics measured against pnpm (see {@link PeerCheck.run}): `allowedVersions`
2668
+ * permits a resolved version by range, `ignoreMissing` hides a required peer
2669
+ * nothing resolved for, and `allowAny` hides a peer that resolved outside its
2670
+ * range. The two list axes are name patterns, never `parent>peer` keys.
2671
+ *
2672
+ * @public
2673
+ */
2674
+ interface PeerCheckOptions {
2675
+ /** The workspace's effective rules, from `WorkspaceCatalogs.peerDependencyRules()`. */
2676
+ readonly peerDependencyRules?: PeerDependencyRules;
2677
+ /**
2678
+ * The workspace's discovered packages, from `WorkspaceDiscovery` — the
2679
+ * manifests a `link:`-resolved parent's peers have to be read from,
2680
+ * because the lockfile does not record them.
2681
+ *
2682
+ * @remarks
2683
+ * **Supplying this key is what turns a `link:` edge from a refusal into an
2684
+ * answer.** pnpm records no peer declarations for workspace projects, so a
2685
+ * parent reached through `link:` joins at best to a row whose peers are
2686
+ * empty *by design*; with the manifests supplied, that parent's declared
2687
+ * peers are joined and judged instead. A `link:` target absent from the
2688
+ * supplied set — and every target, when the key is omitted — keeps the
2689
+ * report's `"unresolvedEdge"` marker, on the same presence-is-the-
2690
+ * assertion rule as `peerDependencyRules`.
2691
+ *
2692
+ * A `link:` target matches a package when it names the package's
2693
+ * `relativePath` — the POSIX workspace-relative directory the lockfile's
2694
+ * importer paths are spelled in — or its publish directory: pnpm links a
2695
+ * workspace dependency INTO `publishConfig.directory` unless
2696
+ * `publishConfig.linkDirectory` is `false` (it defaults to true), so the
2697
+ * lockfile then records `link:../a/dist`. Both spellings are normalized,
2698
+ * and the caller passes what discovery produced without re-spelling
2699
+ * anything.
2700
+ *
2701
+ * For a publish-directory link, pnpm reads the peers from the manifest AT
2702
+ * the link target — the built one — while this check reads the supplied
2703
+ * source manifest and resolves its `catalog:` ranges through
2704
+ * {@link PeerCheckOptions.catalogs}. The two agree when the build emits the
2705
+ * peer ranges the source's specifiers resolve to, which is the contract a
2706
+ * publish-directory build keeps.
2707
+ */
2708
+ readonly workspacePackages?: ReadonlyArray<WorkspacePackage>;
2709
+ /**
2710
+ * The workspace's catalogs, from `WorkspaceCatalogs.set()` — which already
2711
+ * includes any catalogs a config-dependency hook injects.
2712
+ *
2713
+ * @remarks
2714
+ * A joined manifest may declare a peer as `catalog:` or `catalog:<name>`
2715
+ * rather than as a range. With this key supplied, such a specifier is
2716
+ * resolved through the set and the RESOLVED range is judged and reported
2717
+ * as `wanted`, which is what `pnpm peers check` reports as `wantedRange`.
2718
+ * A specifier the set names nothing for — and every `catalog:` specifier,
2719
+ * when the key is omitted — cannot be judged against a provider, and the
2720
+ * report carries `"peerRangeUnresolved"` rather than calling the peer
2721
+ * satisfied: the same presence-is-the-assertion rule as the other two
2722
+ * keys.
2723
+ */
2724
+ readonly catalogs?: CatalogSet;
2725
+ }
2726
+ declare const PeerParent_base: Schema.Class<PeerParent, Schema.Struct<{
2727
+ /** The package name. */
2728
+ readonly name: Schema.NonEmptyString;
2729
+ /** The resolved version of that package. */
2730
+ readonly version: Schema.String;
2731
+ }>, {}>;
2732
+ /**
2733
+ * One link in the chain from an importer to the package that declared an
2734
+ * unsatisfied peer.
2735
+ *
2736
+ * @remarks
2737
+ * Mirrors the shape `pnpm peers check --json` reports, so the two can be
2738
+ * compared directly.
2739
+ *
2740
+ * @public
2741
+ */
2742
+ export declare class PeerParent extends PeerParent_base {}
2743
+ declare const UnsatisfiedPeer_base: Schema.Class<UnsatisfiedPeer, Schema.Struct<{
2744
+ /** The importer path the problem belongs to (`"."` for the root). */
2745
+ readonly importer: Schema.NonEmptyString;
2746
+ /** The peer dependency's name. */
2747
+ readonly dependency: Schema.NonEmptyString;
2748
+ /** The range the declaring package asked for. */
2749
+ readonly wanted: Schema.String;
2750
+ /** The version that resolved, or `null` when nothing resolved. */
2751
+ readonly found: Schema.NullOr<Schema.String>;
2752
+ /** Whether the declaring package marked the peer optional. */
2753
+ readonly optional: Schema.Boolean;
2754
+ /** The path from the importer to the declaring package. */
2755
+ readonly parents: Schema.$Array<typeof PeerParent>;
2756
+ }>, {}>;
2757
+ /**
2758
+ * One peer dependency that is declared but not satisfied.
2759
+ *
2760
+ * @remarks
2761
+ * `found` carries the version that actually resolved, or `null` when nothing
2762
+ * resolved at all. There is deliberately **no separate discriminant** between
2763
+ * "missing" and "unmet": `found === null` is the whole distinction, and a
2764
+ * renderer reads it directly.
2765
+ *
2766
+ * `optional` must be respected by any gate — an unsatisfied *optional* peer is
2767
+ * normal and should not fail a build, which is why the flag travels with the
2768
+ * row rather than being filtered out here. {@link PeerCheck.required} is the
2769
+ * convenience for the common case.
2770
+ *
2771
+ * `parents` is the path from the importer down to the package that declared
2772
+ * the peer, nearest-to-the-importer first. It is a path rather than a single
2773
+ * package because a peer declared by a transitive dependency is still that
2774
+ * importer's problem, and the chain is what makes the report actionable.
2775
+ *
2776
+ * A package an importer reaches by more than one path yields **one** row, not
2777
+ * one per path, carrying the path the walk reached first — which is what
2778
+ * `pnpm peers check` reports for the same graph. Read `parents` as *a* route to
2779
+ * the declaring package, never as the complete set of them.
2780
+ *
2781
+ * @public
2782
+ */
2783
+ export declare class UnsatisfiedPeer extends UnsatisfiedPeer_base {}
2784
+ declare const PeerCheck_base: Schema.Class<PeerCheck, Schema.Struct<{
2785
+ /**
2786
+ * Whether the lockfile's format records peer resolution at all.
2787
+ *
2788
+ * `false` for yarn: yarn resolves peers **virtually**, giving a
2789
+ * peer-bearing package one `@virtual:` locator per consumer, and the
2790
+ * lockfile does not record which virtual instance satisfied which peer.
2791
+ * The answer is not recoverable, so it is not fabricated — `unsatisfied`
2792
+ * is empty and this flag says why.
2793
+ */
2794
+ readonly supported: Schema.Boolean;
2795
+ /** Every unsatisfied peer found, optional ones included. */
2796
+ readonly unsatisfied: Schema.$Array<typeof UnsatisfiedPeer>;
2797
+ /**
2798
+ * Importers whose dependencies could not be resolved to instances, so no
2799
+ * verdict was reached for them.
2800
+ *
2801
+ * @remarks
2802
+ * In practice this is the **root importer under npm and bun**: neither
2803
+ * records a resolved version per importer dependency, and neither emits a
2804
+ * package row for the root, so there is nothing to join on. pnpm records a
2805
+ * version per importer dependency and is unaffected.
2806
+ *
2807
+ * Reported rather than silently skipped, for the same reason `supported`
2808
+ * exists: a gate that sees no rows is entitled to know whether that means
2809
+ * "clean" or "not looked at".
2810
+ */
2811
+ readonly unresolvedImporters: Schema.$Array<Schema.String>;
2812
+ /**
2813
+ * Why this report is not a complete answer, or empty when it is.
2814
+ *
2815
+ * @remarks
2816
+ * See {@link UnverifiedReason}. A gate must treat a non-empty `unverified`
2817
+ * as **not proven clean** rather than as a pass: every reason means some
2818
+ * finding may be missing or spurious, and failing closed is the requirement.
2819
+ */
2820
+ readonly unverified: Schema.$Array<Schema.Literals<readonly ["peerRulesNotApplied", "unresolvedEdge", "peerRangeUnresolved", "peerVersionUnresolved"]>>;
2821
+ }>, {}>;
2822
+ /**
2823
+ * The result of checking a lockfile for unsatisfied peer dependencies.
2824
+ *
2825
+ * @remarks
2826
+ * A report rather than a bare array, because an empty array is the most
2827
+ * dangerous success shape this domain has: it is indistinguishable from "this
2828
+ * format cannot be checked". `supported` and `unresolvedImporters` make the
2829
+ * difference legible, so a consumer cannot read a limitation as a clean bill
2830
+ * of health.
2831
+ *
2832
+ * Pure and total — construct it with {@link PeerCheck.run}, which never fails.
2833
+ *
2834
+ * @example
2835
+ * ```ts
2836
+ * import { PeerCheck } from "@effected/workspaces";
2837
+ * import { Lockfile } from "@effected/lockfiles";
2838
+ * import { Effect } from "effect";
2839
+ *
2840
+ * const program = Effect.gen(function* () {
2841
+ * const lockfile = yield* Lockfile.parse(text, { format: "pnpm" });
2842
+ * const report = PeerCheck.run(lockfile);
2843
+ * return report.supported ? report.required : [];
2844
+ * });
2845
+ * ```
2846
+ *
2847
+ * @public
2848
+ */
2849
+ export declare class PeerCheck extends PeerCheck_base {
2850
+ /** The unsatisfied peers a gate should act on — the non-optional ones. */
2851
+ get required(): ReadonlyArray<UnsatisfiedPeer>;
2852
+ /**
2853
+ * Compute the unsatisfied peer dependencies of a parsed lockfile.
2328
2854
  *
2329
2855
  * @remarks
2330
2856
  * Pure, total and format-free: no IO, no error channel, and no knowledge of
@@ -2335,8 +2861,13 @@ export declare class PeerCheck extends PeerCheck_base {
2335
2861
  * `resolved` edges, so a peer declared by a *transitive* dependency is
2336
2862
  * attributed to the importer that pulls it in, with the chain in
2337
2863
  * `parents` — matching how `pnpm peers check` attributes them.
2864
+ * Attribution stops at a workspace package: a linked package's own
2865
+ * dependencies — registry or linked — are judged by that package's own
2866
+ * importer, and a linked package's manifest peers are judged only for the
2867
+ * importer that links it DIRECTLY, against that importer's dependencies.
2868
+ * pnpm never surfaces them on a consumer one link further out.
2338
2869
  *
2339
- * Three judgements are deliberate:
2870
+ * Four judgements are deliberate:
2340
2871
  *
2341
2872
  * - **A required peer with nothing resolved is reported** (`found: null`),
2342
2873
  * whether or not the declared range is parseable — a peer with no
@@ -2349,6 +2880,9 @@ export declare class PeerCheck extends PeerCheck_base {
2349
2880
  * - **An unparseable range or version with something resolved is skipped.**
2350
2881
  * The check cannot judge it, and asserting "unsatisfied" on a comparison
2351
2882
  * that was never performed would be a wrong answer rather than a gap.
2883
+ * Two protocol-specifier cases are the exception and fail the report
2884
+ * closed instead: a joined manifest's range, and a provider whose version
2885
+ * is a protocol specifier (both below).
2352
2886
  * - **A workspace-linked provider counts.** Resolution is followed through
2353
2887
  * whatever the edge names, including a workspace row.
2354
2888
  *
@@ -2356,7 +2890,44 @@ export declare class PeerCheck extends PeerCheck_base {
2356
2890
  * bun root importer (see `unresolvedImporters`), and pnpm recording no peer
2357
2891
  * declarations for workspace projects themselves — under pnpm a workspace
2358
2892
  * package's *own* unsatisfied peers are not in the lockfile at all, and
2359
- * `pnpm peers check` does not report them either.
2893
+ * `pnpm peers check` does not report them either. A `link:`-resolved
2894
+ * importer dependency is the same blind spot from the other side, and there
2895
+ * `pnpm peers check` DOES report the linked parent's peers (it reads the
2896
+ * manifest on disk): every `link:` edge whose target the caller did not
2897
+ * supply therefore fails the report closed rather than letting the
2898
+ * invisible parent pass as checked (effected#800).
2899
+ *
2900
+ * Supplying {@link PeerCheckOptions.workspacePackages} closes that gap for
2901
+ * every `link:` target the set covers — the root importer's included,
2902
+ * whose linked targets the walk seeds itself because the root has no
2903
+ * workspace row to reach them through: the claimed manifest's declared
2904
+ * peers are walked, judged against the importer's own dependency set
2905
+ * (never a sibling importer's), and reported with the manifest's name and
2906
+ * version in `parents` — the two facts a workspace row cannot carry, since
2907
+ * the lockfile names an importer by directory at the placeholder version
2908
+ * `0.0.0`.
2909
+ *
2910
+ * A joined manifest's peer range may be a protocol specifier rather than a
2911
+ * range. A `catalog:` specifier is resolved through
2912
+ * {@link PeerCheckOptions.catalogs} and judged and reported by its
2913
+ * resolved range, as `pnpm peers check` reports `wantedRange`. One that
2914
+ * stays unresolvable — the catalogs key omitted, a catalog naming nothing
2915
+ * for the peer, or another protocol such as `workspace:*` — is not judged
2916
+ * against a provider that resolved for it, and the report carries
2917
+ * `"peerRangeUnresolved"` rather than passing the peer as satisfied. With
2918
+ * nothing resolved for it, the row is still reported, since "no provider"
2919
+ * needs no range.
2920
+ *
2921
+ * The provider side has the same hole: a peer that resolved to a `file:`
2922
+ * dependency, directly or through a `file:` override, carries the specifier
2923
+ * (`file:vendor/react`) where a version belongs, and for a `file:` directory
2924
+ * the lockfile records no version anywhere. `pnpm peers check` reports such a
2925
+ * peer as a `bad` row with the specifier as its found version, even when the
2926
+ * directory's manifest satisfies the range. Such a peer is neither reported
2927
+ * nor passed: no row is fabricated, and the report carries
2928
+ * `"peerVersionUnresolved"`, for a lockfile row's peer and a joined
2929
+ * manifest's peer alike. A workspace-row provider is still accepted without a
2930
+ * version check, and a plain unparseable version is still skipped.
2360
2931
  *
2361
2932
  * @param lockfile - a lockfile parsed by `@effected/lockfiles`
2362
2933
  * @returns the report; never fails
@@ -2690,612 +3261,216 @@ type TagClassification = {
2690
3261
  *
2691
3262
  * @remarks
2692
3263
  * The two families are told apart by **segment count**, not by the `v` prefix:
2693
- * three numeric segments is a version (so `1.0.0` and `v1.0.0` are both release
2694
- * tags), while one or two segments is a truncated alias. The `v` *is* required
2695
- * on an alias — a bare `1` is neither valid SemVer nor the tracking convention,
2696
- * and accepting it would make this function guess.
2697
- *
2698
- * The package prefix splits at the **last** `@`, so a leading npm scope
2699
- * survives: `@scope/pkg@1.0.0` is package `@scope/pkg` at version `1.0.0`.
2700
- *
2701
- * Round-tripping is a tested property: every tag {@link ReleaseTag} and
2702
- * {@link TrackingTag} format classifies back to the family that produced it,
2703
- * with its fields intact.
2704
- *
2705
- * @param tag - Any tag string.
2706
- * @returns The classification.
2707
- *
2708
- * @public
2709
- */
2710
- export declare const classifyTag: (tag: string) => TagClassification;
2711
- declare const ReleaseTag_base: Schema.Class<ReleaseTag, Schema.Struct<{
2712
- /** The tag string exactly as it appears in git. */
2713
- readonly value: Schema.NonEmptyString;
2714
- /** The package the tag names; absent on a workspace-wide single tag. */
2715
- readonly packageName: Schema.optionalKey<Schema.NonEmptyString>;
2716
- /** The version the tag names, without any prefix. */
2717
- readonly version: Schema.NonEmptyString;
2718
- /** Which style produced it. */
2719
- readonly style: Schema.Literals<readonly ["single", "scoped"]>;
2720
- }>, {}>;
2721
- /**
2722
- * A git tag naming a release, and the parts it was built from.
2723
- *
2724
- * @remarks
2725
- * `value` is the tag exactly as it appears in git; `version` stays **bare**
2726
- * even when `value` carries a prefix, so a consumer comparing versions never
2727
- * has to strip one back off.
2728
- *
2729
- * Formatting is **total**: there is no error channel, because the only failure
2730
- * v3 modelled — an empty version — is caught by `Schema.NonEmptyString` when
2731
- * the value is constructed. A bad version reaching these statics is developer
2732
- * wiring rather than untrusted input, so it dies as a defect, the same posture
2733
- * as an uncompilable glob literal in `WorkspacePackage.matchesDependency`.
2734
- *
2735
- * @example
2736
- * ```ts
2737
- * import { ReleaseTag } from "@effected/workspaces";
2738
- *
2739
- * ReleaseTag.single("1.2.3").value; // "1.2.3"
2740
- * ReleaseTag.scoped("@acme/cli", "1.2.3").value; // "@acme/cli@1.2.3"
2741
- * ReleaseTag.scoped("cli", "1.2.3").value; // "cli@1.2.3"
2742
- * ReleaseTag.scoped("cli", "1.2.3", { versionPrefix: "v" }).value; // "cli@v1.2.3"
2743
- * ```
2744
- *
2745
- * @public
2746
- */
2747
- export declare class ReleaseTag extends ReleaseTag_base {
2748
- /**
2749
- * One shared tag for a whole release: `1.2.3`.
2750
- *
2751
- * @param version - The version being released. Must not be empty.
2752
- * @param options - Formatting overrides.
2753
- */
2754
- static single(version: string, options?: TagFormatOptions): ReleaseTag;
2755
- /**
2756
- * A per-package tag: `<packageName>@<version>` — `@scope/pkg@1.2.3` for a
2757
- * scoped name, `pkg@1.2.3` for an unscoped one, uniformly, unless
2758
- * `options.versionPrefix` says otherwise.
2759
- *
2760
- * @param packageName - The package being released. Must not be empty.
2761
- * @param version - The version being released. Must not be empty.
2762
- * @param options - Formatting overrides.
2763
- */
2764
- static scoped(packageName: string, version: string, options?: TagFormatOptions): ReleaseTag;
2765
- }
2766
- //#endregion
2767
- //#region src/VersioningStrategy.d.ts
2768
- /**
2769
- * How a workspace assigns versions across its publishable packages.
2770
- *
2771
- * @remarks
2772
- * - `single` — zero or one publishable package, so one tag names the release.
2773
- * - `fixed-group` — every publishable package sits inside one group that
2774
- * versions in lockstep, so one tag still names the release.
2775
- * - `independent` — publishable packages version separately, so a shared tag
2776
- * would be ambiguous and each package needs its own.
2777
- *
2778
- * @public
2779
- */
2780
- export declare const VersioningStrategyType: Schema.Literals<readonly ["single", "fixed-group", "independent"]>;
2781
- /**
2782
- * The decoded type of {@link (VersioningStrategyType:variable)}.
2783
- *
2784
- * @public
2785
- */
2786
- export type VersioningStrategyType = typeof VersioningStrategyType.Type;
2787
- /**
2788
- * Arguments to {@link VersioningStrategy.classify}.
2789
- *
2790
- * @public
2791
- */
2792
- interface ClassifyOptions {
2793
- /** Publishable package names, in any order. Duplicates are collapsed. */
2794
- readonly packages: ReadonlyArray<string>;
2795
- /**
2796
- * Groups of packages that version in lockstep.
2797
- *
2798
- * @remarks
2799
- * A **plain argument, deliberately.** Fixed groups are a release tool's
2800
- * concept — changesets writes them to `.changeset/config.json` — and a
2801
- * workspace-model package that read that file would be taking on one tool's
2802
- * schema and one tool's release policy. The caller reads its own tool's
2803
- * config and hands the groups in. Groups may name packages that are not
2804
- * publishable, or do not exist; only whether some single group covers the
2805
- * whole publishable set matters.
2806
- */
2807
- readonly fixedGroups?: ReadonlyArray<ReadonlyArray<string>>;
2808
- }
2809
- /**
2810
- * Arguments to {@link VersioningStrategy.detect}.
2811
- *
2812
- * @public
2813
- */
2814
- interface VersioningDetectOptions {
2815
- /** See {@link ClassifyOptions.fixedGroups}. Defaults to none. */
2816
- readonly fixedGroups?: ReadonlyArray<ReadonlyArray<string>>;
2817
- }
2818
- /**
2819
- * One entry in a release batch: which package went out, at which version.
2820
- *
2821
- * @remarks
2822
- * Deliberately structural and minimal — a caller passes whatever it already
2823
- * has (a publish result, a changeset plan row) without projecting it into a
2824
- * package-specific type first.
2825
- *
2826
- * @public
2827
- */
2828
- interface PackageRelease {
2829
- /** The package that was released. */
2830
- readonly name: string;
2831
- /** The version it was released at. */
2832
- readonly version: string;
2833
- }
2834
- declare const VersioningStrategy_base: Schema.Class<VersioningStrategy, Schema.Struct<{
2835
- /** The classification. */
2836
- readonly type: Schema.Literals<readonly ["single", "fixed-group", "independent"]>;
2837
- /** The groups classification was performed against, as supplied. */
2838
- readonly fixedGroups: Schema.$Array<Schema.$Array<Schema.String>>;
2839
- /** The publishable package names, sorted and de-duplicated. */
2840
- readonly publishablePackages: Schema.$Array<Schema.String>;
2841
- }>, {}>;
2842
- /**
2843
- * How a workspace versions, and the tagging that follows from it.
2844
- *
2845
- * @remarks
2846
- * Built either purely with {@link VersioningStrategy.classify}, or from a live
2847
- * workspace with {@link VersioningStrategy.detect}.
2848
- *
2849
- * @example
2850
- * ```ts
2851
- * import { VersioningStrategy } from "@effected/workspaces";
2852
- * import { Effect } from "effect";
2853
- *
2854
- * const program = Effect.gen(function* () {
2855
- * const strategy = yield* VersioningStrategy.detect({ fixedGroups });
2856
- * return strategy.tagsFor(released).map((tag) => tag.value);
2857
- * });
2858
- * ```
2859
- *
2860
- * @public
2861
- */
2862
- export declare class VersioningStrategy extends VersioningStrategy_base {
2863
- /**
2864
- * Whether a release needs one tag per package rather than one shared tag.
2865
- */
2866
- get perPackageTags(): boolean;
2867
- /** The tag style this strategy implies. */
2868
- get tagStyle(): TagStyle;
2869
- /**
2870
- * Classify a workspace from its publishable package names and fixed groups.
2871
- *
2872
- * @remarks
2873
- * Pure and total — no IO, no error channel. `packages` is sorted and
2874
- * de-duplicated first, so a name listed twice cannot inflate a one-package
2875
- * repo into an independent one.
2876
- */
2877
- static classify(options: ClassifyOptions): VersioningStrategy;
2878
- /**
2879
- * Classify the ambient workspace: enumerate its packages, keep the ones the
2880
- * {@link PublishabilityDetector} says publish somewhere, and classify those.
2881
- *
2882
- * @remarks
2883
- * The publishability question is asked through the service precisely so a
2884
- * consumer with its own rules — honouring a release tool's ignore list, say —
2885
- * swaps the layer instead of filtering afterwards.
2886
- */
2887
- static readonly detect: (options?: VersioningDetectOptions | undefined) => Effect.Effect<VersioningStrategy, WorkspaceDiscoveryFailure, PublishabilityDetector | WorkspaceDiscovery>;
2888
- /**
2889
- * The tags a release of `releases` produces under this strategy.
2890
- *
2891
- * @remarks
2892
- * Under `independent` this is one {@link ReleaseTag} per release, in the
2893
- * order given. Under `single` and `fixed-group` it is exactly one shared tag
2894
- * carrying the **first** release's version — every release in a lockstep
2895
- * batch shares a version by construction, so the choice is only visible on a
2896
- * batch that should not exist. Whether a batch actually agreed is a property
2897
- * of that batch rather than of the workspace, so it stays the caller's
2898
- * one-line check rather than a field here.
2899
- *
2900
- * An empty batch produces no tags under either style.
2901
- */
2902
- tagsFor(releases: ReadonlyArray<PackageRelease>, options?: TagFormatOptions): ReadonlyArray<ReleaseTag>;
2903
- }
2904
- //#endregion
2905
- //#region src/WorkspaceCatalogs.d.ts
2906
- /**
2907
- * Each importer's dependency-name → resolved-version map, keyed by importer path
2908
- * (`"."` for the root package — the same keys `WorkspaceDiscovery.importerMap()`
2909
- * uses, and the same value `PackageStateSnapshot.relativePath` carries).
3264
+ * three numeric segments is a version (so `1.0.0` and `v1.0.0` are both release
3265
+ * tags), while one or two segments is a truncated alias. The `v` *is* required
3266
+ * on an alias — a bare `1` is neither valid SemVer nor the tracking convention,
3267
+ * and accepting it would make this function guess.
2910
3268
  *
2911
- * @remarks
2912
- * What a manager's lockfile records as actually installed, as opposed to what a
2913
- * manifest declares. It answers a `catalog:` specifier no committed catalog
2914
- * source declares — the shape a pnpm config-dependency pnpmfile hook produces —
2915
- * which would otherwise resolve to nothing on both sides of a diff and hide a
2916
- * real version movement.
3269
+ * The package prefix splits at the **last** `@`, so a leading npm scope
3270
+ * survives: `@scope/pkg@1.0.0` is package `@scope/pkg` at version `1.0.0`.
2917
3271
  *
2918
- * Versions are normalized: pnpm's peer-disambiguation suffix
2919
- * (`1.2.3(effect@4.0.0)`) is stripped, and `link:` / `file:` entries are omitted
2920
- * because a filesystem edge is not a version. Only pnpm records importer
2921
- * versions; bun and npm yield an empty index.
3272
+ * Round-tripping is a tested property: every tag {@link ReleaseTag} and
3273
+ * {@link TrackingTag} format classifies back to the family that produced it,
3274
+ * with its fields intact.
3275
+ *
3276
+ * @param tag - Any tag string.
3277
+ * @returns The classification.
2922
3278
  *
2923
3279
  * @public
2924
3280
  */
2925
- type ImporterVersions = Readonly<Record<string, Readonly<Record<string, string>>>>;
2926
- declare const CatalogSet_base: Schema.Class<CatalogSet, Schema.Struct<{
2927
- /** Catalog name → dependency name → version range. */
2928
- readonly entries: Schema.$Record<Schema.String, Schema.$Record<Schema.String, Schema.String>>;
3281
+ export declare const classifyTag: (tag: string) => TagClassification;
3282
+ declare const ReleaseTag_base: Schema.Class<ReleaseTag, Schema.Struct<{
3283
+ /** The tag string exactly as it appears in git. */
3284
+ readonly value: Schema.NonEmptyString;
3285
+ /** The package the tag names; absent on a workspace-wide single tag. */
3286
+ readonly packageName: Schema.optionalKey<Schema.NonEmptyString>;
3287
+ /** The version the tag names, without any prefix. */
3288
+ readonly version: Schema.NonEmptyString;
3289
+ /** Which style produced it. */
3290
+ readonly style: Schema.Literals<readonly ["single", "scoped"]>;
2929
3291
  }>, {}>;
2930
3292
  /**
2931
- * An immutable, fully-normalized catalog collection — the one catalog
2932
- * resolution semantic in the package.
3293
+ * A git tag naming a release, and the parts it was built from.
2933
3294
  *
2934
3295
  * @remarks
2935
- * `entries` is catalog name → dependency → range, with pnpm's unnamed
2936
- * top-level `catalog:` block normalized under the key `"default"`. Lockfile
2937
- * catalog entries, which pnpm records as `{ specifier, version }`, are
2938
- * normalized to their `specifier` — the declared range, which is what a catalog
2939
- * resolves to.
3296
+ * `value` is the tag exactly as it appears in git; `version` stays **bare**
3297
+ * even when `value` carries a prefix, so a consumer comparing versions never
3298
+ * has to strip one back off.
3299
+ *
3300
+ * Formatting is **total**: there is no error channel, because the only failure
3301
+ * v3 modelled — an empty version — is caught by `Schema.NonEmptyString` when
3302
+ * the value is constructed. A bad version reaching these statics is developer
3303
+ * wiring rather than untrusted input, so it dies as a defect, the same posture
3304
+ * as an uncompilable glob literal in `WorkspacePackage.matchesDependency`.
3305
+ *
3306
+ * @example
3307
+ * ```ts
3308
+ * import { ReleaseTag } from "@effected/workspaces";
3309
+ *
3310
+ * ReleaseTag.single("1.2.3").value; // "1.2.3"
3311
+ * ReleaseTag.scoped("@acme/cli", "1.2.3").value; // "@acme/cli@1.2.3"
3312
+ * ReleaseTag.scoped("cli", "1.2.3").value; // "cli@1.2.3"
3313
+ * ReleaseTag.scoped("cli", "1.2.3", { versionPrefix: "v" }).value; // "cli@v1.2.3"
3314
+ * ```
2940
3315
  *
2941
3316
  * @public
2942
3317
  */
2943
- export declare class CatalogSet extends CatalogSet_base {
2944
- /** The empty set — a workspace with no catalogs. */
2945
- static empty(): CatalogSet;
2946
- /** Wrap a pnpm `Catalogs` map, dropping unusable entries. */
2947
- static fromCatalogs(catalogs: unknown): CatalogSet;
2948
- /**
2949
- * The `catalog:` and `catalogs:` blocks of a `pnpm-workspace.yaml` document.
2950
- *
2951
- * @param text - The raw YAML text.
2952
- */
2953
- static readonly fromWorkspaceYaml: (text: string) => Effect.Effect<CatalogSet, CatalogAssemblyError, never>;
2954
- /**
2955
- * The `catalogs:` section of a pnpm lockfile, whose entries are either a bare
2956
- * range or a `{ specifier, version }` pair.
2957
- */
2958
- static fromLockfileCatalogs(raw: unknown): CatalogSet;
2959
- /**
2960
- * The catalog set a parsed lockfile records, PM-aware.
2961
- *
2962
- * @remarks
2963
- * Both pnpm and bun record catalogs in the lockfile, in different shapes:
2964
- * pnpm under `extension.catalogs`, bun under `extension.catalog` /
2965
- * `extension.catalogs`. Assembly reads whichever the parsed lockfile carries
2966
- * rather than assuming the pnpm extension. A lockfile with no extension, or a
2967
- * pnpm one with no catalogs, yields the empty set.
2968
- *
2969
- * @param lockfile - A parsed lockfile from `@effected/lockfiles`.
2970
- */
2971
- static fromLockfile(lockfile: Lockfile): CatalogSet;
2972
- /**
2973
- * A catalog set from bun's `{ catalog, catalogs }` blocks — used for the
2974
- * `bun.lock` extension and for the root `package.json` `workspaces` block. The
2975
- * unnamed default catalog normalizes under `"default"`. Tolerant: unusable
2976
- * values are dropped, never fatal.
2977
- *
2978
- * @param blocks - The `catalog` (default) and `catalogs` (named) blocks.
2979
- */
2980
- static fromBunBlocks(blocks: {
2981
- readonly catalog?: unknown;
2982
- readonly catalogs?: unknown;
2983
- }): CatalogSet;
2984
- /**
2985
- * The `catalog` / `catalogs` blocks of a root `package.json` `workspaces` field
2986
- * — bun's package.json analogue of pnpm's `pnpm-workspace.yaml` blocks.
2987
- *
2988
- * @remarks
2989
- * **Hard-fail by design**, preserving the semantics catalog output is
2990
- * load-bearing for: a present-but-malformed `workspaces` shape (a number, a
2991
- * string, an object with a malformed `packages` / `catalog` / `catalogs`), or
2992
- * the default catalog declared twice — once as `workspaces.catalog` and again
2993
- * as `workspaces.catalogs.default` — fails with `CatalogAssemblyError`
2994
- * naming what was wrong. An absent `workspaces` field, one explicitly `null`,
2995
- * or the plain array form (npm/yarn patterns, which carry no catalogs) yields
2996
- * the empty set. Presence is checked structurally, so an explicitly-declared
2997
- * empty `catalog: {}` still counts as a declaration.
2998
- *
2999
- * @param text - The raw root `package.json` text.
3000
- */
3001
- static readonly fromManifestWorkspaces: (text: string) => Effect.Effect<CatalogSet, CatalogAssemblyError, never>;
3002
- /** Merge sets. Later sets win per dependency within a catalog. */
3003
- static merge(...sets: ReadonlyArray<CatalogSet>): CatalogSet;
3004
- /** Whether any catalog declares anything. */
3005
- get isEmpty(): boolean;
3318
+ export declare class ReleaseTag extends ReleaseTag_base {
3006
3319
  /**
3007
- * The range a `catalog:` specifier resolves to.
3008
- *
3009
- * @remarks
3010
- * Total: an unmatched dependency, an unknown catalog name, or a non-catalog
3011
- * specifier all yield `Option.none()`. A *malformed* catalog — one pnpm
3012
- * itself rejects — also yields `Option.none()` here; the fallible surface is
3013
- * {@link WorkspaceCatalogs}, which fails typed on assembly instead.
3320
+ * One shared tag for a whole release: `1.2.3`.
3014
3321
  *
3015
- * @param dependency - The package name being resolved.
3016
- * @param specifier - The declared specifier, e.g. `catalog:` or `catalog:build`.
3322
+ * @param version - The version being released. Must not be empty.
3323
+ * @param options - Formatting overrides.
3017
3324
  */
3018
- resolveSpecifier(dependency: string, specifier: string): Option.Option<string>;
3325
+ static single(version: string, options?: TagFormatOptions): ReleaseTag;
3019
3326
  /**
3020
- * The range `dependency` carries in a named catalog, or in the default
3021
- * catalog when `catalog` is `Option.none()`.
3327
+ * A per-package tag: `<packageName>@<version>` — `@scope/pkg@1.2.3` for a
3328
+ * scoped name, `pkg@1.2.3` for an unscoped one, uniformly, unless
3329
+ * `options.versionPrefix` says otherwise.
3022
3330
  *
3023
- * @remarks
3024
- * The shape `@effected/npm`'s `CatalogResolver` contract asks for.
3331
+ * @param packageName - The package being released. Must not be empty.
3332
+ * @param version - The version being released. Must not be empty.
3333
+ * @param options - Formatting overrides.
3025
3334
  */
3026
- rangeOf(dependency: string, catalog: Option.Option<string>): Option.Option<string>;
3335
+ static scoped(packageName: string, version: string, options?: TagFormatOptions): ReleaseTag;
3027
3336
  }
3337
+ //#endregion
3338
+ //#region src/VersioningStrategy.d.ts
3028
3339
  /**
3029
- * Every failure catalog assembly can surface.
3340
+ * How a workspace assigns versions across its publishable packages.
3341
+ *
3342
+ * @remarks
3343
+ * - `single` — zero or one publishable package, so one tag names the release.
3344
+ * - `fixed-group` — every publishable package sits inside one group that
3345
+ * versions in lockstep, so one tag still names the release.
3346
+ * - `independent` — publishable packages version separately, so a shared tag
3347
+ * would be ambiguous and each package needs its own.
3030
3348
  *
3031
3349
  * @public
3032
3350
  */
3033
- type CatalogAssemblyFailure = CatalogAssemblyError | WorkspaceRootNotFoundError;
3351
+ export declare const VersioningStrategyType: Schema.Literals<readonly ["single", "fixed-group", "independent"]>;
3034
3352
  /**
3035
- * The {@link WorkspaceCatalogs} service shape.
3353
+ * The decoded type of {@link (VersioningStrategyType:variable)}.
3036
3354
  *
3037
3355
  * @public
3038
3356
  */
3039
- interface WorkspaceCatalogsShape {
3040
- /** The assembled catalog set for the workspace. Memoized after the first call. */
3041
- readonly set: () => Effect.Effect<CatalogSet, CatalogAssemblyFailure>;
3042
- /** Resolve one `catalog:` specifier; `Option.none()` when it names nothing. */
3043
- readonly resolveSpecifier: (dependency: string, specifier: string) => Effect.Effect<Option.Option<string>, CatalogAssemblyFailure>;
3044
- /**
3045
- * The effective pnpm release-age gate for the workspace, combined
3046
- * strictest-wins from the inline `pnpm-workspace.yaml` keys
3047
- * (`minimumReleaseAge` / `minimumReleaseAgeExclude`) and the replayed
3048
- * config-dependency hooks. Assembled from the same single read and hook
3049
- * replay as `set`, and memoized with it.
3050
- *
3051
- * @remarks
3052
- * Under the default layer (no-op hooks) only inline values contribute; under
3053
- * {@link WorkspaceCatalogs.layerWithConfigDependencies} the replayed hooks
3054
- * contribute too. A workspace with no pnpm-workspace.yaml (a bun/npm
3055
- * workspace) has no release-age keys, so the gate is the inert zero gate.
3056
- */
3057
- readonly releaseAgeGate: () => Effect.Effect<ReleaseAgeGate, CatalogAssemblyFailure>;
3058
- /**
3059
- * The workspace's **effective** `peerDependencyRules` — pnpm's post-hoc
3060
- * suppression policy, which the lockfile does not record at all.
3061
- *
3062
- * @remarks
3063
- * Assembled from the same single read and hook replay as `set`, and memoized
3064
- * with it. The `pnpm-workspace.yaml` block (what `pnpm:export` materializes)
3065
- * is **seeded** into the replayed config and the config-dependency hooks
3066
- * merge onto it, so one object carries both sources — the kit never merges
3067
- * them itself, because that would be a second implementation of a rule the
3068
- * hook seam already enforces.
3069
- *
3070
- * A consumer needs these to avoid **reporting** what pnpm suppresses: pnpm
3071
- * computes the same peer violations and then hides the ones a rule allows,
3072
- * so a checker without them reports findings pnpm calls clean. `PeerCheck`
3073
- * applies all three axes.
3074
- */
3075
- readonly peerDependencyRules: () => Effect.Effect<PeerDependencyRules, CatalogAssemblyFailure>;
3076
- /**
3077
- * Each importer's dependency-name → resolved-version map, as the manager's
3078
- * lockfile records it. Read from the same single lockfile read as `set`, and
3079
- * memoized with it.
3080
- *
3081
- * @remarks
3082
- * Feeds `WorkspaceStateSnapshot`'s fallback for a `catalog:` specifier no
3083
- * committed catalog source declares — the shape a config-dependency pnpmfile
3084
- * hook produces. Only pnpm records importer versions; a bun or npm workspace
3085
- * yields an empty index, and an absent or unreadable lockfile contributes
3086
- * nothing rather than failing.
3087
- */
3088
- readonly importerVersions: () => Effect.Effect<ImporterVersions, CatalogAssemblyFailure>;
3089
- /**
3090
- * Which version each declared config dependency was replayed from, keyed
3091
- * by name — off the same single memoized assemble pass as `set()`.
3092
- *
3093
- * @remarks
3094
- * Every dependency the hooks layer resolved when a `pnpm-workspace.yaml`
3095
- * was read — `{}` when it declares no config dependencies or under the
3096
- * no-op layer, which resolves nothing. Config dependencies are a pnpm
3097
- * feature, so the bun / `package.json` path yields `{}` too, exactly as
3098
- * `importerVersions` does.
3099
- */
3100
- readonly hookReplays: () => Effect.Effect<Readonly<Record<string, HookReplay>>, CatalogAssemblyFailure>;
3357
+ export type VersioningStrategyType = typeof VersioningStrategyType.Type;
3358
+ /**
3359
+ * Arguments to {@link VersioningStrategy.classify}.
3360
+ *
3361
+ * @public
3362
+ */
3363
+ interface ClassifyOptions {
3364
+ /** Publishable package names, in any order. Duplicates are collapsed. */
3365
+ readonly packages: ReadonlyArray<string>;
3101
3366
  /**
3102
- * Discard the memoized assembly so the **next** read re-assembles — the same
3103
- * single read and hook replay as the first, over the workspace as it stands
3104
- * then.
3105
- *
3106
- * @remarks
3107
- * The explicit memoization boundary for a tool that **mutates the workspace
3108
- * mid-run** — installs, bumps a config dependency, regenerates the lockfile.
3109
- * Release-age gating wants the before-state, a post-install peer check the
3110
- * after-state, and one infinite memo cannot serve both without this call in
3111
- * between; without it, `peerDependencyRules` keeps answering from the
3112
- * pre-mutation hook replay and the checker reports findings the new rules
3113
- * suppress.
3367
+ * Groups of packages that version in lockstep.
3114
3368
  *
3115
- * Unconditional and infallible: the memo is already success-only (a failed
3116
- * or interrupted assembly retries by itself), so `refresh` exists solely to
3117
- * discard a *successful* assembly that mutation has made stale. Calling it
3118
- * before any read is harmless.
3369
+ * @remarks
3370
+ * A **plain argument, deliberately.** Fixed groups are a release tool's
3371
+ * concept — changesets writes them to `.changeset/config.json` — and a
3372
+ * workspace-model package that read that file would be taking on one tool's
3373
+ * schema and one tool's release policy. The caller reads its own tool's
3374
+ * config and hands the groups in. Groups may name packages that are not
3375
+ * publishable, or do not exist; only whether some single group covers the
3376
+ * whole publishable set matters.
3119
3377
  */
3120
- readonly refresh: () => Effect.Effect<void>;
3378
+ readonly fixedGroups?: ReadonlyArray<ReadonlyArray<string>>;
3121
3379
  }
3122
3380
  /**
3123
- * Options for the {@link WorkspaceCatalogs} layer.
3381
+ * Arguments to {@link VersioningStrategy.detect}.
3124
3382
  *
3125
3383
  * @public
3126
3384
  */
3127
- interface WorkspaceCatalogsOptions {
3128
- /**
3129
- * The directory the workspace root is resolved from.
3130
- *
3131
- * @defaultValue `process.cwd()`, read lazily on first use.
3132
- */
3133
- readonly cwd?: string;
3385
+ interface VersioningDetectOptions {
3386
+ /** See {@link ClassifyOptions.fixedGroups}. Defaults to none. */
3387
+ readonly fixedGroups?: ReadonlyArray<ReadonlyArray<string>>;
3134
3388
  }
3135
- declare const WorkspaceCatalogs_base: Context.ServiceClass<WorkspaceCatalogs, "@effected/workspaces/WorkspaceCatalogs", WorkspaceCatalogsShape>;
3136
3389
  /**
3137
- * Assembles a workspace's catalogs, package-manager-aware.
3390
+ * One entry in a release batch: which package went out, at which version.
3138
3391
  *
3139
3392
  * @remarks
3140
- * The inline source is picked by **file presence**, the same rule
3141
- * `internal/patterns.ts` uses for globs: a `pnpm-workspace.yaml` selects the pnpm
3142
- * path (its `catalog:` / `catalogs:` blocks); its absence selects the root
3143
- * `package.json` `workspaces.catalog` / `workspaces.catalogs` (bun's analogue).
3144
- * The lockfile source is PM-aware too — whichever extension the parsed lockfile
3145
- * carries (pnpm or bun) contributes its recorded catalogs.
3393
+ * Deliberately structural and minimal — a caller passes whatever it already
3394
+ * has (a publish result, a changeset plan row) without projecting it into a
3395
+ * package-specific type first.
3146
3396
  *
3147
- * Precedence, lowest first: catalogs recorded in the lockfile, then the inline
3148
- * declaration, then — only under {@link WorkspaceCatalogs.layerWithConfigDependencies}
3149
- * — the config-dependency `pnpmfile.cjs` hooks, each layer winning per dependency
3150
- * within a catalog. An unreadable or absent lockfile degrades to no lockfile
3151
- * catalogs rather than failing (the inline declaration is the source of truth, the
3152
- * lockfile a record of what was installed); a malformed inline source **fails
3153
- * typed** with `CatalogAssemblyError`, because a silently-empty catalog read
3154
- * is the "every dependency looks newly added" bug.
3397
+ * @public
3398
+ */
3399
+ interface PackageRelease {
3400
+ /** The package that was released. */
3401
+ readonly name: string;
3402
+ /** The version it was released at. */
3403
+ readonly version: string;
3404
+ }
3405
+ declare const VersioningStrategy_base: Schema.Class<VersioningStrategy, Schema.Struct<{
3406
+ /** The classification. */
3407
+ readonly type: Schema.Literals<readonly ["single", "fixed-group", "independent"]>;
3408
+ /** The groups classification was performed against, as supplied. */
3409
+ readonly fixedGroups: Schema.$Array<Schema.$Array<Schema.String>>;
3410
+ /** The publishable package names, sorted and de-duplicated. */
3411
+ readonly publishablePackages: Schema.$Array<Schema.String>;
3412
+ }>, {}>;
3413
+ /**
3414
+ * How a workspace versions, and the tagging that follows from it.
3155
3415
  *
3156
- * Assembly is deferred to the first call and memoized success-only, matching
3157
- * {@link WorkspaceDiscovery}.
3416
+ * @remarks
3417
+ * Built either purely with {@link VersioningStrategy.classify}, or from a live
3418
+ * workspace with {@link VersioningStrategy.detect}.
3419
+ *
3420
+ * @example
3421
+ * ```ts
3422
+ * import { VersioningStrategy } from "@effected/workspaces";
3423
+ * import { Effect } from "effect";
3424
+ *
3425
+ * const program = Effect.gen(function* () {
3426
+ * const strategy = yield* VersioningStrategy.detect({ fixedGroups });
3427
+ * return strategy.tagsFor(released).map((tag) => tag.value);
3428
+ * });
3429
+ * ```
3158
3430
  *
3159
3431
  * @public
3160
3432
  */
3161
- export declare class WorkspaceCatalogs extends WorkspaceCatalogs_base {
3162
- /** Builds the service. */
3163
- static readonly make: (options?: WorkspaceCatalogsOptions) => Effect.Effect<WorkspaceCatalogsShape, never, WorkspaceRoot | LockfileReader | ConfigDependencyHooks | FileSystem.FileSystem | Path.Path>;
3164
- /**
3165
- * The service over an explicit {@link ConfigDependencyHooks} layer — the
3166
- * one builder every `layer*` static and the `Workspaces` composites
3167
- * delegate to, so the hooks layer is provided to {@link WorkspaceCatalogs.make}
3168
- * in exactly one place.
3169
- *
3170
- * @remarks
3171
- * Reach for it when the hooks layer is chosen elsewhere — a composite that
3172
- * must hand ONE hooks reference to both this service and
3173
- * `WorkspaceSnapshots` (layers memoize by reference, so two references
3174
- * would replay twice and could run two policies), or a test wiring
3175
- * {@link ConfigDependencyHooks.layerFrom}. The three `layer*` statics are
3176
- * this with the hooks layer fixed. `R` is the hooks layer's own
3177
- * requirement (the subprocess variant's `ChildProcessSpawner`), threaded
3178
- * through. Parameterized, so bind it to a `const` and reuse it.
3179
- *
3180
- * @param hooks - The {@link ConfigDependencyHooks} layer to replay through.
3181
- * @param options - The same options the other layer statics take.
3182
- */
3183
- static readonly layerWithHooks: <R = never>(hooks: Layer.Layer<ConfigDependencyHooks, never, R>, options?: WorkspaceCatalogsOptions) => Layer.Layer<WorkspaceCatalogs, never, WorkspaceRoot | LockfileReader | FileSystem.FileSystem | Path.Path | R>;
3184
- /**
3185
- * The live layer — the default, which **never executes config-dependency
3186
- * code**: it wires {@link ConfigDependencyHooks.layerNoop}, whose hooks return
3187
- * the inline-catalog seed untouched.
3188
- *
3189
- * @remarks
3190
- * Parameterized, so it mints a fresh reference per call — bind it to a
3191
- * `const` and reuse it, or layer memoization does not apply.
3192
- */
3193
- static readonly layer: (options?: WorkspaceCatalogsOptions) => Layer.Layer<WorkspaceCatalogs, never, WorkspaceRoot | LockfileReader | FileSystem.FileSystem | Path.Path>;
3433
+ export declare class VersioningStrategy extends VersioningStrategy_base {
3194
3434
  /**
3195
- * The opt-in live layer that **does** replay config-dependency `pnpmfile.cjs`
3196
- * hooks: it wires {@link ConfigDependencyHooks.layerLive}, which dynamically
3197
- * imports and runs each config dependency's `updateConfig` in process.
3198
- *
3199
- * @remarks
3200
- * Same output and requirement set as {@link WorkspaceCatalogs.layer} — the only
3201
- * difference is that config-dependency code is executed. Use it deliberately;
3202
- * the default catalog path stays free of any config-dependency execution.
3203
- * Parameterized, so bind it to a `const` and reuse it.
3435
+ * Whether a release needs one tag per package rather than one shared tag.
3204
3436
  */
3205
- static readonly layerWithConfigDependencies: (options?: WorkspaceCatalogsOptions) => Layer.Layer<WorkspaceCatalogs, never, WorkspaceRoot | LockfileReader | FileSystem.FileSystem | Path.Path>;
3437
+ get perPackageTags(): boolean;
3438
+ /** The tag style this strategy implies. */
3439
+ get tagStyle(): TagStyle;
3206
3440
  /**
3207
- * The opt-in layer that replays config-dependency `pnpmfile` hooks in a
3208
- * `node` **child process**: it wires
3209
- * {@link ConfigDependencyHooks.layerSubprocess} in place of the in-process
3210
- * `layerLive`.
3441
+ * Classify a workspace from its publishable package names and fixed groups.
3211
3442
  *
3212
3443
  * @remarks
3213
- * Same typed semantics as {@link WorkspaceCatalogs.layerWithConfigDependencies}
3214
- * — the two hook layers are drop-in interchangeable — but the replay's
3215
- * computed dynamic `import()` runs in the subprocess, so it survives bundling
3216
- * (a bundler compiles a computed in-process `import()` into a context module
3217
- * that cannot resolve at runtime — every bundled GitHub Action hits this).
3218
- * The cost is one extra requirement: core's `ChildProcessSpawner`, provided
3219
- * once at the edge (`@effect/platform-node`'s `NodeServices.layer`) — the
3220
- * same sanctioned R-widening as `Workspaces.layerWithGit`. Parameterized, so
3221
- * bind it to a `const` and reuse it.
3444
+ * Pure and total — no IO, no error channel. `packages` is sorted and
3445
+ * de-duplicated first, so a name listed twice cannot inflate a one-package
3446
+ * repo into an independent one.
3222
3447
  */
3223
- static readonly layerWithConfigDependenciesSubprocess: (options?: WorkspaceCatalogsOptions) => Layer.Layer<WorkspaceCatalogs, never, WorkspaceRoot | LockfileReader | FileSystem.FileSystem | Path.Path | ChildProcessSpawner.ChildProcessSpawner>;
3448
+ static classify(options: ClassifyOptions): VersioningStrategy;
3224
3449
  /**
3225
- * A test double satisfying the full {@link WorkspaceCatalogsShape} with no
3226
- * filesystem, lockfile read, or hook replay.
3450
+ * Classify the ambient workspace: enumerate its packages, keep the ones the
3451
+ * {@link PublishabilityDetector} says publish somewhere, and classify those.
3227
3452
  *
3228
3453
  * @remarks
3229
- * There is **no honest default catalog set**: an empty `CatalogSet` that
3230
- * looks like a legitimate answer is the "every dependency looks newly added"
3231
- * failure class the live assembler hard-fails to prevent, so every method
3232
- * **dies** with an instructive defect until stubbed — a test-wiring mistake
3233
- * fails loudly as a defect rather than succeeding with a lie or failing with
3234
- * a dishonest typed error.
3235
- *
3236
- * The one derivation mirrors `WorkspaceDiscovery.makeTest`'s
3237
- * derived-from-the-primary rule: when a `set` override is supplied,
3238
- * `resolveSpecifier` answers from that `CatalogSet`'s own
3239
- * {@link CatalogSet.resolveSpecifier} — exactly what the live service runs
3240
- * over its assembled set — so the two stay consistent by construction.
3241
- * `releaseAgeGate`, `importerVersions` and `hookReplays` are **not**
3242
- * derivable from a catalog set (the gate comes from release-age keys and
3243
- * hook contributions, the importer index from the lockfile's importer
3244
- * blocks, the replay record from the hooks layer — none is in a
3245
- * `CatalogSet`) and always die unless stubbed.
3246
- *
3247
- * `refresh` defaults to `Effect.void` honestly: the double holds no memo,
3248
- * so "drop the memoized assembly" is genuinely a no-op — the stubs answer
3249
- * fresh on every call already. This mirrors `WorkspaceDiscovery.makeTest`'s
3250
- * `refresh`.
3251
- *
3252
- * @example
3253
- * ```ts
3254
- * import { CatalogSet, WorkspaceCatalogs } from "@effected/workspaces";
3255
- * import { Effect } from "effect";
3256
- *
3257
- * const double = WorkspaceCatalogs.makeTest({
3258
- * set: () => Effect.succeed(CatalogSet.fromCatalogs({ default: { effect: "4.0.0" } })),
3259
- * });
3260
- * // `resolveSpecifier` now answers consistently from that set.
3261
- * ```
3454
+ * The publishability question is asked through the service precisely so a
3455
+ * consumer with its own rules — honouring a release tool's ignore list, say —
3456
+ * swaps the layer instead of filtering afterwards.
3262
3457
  */
3263
- static readonly makeTest: (overrides?: Partial<WorkspaceCatalogsShape>) => WorkspaceCatalogsShape;
3458
+ static readonly detect: (options?: VersioningDetectOptions | undefined) => Effect.Effect<VersioningStrategy, WorkspaceDiscoveryFailure, PublishabilityDetector | WorkspaceDiscovery>;
3264
3459
  /**
3265
- * The test layer: {@link WorkspaceCatalogs.makeTest} behind `Layer.succeed`,
3266
- * so a suite provides only the methods it exercises.
3460
+ * The tags a release of `releases` produces under this strategy.
3267
3461
  *
3268
3462
  * @remarks
3269
- * A parameterized layer factory mints a **fresh reference per call**, and
3270
- * layers memoize by reference — bind the result to a `const` and reuse it
3271
- * rather than calling `layerTest(...)` at each composition site.
3272
- *
3273
- * @example
3274
- * ```ts
3275
- * import { CatalogSet, WorkspaceCatalogs } from "@effected/workspaces";
3276
- * import { Effect } from "effect";
3277
- *
3278
- * const TestCatalogs = WorkspaceCatalogs.layerTest({
3279
- * set: () => Effect.succeed(CatalogSet.empty()),
3280
- * });
3281
- * // program.pipe(Effect.provide(TestCatalogs))
3282
- * ```
3283
- */
3284
- static readonly layerTest: (overrides?: Partial<WorkspaceCatalogsShape>) => Layer.Layer<WorkspaceCatalogs>;
3285
- /**
3286
- * The real implementation of `@effected/npm`'s `CatalogResolver` contract —
3287
- * the one `@effected/package-json` declares but cannot fill.
3463
+ * Under `independent` this is one {@link ReleaseTag} per release, in the
3464
+ * order given. Under `single` and `fixed-group` it is exactly one shared tag
3465
+ * carrying the **first** release's version — every release in a lockstep
3466
+ * batch shares a version by construction, so the choice is only visible on a
3467
+ * batch that should not exist. Whether a batch actually agreed is a property
3468
+ * of that batch rather than of the workspace, so it stays the caller's
3469
+ * one-line check rather than a field here.
3288
3470
  *
3289
- * @remarks
3290
- * `rangeOf` returns `Option.none()` for a dependency no catalog declares, per
3291
- * the contract's convention. A failed catalog *assembly* — an unreadable or
3292
- * malformed `pnpm-workspace.yaml`, a broken config-dependency hook — passes
3293
- * through **typed** as the contract's `CatalogAssemblyError` (it used to be
3294
- * folded into `DependencyResolutionError`'s defect `cause`, which forced
3295
- * consumers to `_tag`-sniff `unknown`); only the remaining mechanism failure,
3296
- * an unfindable workspace root, is wrapped as `DependencyResolutionError`.
3471
+ * An empty batch produces no tags under either style.
3297
3472
  */
3298
- static readonly catalogResolver: Layer.Layer<CatalogResolver, never, WorkspaceCatalogs>;
3473
+ tagsFor(releases: ReadonlyArray<PackageRelease>, options?: TagFormatOptions): ReadonlyArray<ReleaseTag>;
3299
3474
  }
3300
3475
  //#endregion
3301
3476
  //#region src/WorkspaceStateSnapshot.d.ts
@@ -3303,23 +3478,24 @@ declare const PackageStateSnapshot_base: Schema.Class<PackageStateSnapshot, Sche
3303
3478
  /** The package name. */
3304
3479
  readonly name: Schema.NonEmptyString;
3305
3480
  /**
3306
- * The raw `version` string, as recorded at the captured moment — or `""` for
3481
+ * The raw `version` string, as recorded at the captured moment — absent for
3307
3482
  * a manifest that declared none.
3308
3483
  *
3309
3484
  * @remarks
3310
- * `WorkspacePackage.version` is optional and a version-less member is an
3311
- * ordinary pnpm shape, but this field stays a plain required string because a
3312
- * snapshot is a serialized value someone stores and diffs: an absent key and
3313
- * a present one would read as a change the moment one side of a diff was
3314
- * captured by a different code path. **`""` is therefore the sentinel for
3315
- * "the manifest declared no version", not a version anyone can use** — both
3316
- * `snapshotOf` at a ref and the worktree snapshot write it, so the two sides
3317
- * agree. Every resolution surface treats it as absence:
3318
- * {@link WorkspaceStateSnapshot.resolve} answers `Option.none()` for a
3319
- * `workspace:` specifier against it, because `some("")` would rewrite
3320
- * `workspace:^` as a bare `"^"`.
3321
- */
3322
- readonly version: Schema.String;
3485
+ * Optional exactly as `WorkspacePackage.version` is: a version-less member is
3486
+ * an ordinary pnpm shape. Both capture paths — `WorkspaceSnapshots.at(ref)`
3487
+ * and `WorkspaceSnapshots.worktree()` — omit the key for such a member, so
3488
+ * the two sides of a diff agree without a placeholder. Never `""`: `make`
3489
+ * rejects it, and a snapshot serialized when `""` was the "no version"
3490
+ * sentinel decodes to the absent key.
3491
+ *
3492
+ * A version-less member is still a member: it appears in `packages` and
3493
+ * answers {@link WorkspaceStateSnapshot.package}, but is absent from
3494
+ * {@link WorkspaceStateSnapshot.versions}, resolves a `workspace:` specifier
3495
+ * to `Option.none()`, and fails its snapshot-bound `WorkspaceResolver`'s
3496
+ * `versionOf` typed.
3497
+ */
3498
+ readonly version: Schema.decodeTo<Schema.optionalKey<Schema.NonEmptyString>, Schema.optionalKey<Schema.String>, never, never>;
3323
3499
  /** POSIX path relative to the workspace root; `"."` for the root package. */
3324
3500
  readonly relativePath: Schema.String;
3325
3501
  /** Production dependencies. */
@@ -3450,15 +3626,13 @@ declare const WorkspaceStateSnapshot_base: Schema.Class<WorkspaceStateSnapshot,
3450
3626
  export declare class WorkspaceStateSnapshot extends WorkspaceStateSnapshot_base {
3451
3627
  #private;
3452
3628
  /**
3453
- * Every captured package's name → version. Total; O(1) after the first call.
3629
+ * Every captured package's name → version, for the packages that declared
3630
+ * one. Total; O(1) after the first call.
3454
3631
  *
3455
3632
  * @remarks
3456
- * Values are `PackageStateSnapshot.version` verbatim, so a member whose
3457
- * manifest declared no version maps to the **`""` sentinel** rather than
3458
- * being absent from the map — presence here answers membership, not
3459
- * "has a usable version". A caller reading a version out of this map owes
3460
- * the `""` check itself; {@link WorkspaceStateSnapshot.resolve} already
3461
- * makes it.
3633
+ * A member whose manifest declared no version is **absent** from this map,
3634
+ * so every value is a real version and presence answers "has a version",
3635
+ * not membership — ask {@link WorkspaceStateSnapshot.package} for that.
3462
3636
  */
3463
3637
  get versions(): ReadonlyMap<string, string>;
3464
3638
  /** A single captured package by name, or `Option.none()`. Total. */
@@ -3684,6 +3858,21 @@ interface WorkspaceSnapshotsOptions {
3684
3858
  * first call is honoured.
3685
3859
  */
3686
3860
  readonly cwd?: string;
3861
+ /**
3862
+ * A ceiling for the root ascent from `cwd`, passed straight through to the
3863
+ * `stopAt` of {@link WorkspaceRoot}'s `find`.
3864
+ *
3865
+ * @remarks
3866
+ * Inclusive, and resolved to an absolute path exactly as `cwd` is. When no
3867
+ * root is found at or below the ceiling, `at(ref)` fails with
3868
+ * {@link WorkspaceRootNotFoundError} carrying the resolved `stopAt` rather
3869
+ * than adopting an enclosing directory's workspace. Pass the same value as
3870
+ * {@link WorkspaceDiscoveryOptions.stopAt} so every service agrees on the
3871
+ * root; the `Workspaces.*` composites forward one `stopAt` to all of them.
3872
+ *
3873
+ * @defaultValue no ceiling — the ascent runs to the filesystem root.
3874
+ */
3875
+ readonly stopAt?: string | undefined;
3687
3876
  /**
3688
3877
  * Catalogs every snapshot this service produces carries as its
3689
3878
  * `seededCatalogs` — consulted only where the snapshot's own catalogs cannot
@@ -3851,9 +4040,17 @@ export declare class WorkspaceSnapshots extends WorkspaceSnapshots_base {
3851
4040
  /**
3852
4041
  * Options shared by the composite layers.
3853
4042
  *
4043
+ * @remarks
4044
+ * Derived from the option shapes of the services a composite builds —
4045
+ * {@link WorkspaceDiscoveryOptions}, {@link LockfileReaderOptions} and
4046
+ * {@link WorkspaceCatalogsOptions} — and handed to each of them unchanged, so a
4047
+ * root-resolving option one service grows cannot silently stop at the
4048
+ * composite. `cwd` and `stopAt` are re-declared here only to document them as
4049
+ * the one concern they are across the composite.
4050
+ *
3854
4051
  * @public
3855
4052
  */
3856
- interface WorkspacesOptions {
4053
+ interface WorkspacesOptions extends WorkspaceDiscoveryOptions, LockfileReaderOptions, WorkspaceCatalogsOptions {
3857
4054
  /**
3858
4055
  * The directory every root-consuming service resolves the workspace root
3859
4056
  * from — one explicit concern, applied uniformly.
@@ -3861,8 +4058,23 @@ interface WorkspacesOptions {
3861
4058
  * @defaultValue `process.cwd()`, read lazily on first use.
3862
4059
  */
3863
4060
  readonly cwd?: string;
3864
- /** Descent cap for segment-crossing `packages:` patterns. Defaults to 32. */
3865
- readonly maxDepth?: number;
4061
+ /**
4062
+ * A ceiling for the root ascent from `cwd`, forwarded to EVERY
4063
+ * root-resolving service the composite builds — {@link WorkspaceDiscovery},
4064
+ * {@link LockfileReader}, {@link WorkspaceCatalogs} and, on the git
4065
+ * composites, {@link WorkspaceSnapshots}.
4066
+ *
4067
+ * @remarks
4068
+ * Inclusive and resolved to an absolute path, as {@link WorkspaceRoot}'s
4069
+ * `find` does. Pass `stopAt: cwd` for a checkout nested inside someone
4070
+ * else's workspace: every service then fails with
4071
+ * {@link WorkspaceRootNotFoundError} consistently, instead of discovery
4072
+ * refusing the enclosing workspace while the lockfile and catalog reads
4073
+ * adopt it. A checkout that is itself a workspace root still resolves.
4074
+ *
4075
+ * @defaultValue no ceiling — the ascent runs to the filesystem root.
4076
+ */
4077
+ readonly stopAt?: string | undefined;
3866
4078
  }
3867
4079
  /**
3868
4080
  * Options for the composites that additionally provide {@link WorkspaceSnapshots}
@@ -4108,7 +4320,10 @@ export declare class Workspaces {
4108
4320
  * verbatim.
4109
4321
  *
4110
4322
  * `directory` is the resolved **workspace root**, not the caller's cwd: a
4111
- * project-local launcher has to run where the workspace is.
4323
+ * project-local launcher has to run where the workspace is. `stopAt` caps
4324
+ * that ascent exactly as {@link WorkspacesOptions.stopAt} does, and a
4325
+ * ceiling with no root at or below it is the same `Option.none()` as no
4326
+ * root at all.
4112
4327
  *
4113
4328
  * A consumer with no monorepo never needs this layer, and therefore never
4114
4329
  * installs this package — `LocalExec.layerNone` and `LocalExec.layerFor`
@@ -4137,6 +4352,7 @@ export declare class Workspaces {
4137
4352
  */
4138
4353
  static readonly localExecLayer: (options?: {
4139
4354
  readonly cwd?: string;
4355
+ readonly stopAt?: string | undefined;
4140
4356
  }) => Layer.Layer<LocalExec, never, PackageManagerDetector | WorkspaceRoot>;
4141
4357
  /**
4142
4358
  * Resolve every `catalog:` and `workspace:` specifier in one `Manifest`
@@ -4408,6 +4624,33 @@ interface WorkspaceDiscoverySkip {
4408
4624
  */
4409
4625
  readonly cause: unknown;
4410
4626
  }
4627
+ /**
4628
+ * Options for {@link findWorkspaceRootSync}: the consumer-supplied operations
4629
+ * plus the ascent's ceiling.
4630
+ *
4631
+ * @public
4632
+ */
4633
+ interface FindWorkspaceRootSyncOptions extends WorkspacesSyncOptions {
4634
+ /**
4635
+ * A ceiling directory, with the semantics of `FindWorkspaceRootOptions.stopAt`
4636
+ * on the Effect surface. The ascent stops after probing it, so an unmarked
4637
+ * `stopAt` returns `null` rather than silently adopting an enclosing
4638
+ * repository's workspace.
4639
+ *
4640
+ * @remarks
4641
+ * Inclusive: a ceiling that is itself a workspace root is returned. The
4642
+ * ceiling is resolved through the supplied {@link SyncPath.resolve} at
4643
+ * lookup time, so a relative one is taken against the process working
4644
+ * directory when the call runs (with `node:path`). A ceiling that names no
4645
+ * ancestor of `cwd` never matches, and the ascent runs to the filesystem
4646
+ * root. Omit it for the unbounded ascent.
4647
+ *
4648
+ * Pass `stopAt: cwd` for a checkout nested inside someone else's workspace
4649
+ * (a self-hosted runner, `actions/checkout` with `path:`): a checkout that is
4650
+ * itself a root still resolves, and one that is not returns `null`.
4651
+ */
4652
+ readonly stopAt?: string | undefined;
4653
+ }
4411
4654
  /**
4412
4655
  * The nearest workspace root at or above `cwd`, or `null`.
4413
4656
  *
@@ -4428,9 +4671,17 @@ interface WorkspaceDiscoverySkip {
4428
4671
  * Markers match the async service exactly: a `pnpm-workspace.yaml`, or a
4429
4672
  * `package.json` carrying a `workspaces` field.
4430
4673
  *
4674
+ * The ascent is unbounded unless `options.stopAt` names a ceiling
4675
+ * ({@link FindWorkspaceRootSyncOptions.stopAt}), with the Effect surface's
4676
+ * semantics: inclusive, resolved at lookup time. Where `WorkspaceRoot.find`
4677
+ * fails `WorkspaceRootNotFoundError` carrying the resolved ceiling, this
4678
+ * returns `null` — the facade is total and has no error channel, and the
4679
+ * caller already holds the ceiling it passed.
4680
+ *
4431
4681
  * @param cwd - Where to start the ascent (typically `process.cwd()`),
4432
4682
  * matching the Effect layers' `{ cwd }` option.
4433
- * @param options - The consumer-supplied file and path operations.
4683
+ * @param options - The consumer-supplied file and path operations, plus an
4684
+ * optional `stopAt` ceiling.
4434
4685
  *
4435
4686
  * @example
4436
4687
  * ```ts
@@ -4451,7 +4702,7 @@ interface WorkspaceDiscoverySkip {
4451
4702
  *
4452
4703
  * @public
4453
4704
  */
4454
- export declare const findWorkspaceRootSync: (cwd: string, options: WorkspacesSyncOptions) => string | null;
4705
+ export declare const findWorkspaceRootSync: (cwd: string, options: FindWorkspaceRootSyncOptions) => string | null;
4455
4706
  /**
4456
4707
  * Options for {@link getWorkspacePackagesSync}: the required consumer-supplied
4457
4708
  * operations plus the traversal bound.
@@ -4539,5 +4790,5 @@ interface GetWorkspacePackagesSyncOptions extends WorkspacesSyncOptions {
4539
4790
  */
4540
4791
  export declare const getWorkspacePackagesSync: (root: string, options: GetWorkspacePackagesSyncOptions) => ReadonlyArray<WorkspacePackage>;
4541
4792
  //#endregion
4542
- export type { CatalogAssemblyFailure, ChangeDetectionFailure, ChangeDetectorShape, ClassifyOptions, ConfigDependencyHooksShape, DependencyDiff, DuplicateCheckOptions, FindWorkspaceRootOptions, GetWorkspacePackagesSyncOptions, HookInjection, HookReplay, HookReplayContext, HookReplaySource, ImporterVersions, LockfileReadFailure, LockfileReaderOptions, LockfileReaderShape, PackageManagerDetectionFailure, PackageManagerDetectorShape, PackageRelease, PeerCheckOptions, PeerDependencyRules, PublishabilityDetectorShape, SyncDirectoryEntry, SyncFileSystem, SyncPath, TagClassification, TagFormatOptions, TrackingTagOptions, UnverifiedReason, VersioningDetectOptions, WorkspaceCatalogsOptions, WorkspaceCatalogsShape, WorkspaceDiscoveryFailure, WorkspaceDiscoveryOptions, WorkspaceDiscoveryShape, WorkspaceDiscoverySkip, WorkspaceDiscoverySkipKind, WorkspaceLookupFailure, WorkspaceRootShape, WorkspaceSnapshotAtFailure, WorkspaceSnapshotWorktreeFailure, WorkspaceSnapshotsOptions, WorkspaceSnapshotsShape, WorkspacesGitOptions, WorkspacesOptions, WorkspacesServices, WorkspacesSyncOptions };
4793
+ export type { CatalogAssemblyFailure, ChangeDetectionFailure, ChangeDetectorShape, ClassifyOptions, ConfigDependencyHooksShape, DependencyDiff, DuplicateCheckOptions, FindWorkspaceRootOptions, FindWorkspaceRootSyncOptions, GetWorkspacePackagesSyncOptions, HookInjection, HookReplay, HookReplayContext, HookReplaySource, ImporterVersions, LockfileReadFailure, LockfileReaderOptions, LockfileReaderShape, PackageManagerDetectionFailure, PackageManagerDetectorShape, PackageRelease, PeerCheckOptions, PeerDependencyRules, PublishabilityDetectorShape, SyncDirectoryEntry, SyncFileSystem, SyncPath, TagClassification, TagFormatOptions, TrackingTagOptions, UnverifiedReason, VersioningDetectOptions, WorkspaceCatalogsOptions, WorkspaceCatalogsShape, WorkspaceDiscoveryFailure, WorkspaceDiscoveryOptions, WorkspaceDiscoveryShape, WorkspaceDiscoverySkip, WorkspaceDiscoverySkipKind, WorkspaceLookupFailure, WorkspaceRootShape, WorkspaceSnapshotAtFailure, WorkspaceSnapshotWorktreeFailure, WorkspaceSnapshotsOptions, WorkspaceSnapshotsShape, WorkspacesGitOptions, WorkspacesOptions, WorkspacesServices, WorkspacesSyncOptions };
4543
4794
  //# sourceMappingURL=index.d.ts.map