@effected/workspaces 0.28.0 → 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
@@ -2182,193 +2182,692 @@ export declare class LockfileReader extends LockfileReader_base {
2182
2182
  static readonly layerTest: (overrides?: Partial<LockfileReaderShape>) => Layer.Layer<LockfileReader>;
2183
2183
  }
2184
2184
  //#endregion
2185
- //#region src/PeerCheck.d.ts
2186
- /**
2187
- * Why a report is not a complete answer.
2188
- *
2189
- * @remarks
2190
- * Closed at exactly two, by measurement rather than by guess:
2191
- *
2192
- * - `"peerRulesNotApplied"` — the effective suppression policy was not applied,
2193
- * so pnpm's post-hoc suppression could not be replicated and some reported
2194
- * rows may be ones pnpm hides. **Omitting the option always produces this**,
2195
- * because "nobody looked" and "I looked and there are none" are different
2196
- * facts. Supplied rules never produce it: all three axes —
2197
- * `allowedVersions`, `ignoreMissing` and `allowAny` — are applied with
2198
- * pnpm's measured semantics.
2199
- * - `"unresolvedEdge"` — some instance records an edge this model could not
2200
- * name (`ResolvedPackage.unresolvedEdges`), so a peer that edge satisfies
2201
- * cannot be verified either way.
2202
- *
2203
- * Both mean **fail closed**: a gate should treat an unverified report as "not
2204
- * proven clean" rather than as a pass.
2205
- *
2206
- * @public
2207
- */
2208
- type UnverifiedReason = "peerRulesNotApplied" | "unresolvedEdge";
2185
+ //#region src/WorkspaceCatalogs.d.ts
2209
2186
  /**
2210
- * 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).
2211
2190
  *
2212
2191
  * @remarks
2213
- * **Presence of `peerDependencyRules` is the assertion; its contents are the
2214
- * value.** Supplying `NoPeerDependencyRules` asserts the workspace has none and
2215
- * yields a verified report; omitting the key entirely says nothing was looked
2216
- * up and always yields `"peerRulesNotApplied"`. The two are deliberately
2217
- * different results, because a gate must be able to tell "clean" from
2218
- * "unchecked".
2219
- *
2220
- * **All three axes of the supplied rules are applied**, each with the
2221
- * semantics measured against pnpm (see {@link PeerCheck.run}): `allowedVersions`
2222
- * permits a resolved version by range, `ignoreMissing` hides a required peer
2223
- * nothing resolved for, and `allowAny` hides a peer that resolved outside its
2224
- * range. The two list axes are name patterns, never `parent>peer` keys.
2225
- *
2226
- * @public
2227
- */
2228
- interface PeerCheckOptions {
2229
- /** The workspace's effective rules, from `WorkspaceCatalogs.peerDependencyRules()`. */
2230
- readonly peerDependencyRules?: PeerDependencyRules;
2231
- }
2232
- declare const PeerParent_base: Schema.Class<PeerParent, Schema.Struct<{
2233
- /** The package name. */
2234
- readonly name: Schema.NonEmptyString;
2235
- /** The resolved version of that package. */
2236
- readonly version: Schema.String;
2237
- }>, {}>;
2238
- /**
2239
- * One link in the chain from an importer to the package that declared an
2240
- * 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.
2241
2197
  *
2242
- * @remarks
2243
- * Mirrors the shape `pnpm peers check --json` reports, so the two can be
2244
- * 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.
2245
2202
  *
2246
2203
  * @public
2247
2204
  */
2248
- export declare class PeerParent extends PeerParent_base {}
2249
- declare const UnsatisfiedPeer_base: Schema.Class<UnsatisfiedPeer, Schema.Struct<{
2250
- /** The importer path the problem belongs to (`"."` for the root). */
2251
- readonly importer: Schema.NonEmptyString;
2252
- /** The peer dependency's name. */
2253
- readonly dependency: Schema.NonEmptyString;
2254
- /** The range the declaring package asked for. */
2255
- readonly wanted: Schema.String;
2256
- /** The version that resolved, or `null` when nothing resolved. */
2257
- readonly found: Schema.NullOr<Schema.String>;
2258
- /** Whether the declaring package marked the peer optional. */
2259
- readonly optional: Schema.Boolean;
2260
- /** The path from the importer to the declaring package. */
2261
- 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>>;
2262
2209
  }>, {}>;
2263
2210
  /**
2264
- * 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.
2265
2213
  *
2266
2214
  * @remarks
2267
- * `found` carries the version that actually resolved, or `null` when nothing
2268
- * resolved at all. There is deliberately **no separate discriminant** between
2269
- * "missing" and "unmet": `found === null` is the whole distinction, and a
2270
- * renderer reads it directly.
2271
- *
2272
- * `optional` must be respected by any gate — an unsatisfied *optional* peer is
2273
- * normal and should not fail a build, which is why the flag travels with the
2274
- * row rather than being filtered out here. {@link PeerCheck.required} is the
2275
- * convenience for the common case.
2276
- *
2277
- * `parents` is the path from the importer down to the package that declared
2278
- * the peer, nearest-to-the-importer first. It is a path rather than a single
2279
- * package because a peer declared by a transitive dependency is still that
2280
- * importer's problem, and the chain is what makes the report actionable.
2281
- *
2282
- * A package an importer reaches by more than one path yields **one** row, not
2283
- * one per path, carrying the path the walk reached first — which is what
2284
- * `pnpm peers check` reports for the same graph. Read `parents` as *a* route to
2285
- * 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.
2286
2220
  *
2287
2221
  * @public
2288
2222
  */
2289
- export declare class UnsatisfiedPeer extends UnsatisfiedPeer_base {}
2290
- 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;
2291
2228
  /**
2292
- * Whether the lockfile's format records peer resolution at all.
2229
+ * The `catalog:` and `catalogs:` blocks of a `pnpm-workspace.yaml` document.
2293
2230
  *
2294
- * `false` for yarn: yarn resolves peers **virtually**, giving a
2295
- * peer-bearing package one `@virtual:` locator per consumer, and the
2296
- * lockfile does not record which virtual instance satisfied which peer.
2297
- * The answer is not recoverable, so it is not fabricated — `unsatisfied`
2298
- * is empty and this flag says why.
2231
+ * @param text - The raw YAML text.
2299
2232
  */
2300
- readonly supported: Schema.Boolean;
2301
- /** Every unsatisfied peer found, optional ones included. */
2302
- readonly unsatisfied: Schema.$Array<typeof UnsatisfiedPeer>;
2233
+ static readonly fromWorkspaceYaml: (text: string) => Effect.Effect<CatalogSet, CatalogAssemblyError, never>;
2303
2234
  /**
2304
- * Importers whose dependencies could not be resolved to instances, so no
2305
- * 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.
2306
2241
  *
2307
2242
  * @remarks
2308
- * In practice this is the **root importer under npm and bun**: neither
2309
- * records a resolved version per importer dependency, and neither emits a
2310
- * package row for the root, so there is nothing to join on. pnpm records a
2311
- * 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.
2312
2248
  *
2313
- * Reported rather than silently skipped, for the same reason `supported`
2314
- * exists: a gate that sees no rows is entitled to know whether that means
2315
- * "clean" or "not looked at".
2249
+ * @param lockfile - A parsed lockfile from `@effected/lockfiles`.
2316
2250
  */
2317
- readonly unresolvedImporters: Schema.$Array<Schema.String>;
2251
+ static fromLockfile(lockfile: Lockfile): CatalogSet;
2318
2252
  /**
2319
- * 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.
2320
2267
  *
2321
2268
  * @remarks
2322
- * See {@link UnverifiedReason}. A gate must treat a non-empty `unverified`
2323
- * as **not proven clean** rather than as a pass: both reasons mean some
2324
- * 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.
2325
2280
  */
2326
- readonly unverified: Schema.$Array<Schema.Literals<readonly ["peerRulesNotApplied", "unresolvedEdge"]>>;
2327
- }>, {}>;
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
+ }
2328
2308
  /**
2329
- * The result of checking a lockfile for unsatisfied peer dependencies.
2330
- *
2331
- * @remarks
2332
- * A report rather than a bare array, because an empty array is the most
2333
- * dangerous success shape this domain has: it is indistinguishable from "this
2334
- * format cannot be checked". `supported` and `unresolvedImporters` make the
2335
- * difference legible, so a consumer cannot read a limitation as a clean bill
2336
- * of health.
2337
- *
2338
- * Pure and total — construct it with {@link PeerCheck.run}, which never fails.
2339
- *
2340
- * @example
2341
- * ```ts
2342
- * import { PeerCheck } from "@effected/workspaces";
2343
- * import { Lockfile } from "@effected/lockfiles";
2344
- * import { Effect } from "effect";
2309
+ * Every failure catalog assembly can surface.
2345
2310
  *
2346
- * const program = Effect.gen(function* () {
2347
- * const lockfile = yield* Lockfile.parse(text, { format: "pnpm" });
2348
- * const report = PeerCheck.run(lockfile);
2349
- * return report.supported ? report.required : [];
2350
- * });
2351
- * ```
2311
+ * @public
2312
+ */
2313
+ type CatalogAssemblyFailure = CatalogAssemblyError | WorkspaceRootNotFoundError;
2314
+ /**
2315
+ * The {@link WorkspaceCatalogs} service shape.
2352
2316
  *
2353
2317
  * @public
2354
2318
  */
2355
- export declare class PeerCheck extends PeerCheck_base {
2356
- /** The unsatisfied peers a gate should act on — the non-optional ones. */
2357
- 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>;
2358
2324
  /**
2359
- * 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.
2360
2330
  *
2361
2331
  * @remarks
2362
- * Pure, total and format-free: no IO, no error channel, and no knowledge of
2363
- * which package manager wrote the file. Range satisfaction is
2364
- * `@effected/semver`'s, never hand-rolled.
2365
- *
2366
- * The walk starts at each importer's resolved dependencies and follows
2367
- * `resolved` edges, so a peer declared by a *transitive* dependency is
2368
- * attributed to the importer that pulls it in, with the chain in
2369
- * `parents` — matching how `pnpm peers check` attributes them.
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.
2854
+ *
2855
+ * @remarks
2856
+ * Pure, total and format-free: no IO, no error channel, and no knowledge of
2857
+ * which package manager wrote the file. Range satisfaction is
2858
+ * `@effected/semver`'s, never hand-rolled.
2859
+ *
2860
+ * The walk starts at each importer's resolved dependencies and follows
2861
+ * `resolved` edges, so a peer declared by a *transitive* dependency is
2862
+ * attributed to the importer that pulls it in, with the chain in
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.
2370
2869
  *
2371
- * Three judgements are deliberate:
2870
+ * Four judgements are deliberate:
2372
2871
  *
2373
2872
  * - **A required peer with nothing resolved is reported** (`found: null`),
2374
2873
  * whether or not the declared range is parseable — a peer with no
@@ -2381,6 +2880,9 @@ export declare class PeerCheck extends PeerCheck_base {
2381
2880
  * - **An unparseable range or version with something resolved is skipped.**
2382
2881
  * The check cannot judge it, and asserting "unsatisfied" on a comparison
2383
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).
2384
2886
  * - **A workspace-linked provider counts.** Resolution is followed through
2385
2887
  * whatever the edge names, including a workspace row.
2386
2888
  *
@@ -2388,7 +2890,44 @@ export declare class PeerCheck extends PeerCheck_base {
2388
2890
  * bun root importer (see `unresolvedImporters`), and pnpm recording no peer
2389
2891
  * declarations for workspace projects themselves — under pnpm a workspace
2390
2892
  * package's *own* unsatisfied peers are not in the lockfile at all, and
2391
- * `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.
2392
2931
  *
2393
2932
  * @param lockfile - a lockfile parsed by `@effected/lockfiles`
2394
2933
  * @returns the report; never fails
@@ -2718,631 +3257,220 @@ type TagClassification = {
2718
3257
  readonly kind: "unrecognized";
2719
3258
  };
2720
3259
  /**
2721
- * Decide whether a tag string is a release tag, a tracking alias, or neither.
2722
- *
2723
- * @remarks
2724
- * The two families are told apart by **segment count**, not by the `v` prefix:
2725
- * three numeric segments is a version (so `1.0.0` and `v1.0.0` are both release
2726
- * tags), while one or two segments is a truncated alias. The `v` *is* required
2727
- * on an alias — a bare `1` is neither valid SemVer nor the tracking convention,
2728
- * and accepting it would make this function guess.
2729
- *
2730
- * The package prefix splits at the **last** `@`, so a leading npm scope
2731
- * survives: `@scope/pkg@1.0.0` is package `@scope/pkg` at version `1.0.0`.
2732
- *
2733
- * Round-tripping is a tested property: every tag {@link ReleaseTag} and
2734
- * {@link TrackingTag} format classifies back to the family that produced it,
2735
- * with its fields intact.
2736
- *
2737
- * @param tag - Any tag string.
2738
- * @returns The classification.
2739
- *
2740
- * @public
2741
- */
2742
- export declare const classifyTag: (tag: string) => TagClassification;
2743
- declare const ReleaseTag_base: Schema.Class<ReleaseTag, Schema.Struct<{
2744
- /** The tag string exactly as it appears in git. */
2745
- readonly value: Schema.NonEmptyString;
2746
- /** The package the tag names; absent on a workspace-wide single tag. */
2747
- readonly packageName: Schema.optionalKey<Schema.NonEmptyString>;
2748
- /** The version the tag names, without any prefix. */
2749
- readonly version: Schema.NonEmptyString;
2750
- /** Which style produced it. */
2751
- readonly style: Schema.Literals<readonly ["single", "scoped"]>;
2752
- }>, {}>;
2753
- /**
2754
- * A git tag naming a release, and the parts it was built from.
2755
- *
2756
- * @remarks
2757
- * `value` is the tag exactly as it appears in git; `version` stays **bare**
2758
- * even when `value` carries a prefix, so a consumer comparing versions never
2759
- * has to strip one back off.
2760
- *
2761
- * Formatting is **total**: there is no error channel, because the only failure
2762
- * v3 modelled — an empty version — is caught by `Schema.NonEmptyString` when
2763
- * the value is constructed. A bad version reaching these statics is developer
2764
- * wiring rather than untrusted input, so it dies as a defect, the same posture
2765
- * as an uncompilable glob literal in `WorkspacePackage.matchesDependency`.
2766
- *
2767
- * @example
2768
- * ```ts
2769
- * import { ReleaseTag } from "@effected/workspaces";
2770
- *
2771
- * ReleaseTag.single("1.2.3").value; // "1.2.3"
2772
- * ReleaseTag.scoped("@acme/cli", "1.2.3").value; // "@acme/cli@1.2.3"
2773
- * ReleaseTag.scoped("cli", "1.2.3").value; // "cli@1.2.3"
2774
- * ReleaseTag.scoped("cli", "1.2.3", { versionPrefix: "v" }).value; // "cli@v1.2.3"
2775
- * ```
2776
- *
2777
- * @public
2778
- */
2779
- export declare class ReleaseTag extends ReleaseTag_base {
2780
- /**
2781
- * One shared tag for a whole release: `1.2.3`.
2782
- *
2783
- * @param version - The version being released. Must not be empty.
2784
- * @param options - Formatting overrides.
2785
- */
2786
- static single(version: string, options?: TagFormatOptions): ReleaseTag;
2787
- /**
2788
- * A per-package tag: `<packageName>@<version>` — `@scope/pkg@1.2.3` for a
2789
- * scoped name, `pkg@1.2.3` for an unscoped one, uniformly, unless
2790
- * `options.versionPrefix` says otherwise.
2791
- *
2792
- * @param packageName - The package being released. Must not be empty.
2793
- * @param version - The version being released. Must not be empty.
2794
- * @param options - Formatting overrides.
2795
- */
2796
- static scoped(packageName: string, version: string, options?: TagFormatOptions): ReleaseTag;
2797
- }
2798
- //#endregion
2799
- //#region src/VersioningStrategy.d.ts
2800
- /**
2801
- * How a workspace assigns versions across its publishable packages.
2802
- *
2803
- * @remarks
2804
- * - `single` — zero or one publishable package, so one tag names the release.
2805
- * - `fixed-group` — every publishable package sits inside one group that
2806
- * versions in lockstep, so one tag still names the release.
2807
- * - `independent` — publishable packages version separately, so a shared tag
2808
- * would be ambiguous and each package needs its own.
2809
- *
2810
- * @public
2811
- */
2812
- export declare const VersioningStrategyType: Schema.Literals<readonly ["single", "fixed-group", "independent"]>;
2813
- /**
2814
- * The decoded type of {@link (VersioningStrategyType:variable)}.
2815
- *
2816
- * @public
2817
- */
2818
- export type VersioningStrategyType = typeof VersioningStrategyType.Type;
2819
- /**
2820
- * Arguments to {@link VersioningStrategy.classify}.
2821
- *
2822
- * @public
2823
- */
2824
- interface ClassifyOptions {
2825
- /** Publishable package names, in any order. Duplicates are collapsed. */
2826
- readonly packages: ReadonlyArray<string>;
2827
- /**
2828
- * Groups of packages that version in lockstep.
2829
- *
2830
- * @remarks
2831
- * A **plain argument, deliberately.** Fixed groups are a release tool's
2832
- * concept — changesets writes them to `.changeset/config.json` — and a
2833
- * workspace-model package that read that file would be taking on one tool's
2834
- * schema and one tool's release policy. The caller reads its own tool's
2835
- * config and hands the groups in. Groups may name packages that are not
2836
- * publishable, or do not exist; only whether some single group covers the
2837
- * whole publishable set matters.
2838
- */
2839
- readonly fixedGroups?: ReadonlyArray<ReadonlyArray<string>>;
2840
- }
2841
- /**
2842
- * Arguments to {@link VersioningStrategy.detect}.
2843
- *
2844
- * @public
2845
- */
2846
- interface VersioningDetectOptions {
2847
- /** See {@link ClassifyOptions.fixedGroups}. Defaults to none. */
2848
- readonly fixedGroups?: ReadonlyArray<ReadonlyArray<string>>;
2849
- }
2850
- /**
2851
- * One entry in a release batch: which package went out, at which version.
2852
- *
2853
- * @remarks
2854
- * Deliberately structural and minimal — a caller passes whatever it already
2855
- * has (a publish result, a changeset plan row) without projecting it into a
2856
- * package-specific type first.
2857
- *
2858
- * @public
2859
- */
2860
- interface PackageRelease {
2861
- /** The package that was released. */
2862
- readonly name: string;
2863
- /** The version it was released at. */
2864
- readonly version: string;
2865
- }
2866
- declare const VersioningStrategy_base: Schema.Class<VersioningStrategy, Schema.Struct<{
2867
- /** The classification. */
2868
- readonly type: Schema.Literals<readonly ["single", "fixed-group", "independent"]>;
2869
- /** The groups classification was performed against, as supplied. */
2870
- readonly fixedGroups: Schema.$Array<Schema.$Array<Schema.String>>;
2871
- /** The publishable package names, sorted and de-duplicated. */
2872
- readonly publishablePackages: Schema.$Array<Schema.String>;
2873
- }>, {}>;
2874
- /**
2875
- * How a workspace versions, and the tagging that follows from it.
2876
- *
2877
- * @remarks
2878
- * Built either purely with {@link VersioningStrategy.classify}, or from a live
2879
- * workspace with {@link VersioningStrategy.detect}.
2880
- *
2881
- * @example
2882
- * ```ts
2883
- * import { VersioningStrategy } from "@effected/workspaces";
2884
- * import { Effect } from "effect";
2885
- *
2886
- * const program = Effect.gen(function* () {
2887
- * const strategy = yield* VersioningStrategy.detect({ fixedGroups });
2888
- * return strategy.tagsFor(released).map((tag) => tag.value);
2889
- * });
2890
- * ```
2891
- *
2892
- * @public
2893
- */
2894
- export declare class VersioningStrategy extends VersioningStrategy_base {
2895
- /**
2896
- * Whether a release needs one tag per package rather than one shared tag.
2897
- */
2898
- get perPackageTags(): boolean;
2899
- /** The tag style this strategy implies. */
2900
- get tagStyle(): TagStyle;
2901
- /**
2902
- * Classify a workspace from its publishable package names and fixed groups.
2903
- *
2904
- * @remarks
2905
- * Pure and total — no IO, no error channel. `packages` is sorted and
2906
- * de-duplicated first, so a name listed twice cannot inflate a one-package
2907
- * repo into an independent one.
2908
- */
2909
- static classify(options: ClassifyOptions): VersioningStrategy;
2910
- /**
2911
- * Classify the ambient workspace: enumerate its packages, keep the ones the
2912
- * {@link PublishabilityDetector} says publish somewhere, and classify those.
2913
- *
2914
- * @remarks
2915
- * The publishability question is asked through the service precisely so a
2916
- * consumer with its own rules — honouring a release tool's ignore list, say —
2917
- * swaps the layer instead of filtering afterwards.
2918
- */
2919
- static readonly detect: (options?: VersioningDetectOptions | undefined) => Effect.Effect<VersioningStrategy, WorkspaceDiscoveryFailure, PublishabilityDetector | WorkspaceDiscovery>;
2920
- /**
2921
- * The tags a release of `releases` produces under this strategy.
2922
- *
2923
- * @remarks
2924
- * Under `independent` this is one {@link ReleaseTag} per release, in the
2925
- * order given. Under `single` and `fixed-group` it is exactly one shared tag
2926
- * carrying the **first** release's version — every release in a lockstep
2927
- * batch shares a version by construction, so the choice is only visible on a
2928
- * batch that should not exist. Whether a batch actually agreed is a property
2929
- * of that batch rather than of the workspace, so it stays the caller's
2930
- * one-line check rather than a field here.
2931
- *
2932
- * An empty batch produces no tags under either style.
2933
- */
2934
- tagsFor(releases: ReadonlyArray<PackageRelease>, options?: TagFormatOptions): ReadonlyArray<ReleaseTag>;
2935
- }
2936
- //#endregion
2937
- //#region src/WorkspaceCatalogs.d.ts
2938
- /**
2939
- * Each importer's dependency-name → resolved-version map, keyed by importer path
2940
- * (`"."` for the root package — the same keys `WorkspaceDiscovery.importerMap()`
2941
- * uses, and the same value `PackageStateSnapshot.relativePath` carries).
3260
+ * Decide whether a tag string is a release tag, a tracking alias, or neither.
2942
3261
  *
2943
3262
  * @remarks
2944
- * What a manager's lockfile records as actually installed, as opposed to what a
2945
- * manifest declares. It answers a `catalog:` specifier no committed catalog
2946
- * source declares — the shape a pnpm config-dependency pnpmfile hook produces —
2947
- * which would otherwise resolve to nothing on both sides of a diff and hide a
2948
- * real version movement.
3263
+ * The two families are told apart by **segment count**, not by the `v` prefix:
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.
2949
3268
  *
2950
- * Versions are normalized: pnpm's peer-disambiguation suffix
2951
- * (`1.2.3(effect@4.0.0)`) is stripped, and `link:` / `file:` entries are omitted
2952
- * because a filesystem edge is not a version. Only pnpm records importer
2953
- * versions; bun and npm yield an empty index.
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`.
3271
+ *
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.
2954
3278
  *
2955
3279
  * @public
2956
3280
  */
2957
- type ImporterVersions = Readonly<Record<string, Readonly<Record<string, string>>>>;
2958
- declare const CatalogSet_base: Schema.Class<CatalogSet, Schema.Struct<{
2959
- /** Catalog name → dependency name → version range. */
2960
- 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"]>;
2961
3291
  }>, {}>;
2962
3292
  /**
2963
- * An immutable, fully-normalized catalog collection — the one catalog
2964
- * resolution semantic in the package.
3293
+ * A git tag naming a release, and the parts it was built from.
2965
3294
  *
2966
3295
  * @remarks
2967
- * `entries` is catalog name → dependency → range, with pnpm's unnamed
2968
- * top-level `catalog:` block normalized under the key `"default"`. Lockfile
2969
- * catalog entries, which pnpm records as `{ specifier, version }`, are
2970
- * normalized to their `specifier` — the declared range, which is what a catalog
2971
- * 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
+ * ```
2972
3315
  *
2973
3316
  * @public
2974
3317
  */
2975
- export declare class CatalogSet extends CatalogSet_base {
2976
- /** The empty set — a workspace with no catalogs. */
2977
- static empty(): CatalogSet;
2978
- /** Wrap a pnpm `Catalogs` map, dropping unusable entries. */
2979
- static fromCatalogs(catalogs: unknown): CatalogSet;
2980
- /**
2981
- * The `catalog:` and `catalogs:` blocks of a `pnpm-workspace.yaml` document.
2982
- *
2983
- * @param text - The raw YAML text.
2984
- */
2985
- static readonly fromWorkspaceYaml: (text: string) => Effect.Effect<CatalogSet, CatalogAssemblyError, never>;
2986
- /**
2987
- * The `catalogs:` section of a pnpm lockfile, whose entries are either a bare
2988
- * range or a `{ specifier, version }` pair.
2989
- */
2990
- static fromLockfileCatalogs(raw: unknown): CatalogSet;
2991
- /**
2992
- * The catalog set a parsed lockfile records, PM-aware.
2993
- *
2994
- * @remarks
2995
- * Both pnpm and bun record catalogs in the lockfile, in different shapes:
2996
- * pnpm under `extension.catalogs`, bun under `extension.catalog` /
2997
- * `extension.catalogs`. Assembly reads whichever the parsed lockfile carries
2998
- * rather than assuming the pnpm extension. A lockfile with no extension, or a
2999
- * pnpm one with no catalogs, yields the empty set.
3000
- *
3001
- * @param lockfile - A parsed lockfile from `@effected/lockfiles`.
3002
- */
3003
- static fromLockfile(lockfile: Lockfile): CatalogSet;
3004
- /**
3005
- * A catalog set from bun's `{ catalog, catalogs }` blocks — used for the
3006
- * `bun.lock` extension and for the root `package.json` `workspaces` block. The
3007
- * unnamed default catalog normalizes under `"default"`. Tolerant: unusable
3008
- * values are dropped, never fatal.
3009
- *
3010
- * @param blocks - The `catalog` (default) and `catalogs` (named) blocks.
3011
- */
3012
- static fromBunBlocks(blocks: {
3013
- readonly catalog?: unknown;
3014
- readonly catalogs?: unknown;
3015
- }): CatalogSet;
3016
- /**
3017
- * The `catalog` / `catalogs` blocks of a root `package.json` `workspaces` field
3018
- * — bun's package.json analogue of pnpm's `pnpm-workspace.yaml` blocks.
3019
- *
3020
- * @remarks
3021
- * **Hard-fail by design**, preserving the semantics catalog output is
3022
- * load-bearing for: a present-but-malformed `workspaces` shape (a number, a
3023
- * string, an object with a malformed `packages` / `catalog` / `catalogs`), or
3024
- * the default catalog declared twice — once as `workspaces.catalog` and again
3025
- * as `workspaces.catalogs.default` — fails with `CatalogAssemblyError`
3026
- * naming what was wrong. An absent `workspaces` field, one explicitly `null`,
3027
- * or the plain array form (npm/yarn patterns, which carry no catalogs) yields
3028
- * the empty set. Presence is checked structurally, so an explicitly-declared
3029
- * empty `catalog: {}` still counts as a declaration.
3030
- *
3031
- * @param text - The raw root `package.json` text.
3032
- */
3033
- static readonly fromManifestWorkspaces: (text: string) => Effect.Effect<CatalogSet, CatalogAssemblyError, never>;
3034
- /** Merge sets. Later sets win per dependency within a catalog. */
3035
- static merge(...sets: ReadonlyArray<CatalogSet>): CatalogSet;
3036
- /** Whether any catalog declares anything. */
3037
- get isEmpty(): boolean;
3318
+ export declare class ReleaseTag extends ReleaseTag_base {
3038
3319
  /**
3039
- * The range a `catalog:` specifier resolves to.
3040
- *
3041
- * @remarks
3042
- * Total: an unmatched dependency, an unknown catalog name, or a non-catalog
3043
- * specifier all yield `Option.none()`. A *malformed* catalog — one pnpm
3044
- * itself rejects — also yields `Option.none()` here; the fallible surface is
3045
- * {@link WorkspaceCatalogs}, which fails typed on assembly instead.
3320
+ * One shared tag for a whole release: `1.2.3`.
3046
3321
  *
3047
- * @param dependency - The package name being resolved.
3048
- * @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.
3049
3324
  */
3050
- resolveSpecifier(dependency: string, specifier: string): Option.Option<string>;
3325
+ static single(version: string, options?: TagFormatOptions): ReleaseTag;
3051
3326
  /**
3052
- * The range `dependency` carries in a named catalog, or in the default
3053
- * 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.
3054
3330
  *
3055
- * @remarks
3056
- * 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.
3057
3334
  */
3058
- rangeOf(dependency: string, catalog: Option.Option<string>): Option.Option<string>;
3335
+ static scoped(packageName: string, version: string, options?: TagFormatOptions): ReleaseTag;
3059
3336
  }
3337
+ //#endregion
3338
+ //#region src/VersioningStrategy.d.ts
3060
3339
  /**
3061
- * 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.
3062
3348
  *
3063
3349
  * @public
3064
3350
  */
3065
- type CatalogAssemblyFailure = CatalogAssemblyError | WorkspaceRootNotFoundError;
3351
+ export declare const VersioningStrategyType: Schema.Literals<readonly ["single", "fixed-group", "independent"]>;
3066
3352
  /**
3067
- * The {@link WorkspaceCatalogs} service shape.
3353
+ * The decoded type of {@link (VersioningStrategyType:variable)}.
3068
3354
  *
3069
3355
  * @public
3070
3356
  */
3071
- interface WorkspaceCatalogsShape {
3072
- /** The assembled catalog set for the workspace. Memoized after the first call. */
3073
- readonly set: () => Effect.Effect<CatalogSet, CatalogAssemblyFailure>;
3074
- /** Resolve one `catalog:` specifier; `Option.none()` when it names nothing. */
3075
- readonly resolveSpecifier: (dependency: string, specifier: string) => Effect.Effect<Option.Option<string>, CatalogAssemblyFailure>;
3076
- /**
3077
- * The effective pnpm release-age gate for the workspace, combined
3078
- * strictest-wins from the inline `pnpm-workspace.yaml` keys
3079
- * (`minimumReleaseAge` / `minimumReleaseAgeExclude`) and the replayed
3080
- * config-dependency hooks. Assembled from the same single read and hook
3081
- * replay as `set`, and memoized with it.
3082
- *
3083
- * @remarks
3084
- * Under the default layer (no-op hooks) only inline values contribute; under
3085
- * {@link WorkspaceCatalogs.layerWithConfigDependencies} the replayed hooks
3086
- * contribute too. A workspace with no pnpm-workspace.yaml (a bun/npm
3087
- * workspace) has no release-age keys, so the gate is the inert zero gate.
3088
- */
3089
- readonly releaseAgeGate: () => Effect.Effect<ReleaseAgeGate, CatalogAssemblyFailure>;
3090
- /**
3091
- * The workspace's **effective** `peerDependencyRules` — pnpm's post-hoc
3092
- * suppression policy, which the lockfile does not record at all.
3093
- *
3094
- * @remarks
3095
- * Assembled from the same single read and hook replay as `set`, and memoized
3096
- * with it. The `pnpm-workspace.yaml` block (what `pnpm:export` materializes)
3097
- * is **seeded** into the replayed config and the config-dependency hooks
3098
- * merge onto it, so one object carries both sources — the kit never merges
3099
- * them itself, because that would be a second implementation of a rule the
3100
- * hook seam already enforces.
3101
- *
3102
- * A consumer needs these to avoid **reporting** what pnpm suppresses: pnpm
3103
- * computes the same peer violations and then hides the ones a rule allows,
3104
- * so a checker without them reports findings pnpm calls clean. `PeerCheck`
3105
- * applies all three axes.
3106
- */
3107
- readonly peerDependencyRules: () => Effect.Effect<PeerDependencyRules, CatalogAssemblyFailure>;
3108
- /**
3109
- * Each importer's dependency-name → resolved-version map, as the manager's
3110
- * lockfile records it. Read from the same single lockfile read as `set`, and
3111
- * memoized with it.
3112
- *
3113
- * @remarks
3114
- * Feeds `WorkspaceStateSnapshot`'s fallback for a `catalog:` specifier no
3115
- * committed catalog source declares — the shape a config-dependency pnpmfile
3116
- * hook produces. Only pnpm records importer versions; a bun or npm workspace
3117
- * yields an empty index, and an absent or unreadable lockfile contributes
3118
- * nothing rather than failing.
3119
- */
3120
- readonly importerVersions: () => Effect.Effect<ImporterVersions, CatalogAssemblyFailure>;
3121
- /**
3122
- * Which version each declared config dependency was replayed from, keyed
3123
- * by name — off the same single memoized assemble pass as `set()`.
3124
- *
3125
- * @remarks
3126
- * Every dependency the hooks layer resolved when a `pnpm-workspace.yaml`
3127
- * was read — `{}` when it declares no config dependencies or under the
3128
- * no-op layer, which resolves nothing. Config dependencies are a pnpm
3129
- * feature, so the bun / `package.json` path yields `{}` too, exactly as
3130
- * `importerVersions` does.
3131
- */
3132
- readonly hookReplays: () => Effect.Effect<Readonly<Record<string, HookReplay>>, CatalogAssemblyFailure>;
3133
- /**
3134
- * Discard the memoized assembly so the **next** read re-assembles — the same
3135
- * single read and hook replay as the first, over the workspace as it stands
3136
- * then.
3137
- *
3138
- * @remarks
3139
- * The explicit memoization boundary for a tool that **mutates the workspace
3140
- * mid-run** — installs, bumps a config dependency, regenerates the lockfile.
3141
- * Release-age gating wants the before-state, a post-install peer check the
3142
- * after-state, and one infinite memo cannot serve both without this call in
3143
- * between; without it, `peerDependencyRules` keeps answering from the
3144
- * pre-mutation hook replay and the checker reports findings the new rules
3145
- * suppress.
3146
- *
3147
- * Unconditional and infallible: the memo is already success-only (a failed
3148
- * or interrupted assembly retries by itself), so `refresh` exists solely to
3149
- * discard a *successful* assembly that mutation has made stale. Calling it
3150
- * before any read is harmless.
3151
- */
3152
- readonly refresh: () => Effect.Effect<void>;
3153
- }
3357
+ export type VersioningStrategyType = typeof VersioningStrategyType.Type;
3154
3358
  /**
3155
- * Options for the {@link WorkspaceCatalogs} layer.
3359
+ * Arguments to {@link VersioningStrategy.classify}.
3156
3360
  *
3157
3361
  * @public
3158
3362
  */
3159
- interface WorkspaceCatalogsOptions {
3160
- /**
3161
- * The directory the workspace root is resolved from.
3162
- *
3163
- * @defaultValue `process.cwd()`, read lazily on first use.
3164
- */
3165
- readonly cwd?: string;
3363
+ interface ClassifyOptions {
3364
+ /** Publishable package names, in any order. Duplicates are collapsed. */
3365
+ readonly packages: ReadonlyArray<string>;
3166
3366
  /**
3167
- * A ceiling for the root ascent from `cwd`, passed straight through to the
3168
- * `stopAt` of {@link WorkspaceRoot}'s `find`.
3367
+ * Groups of packages that version in lockstep.
3169
3368
  *
3170
3369
  * @remarks
3171
- * Inclusive, and resolved to an absolute path exactly as `cwd` is. When no
3172
- * root is found at or below the ceiling, catalog assembly fails with
3173
- * {@link WorkspaceRootNotFoundError} carrying the resolved `stopAt` rather
3174
- * than adopting an enclosing directory's workspace. Pass the same value as
3175
- * {@link WorkspaceDiscoveryOptions.stopAt} so every service agrees on the
3176
- * root; the `Workspaces.*` composites forward one `stopAt` to all of them.
3177
- *
3178
- * @defaultValue no ceiling — the ascent runs to the filesystem root.
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.
3179
3377
  */
3180
- readonly stopAt?: string | undefined;
3378
+ readonly fixedGroups?: ReadonlyArray<ReadonlyArray<string>>;
3181
3379
  }
3182
- declare const WorkspaceCatalogs_base: Context.ServiceClass<WorkspaceCatalogs, "@effected/workspaces/WorkspaceCatalogs", WorkspaceCatalogsShape>;
3183
3380
  /**
3184
- * Assembles a workspace's catalogs, package-manager-aware.
3381
+ * Arguments to {@link VersioningStrategy.detect}.
3382
+ *
3383
+ * @public
3384
+ */
3385
+ interface VersioningDetectOptions {
3386
+ /** See {@link ClassifyOptions.fixedGroups}. Defaults to none. */
3387
+ readonly fixedGroups?: ReadonlyArray<ReadonlyArray<string>>;
3388
+ }
3389
+ /**
3390
+ * One entry in a release batch: which package went out, at which version.
3185
3391
  *
3186
3392
  * @remarks
3187
- * The inline source is picked by **file presence**, the same rule
3188
- * `internal/patterns.ts` uses for globs: a `pnpm-workspace.yaml` selects the pnpm
3189
- * path (its `catalog:` / `catalogs:` blocks); its absence selects the root
3190
- * `package.json` `workspaces.catalog` / `workspaces.catalogs` (bun's analogue).
3191
- * The lockfile source is PM-aware too — whichever extension the parsed lockfile
3192
- * 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.
3193
3396
  *
3194
- * Precedence, lowest first: catalogs recorded in the lockfile, then the inline
3195
- * declaration, then — only under {@link WorkspaceCatalogs.layerWithConfigDependencies}
3196
- * — the config-dependency `pnpmfile.cjs` hooks, each layer winning per dependency
3197
- * within a catalog. An unreadable or absent lockfile degrades to no lockfile
3198
- * catalogs rather than failing (the inline declaration is the source of truth, the
3199
- * lockfile a record of what was installed); a malformed inline source **fails
3200
- * typed** with `CatalogAssemblyError`, because a silently-empty catalog read
3201
- * 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.
3202
3415
  *
3203
- * Assembly is deferred to the first call and memoized success-only, matching
3204
- * {@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
+ * ```
3205
3430
  *
3206
3431
  * @public
3207
3432
  */
3208
- export declare class WorkspaceCatalogs extends WorkspaceCatalogs_base {
3209
- /** Builds the service. */
3210
- static readonly make: (options?: WorkspaceCatalogsOptions) => Effect.Effect<WorkspaceCatalogsShape, never, WorkspaceRoot | LockfileReader | ConfigDependencyHooks | FileSystem.FileSystem | Path.Path>;
3211
- /**
3212
- * The service over an explicit {@link ConfigDependencyHooks} layer — the
3213
- * one builder every `layer*` static and the `Workspaces` composites
3214
- * delegate to, so the hooks layer is provided to {@link WorkspaceCatalogs.make}
3215
- * in exactly one place.
3216
- *
3217
- * @remarks
3218
- * Reach for it when the hooks layer is chosen elsewhere — a composite that
3219
- * must hand ONE hooks reference to both this service and
3220
- * `WorkspaceSnapshots` (layers memoize by reference, so two references
3221
- * would replay twice and could run two policies), or a test wiring
3222
- * {@link ConfigDependencyHooks.layerFrom}. The three `layer*` statics are
3223
- * this with the hooks layer fixed. `R` is the hooks layer's own
3224
- * requirement (the subprocess variant's `ChildProcessSpawner`), threaded
3225
- * through. Parameterized, so bind it to a `const` and reuse it.
3226
- *
3227
- * @param hooks - The {@link ConfigDependencyHooks} layer to replay through.
3228
- * @param options - The same options the other layer statics take.
3229
- */
3230
- static readonly layerWithHooks: <R = never>(hooks: Layer.Layer<ConfigDependencyHooks, never, R>, options?: WorkspaceCatalogsOptions) => Layer.Layer<WorkspaceCatalogs, never, WorkspaceRoot | LockfileReader | FileSystem.FileSystem | Path.Path | R>;
3231
- /**
3232
- * The live layer — the default, which **never executes config-dependency
3233
- * code**: it wires {@link ConfigDependencyHooks.layerNoop}, whose hooks return
3234
- * the inline-catalog seed untouched.
3235
- *
3236
- * @remarks
3237
- * Parameterized, so it mints a fresh reference per call — bind it to a
3238
- * `const` and reuse it, or layer memoization does not apply.
3239
- */
3240
- static readonly layer: (options?: WorkspaceCatalogsOptions) => Layer.Layer<WorkspaceCatalogs, never, WorkspaceRoot | LockfileReader | FileSystem.FileSystem | Path.Path>;
3433
+ export declare class VersioningStrategy extends VersioningStrategy_base {
3241
3434
  /**
3242
- * The opt-in live layer that **does** replay config-dependency `pnpmfile.cjs`
3243
- * hooks: it wires {@link ConfigDependencyHooks.layerLive}, which dynamically
3244
- * imports and runs each config dependency's `updateConfig` in process.
3245
- *
3246
- * @remarks
3247
- * Same output and requirement set as {@link WorkspaceCatalogs.layer} — the only
3248
- * difference is that config-dependency code is executed. Use it deliberately;
3249
- * the default catalog path stays free of any config-dependency execution.
3250
- * Parameterized, so bind it to a `const` and reuse it.
3435
+ * Whether a release needs one tag per package rather than one shared tag.
3251
3436
  */
3252
- 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;
3253
3440
  /**
3254
- * The opt-in layer that replays config-dependency `pnpmfile` hooks in a
3255
- * `node` **child process**: it wires
3256
- * {@link ConfigDependencyHooks.layerSubprocess} in place of the in-process
3257
- * `layerLive`.
3441
+ * Classify a workspace from its publishable package names and fixed groups.
3258
3442
  *
3259
3443
  * @remarks
3260
- * Same typed semantics as {@link WorkspaceCatalogs.layerWithConfigDependencies}
3261
- * — the two hook layers are drop-in interchangeable — but the replay's
3262
- * computed dynamic `import()` runs in the subprocess, so it survives bundling
3263
- * (a bundler compiles a computed in-process `import()` into a context module
3264
- * that cannot resolve at runtime — every bundled GitHub Action hits this).
3265
- * The cost is one extra requirement: core's `ChildProcessSpawner`, provided
3266
- * once at the edge (`@effect/platform-node`'s `NodeServices.layer`) — the
3267
- * same sanctioned R-widening as `Workspaces.layerWithGit`. Parameterized, so
3268
- * 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.
3269
3447
  */
3270
- static readonly layerWithConfigDependenciesSubprocess: (options?: WorkspaceCatalogsOptions) => Layer.Layer<WorkspaceCatalogs, never, WorkspaceRoot | LockfileReader | FileSystem.FileSystem | Path.Path | ChildProcessSpawner.ChildProcessSpawner>;
3448
+ static classify(options: ClassifyOptions): VersioningStrategy;
3271
3449
  /**
3272
- * A test double satisfying the full {@link WorkspaceCatalogsShape} with no
3273
- * 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.
3274
3452
  *
3275
3453
  * @remarks
3276
- * There is **no honest default catalog set**: an empty `CatalogSet` that
3277
- * looks like a legitimate answer is the "every dependency looks newly added"
3278
- * failure class the live assembler hard-fails to prevent, so every method
3279
- * **dies** with an instructive defect until stubbed — a test-wiring mistake
3280
- * fails loudly as a defect rather than succeeding with a lie or failing with
3281
- * a dishonest typed error.
3282
- *
3283
- * The one derivation mirrors `WorkspaceDiscovery.makeTest`'s
3284
- * derived-from-the-primary rule: when a `set` override is supplied,
3285
- * `resolveSpecifier` answers from that `CatalogSet`'s own
3286
- * {@link CatalogSet.resolveSpecifier} — exactly what the live service runs
3287
- * over its assembled set — so the two stay consistent by construction.
3288
- * `releaseAgeGate`, `importerVersions` and `hookReplays` are **not**
3289
- * derivable from a catalog set (the gate comes from release-age keys and
3290
- * hook contributions, the importer index from the lockfile's importer
3291
- * blocks, the replay record from the hooks layer — none is in a
3292
- * `CatalogSet`) and always die unless stubbed.
3293
- *
3294
- * `refresh` defaults to `Effect.void` honestly: the double holds no memo,
3295
- * so "drop the memoized assembly" is genuinely a no-op — the stubs answer
3296
- * fresh on every call already. This mirrors `WorkspaceDiscovery.makeTest`'s
3297
- * `refresh`.
3298
- *
3299
- * @example
3300
- * ```ts
3301
- * import { CatalogSet, WorkspaceCatalogs } from "@effected/workspaces";
3302
- * import { Effect } from "effect";
3303
- *
3304
- * const double = WorkspaceCatalogs.makeTest({
3305
- * set: () => Effect.succeed(CatalogSet.fromCatalogs({ default: { effect: "4.0.0" } })),
3306
- * });
3307
- * // `resolveSpecifier` now answers consistently from that set.
3308
- * ```
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.
3309
3457
  */
3310
- static readonly makeTest: (overrides?: Partial<WorkspaceCatalogsShape>) => WorkspaceCatalogsShape;
3458
+ static readonly detect: (options?: VersioningDetectOptions | undefined) => Effect.Effect<VersioningStrategy, WorkspaceDiscoveryFailure, PublishabilityDetector | WorkspaceDiscovery>;
3311
3459
  /**
3312
- * The test layer: {@link WorkspaceCatalogs.makeTest} behind `Layer.succeed`,
3313
- * so a suite provides only the methods it exercises.
3460
+ * The tags a release of `releases` produces under this strategy.
3314
3461
  *
3315
3462
  * @remarks
3316
- * A parameterized layer factory mints a **fresh reference per call**, and
3317
- * layers memoize by reference — bind the result to a `const` and reuse it
3318
- * rather than calling `layerTest(...)` at each composition site.
3319
- *
3320
- * @example
3321
- * ```ts
3322
- * import { CatalogSet, WorkspaceCatalogs } from "@effected/workspaces";
3323
- * import { Effect } from "effect";
3324
- *
3325
- * const TestCatalogs = WorkspaceCatalogs.layerTest({
3326
- * set: () => Effect.succeed(CatalogSet.empty()),
3327
- * });
3328
- * // program.pipe(Effect.provide(TestCatalogs))
3329
- * ```
3330
- */
3331
- static readonly layerTest: (overrides?: Partial<WorkspaceCatalogsShape>) => Layer.Layer<WorkspaceCatalogs>;
3332
- /**
3333
- * The real implementation of `@effected/npm`'s `CatalogResolver` contract —
3334
- * 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.
3335
3470
  *
3336
- * @remarks
3337
- * `rangeOf` returns `Option.none()` for a dependency no catalog declares, per
3338
- * the contract's convention. A failed catalog *assembly* — an unreadable or
3339
- * malformed `pnpm-workspace.yaml`, a broken config-dependency hook — passes
3340
- * through **typed** as the contract's `CatalogAssemblyError` (it used to be
3341
- * folded into `DependencyResolutionError`'s defect `cause`, which forced
3342
- * consumers to `_tag`-sniff `unknown`); only the remaining mechanism failure,
3343
- * an unfindable workspace root, is wrapped as `DependencyResolutionError`.
3471
+ * An empty batch produces no tags under either style.
3344
3472
  */
3345
- static readonly catalogResolver: Layer.Layer<CatalogResolver, never, WorkspaceCatalogs>;
3473
+ tagsFor(releases: ReadonlyArray<PackageRelease>, options?: TagFormatOptions): ReadonlyArray<ReleaseTag>;
3346
3474
  }
3347
3475
  //#endregion
3348
3476
  //#region src/WorkspaceStateSnapshot.d.ts
@@ -3350,23 +3478,24 @@ declare const PackageStateSnapshot_base: Schema.Class<PackageStateSnapshot, Sche
3350
3478
  /** The package name. */
3351
3479
  readonly name: Schema.NonEmptyString;
3352
3480
  /**
3353
- * 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
3354
3482
  * a manifest that declared none.
3355
3483
  *
3356
3484
  * @remarks
3357
- * `WorkspacePackage.version` is optional and a version-less member is an
3358
- * ordinary pnpm shape, but this field stays a plain required string because a
3359
- * snapshot is a serialized value someone stores and diffs: an absent key and
3360
- * a present one would read as a change the moment one side of a diff was
3361
- * captured by a different code path. **`""` is therefore the sentinel for
3362
- * "the manifest declared no version", not a version anyone can use** — both
3363
- * `snapshotOf` at a ref and the worktree snapshot write it, so the two sides
3364
- * agree. Every resolution surface treats it as absence:
3365
- * {@link WorkspaceStateSnapshot.resolve} answers `Option.none()` for a
3366
- * `workspace:` specifier against it, because `some("")` would rewrite
3367
- * `workspace:^` as a bare `"^"`.
3368
- */
3369
- 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>;
3370
3499
  /** POSIX path relative to the workspace root; `"."` for the root package. */
3371
3500
  readonly relativePath: Schema.String;
3372
3501
  /** Production dependencies. */
@@ -3497,15 +3626,13 @@ declare const WorkspaceStateSnapshot_base: Schema.Class<WorkspaceStateSnapshot,
3497
3626
  export declare class WorkspaceStateSnapshot extends WorkspaceStateSnapshot_base {
3498
3627
  #private;
3499
3628
  /**
3500
- * 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.
3501
3631
  *
3502
3632
  * @remarks
3503
- * Values are `PackageStateSnapshot.version` verbatim, so a member whose
3504
- * manifest declared no version maps to the **`""` sentinel** rather than
3505
- * being absent from the map — presence here answers membership, not
3506
- * "has a usable version". A caller reading a version out of this map owes
3507
- * the `""` check itself; {@link WorkspaceStateSnapshot.resolve} already
3508
- * 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.
3509
3636
  */
3510
3637
  get versions(): ReadonlyMap<string, string>;
3511
3638
  /** A single captured package by name, or `Option.none()`. Total. */
@@ -4497,6 +4624,33 @@ interface WorkspaceDiscoverySkip {
4497
4624
  */
4498
4625
  readonly cause: unknown;
4499
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
+ }
4500
4654
  /**
4501
4655
  * The nearest workspace root at or above `cwd`, or `null`.
4502
4656
  *
@@ -4517,9 +4671,17 @@ interface WorkspaceDiscoverySkip {
4517
4671
  * Markers match the async service exactly: a `pnpm-workspace.yaml`, or a
4518
4672
  * `package.json` carrying a `workspaces` field.
4519
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
+ *
4520
4681
  * @param cwd - Where to start the ascent (typically `process.cwd()`),
4521
4682
  * matching the Effect layers' `{ cwd }` option.
4522
- * @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.
4523
4685
  *
4524
4686
  * @example
4525
4687
  * ```ts
@@ -4540,7 +4702,7 @@ interface WorkspaceDiscoverySkip {
4540
4702
  *
4541
4703
  * @public
4542
4704
  */
4543
- export declare const findWorkspaceRootSync: (cwd: string, options: WorkspacesSyncOptions) => string | null;
4705
+ export declare const findWorkspaceRootSync: (cwd: string, options: FindWorkspaceRootSyncOptions) => string | null;
4544
4706
  /**
4545
4707
  * Options for {@link getWorkspacePackagesSync}: the required consumer-supplied
4546
4708
  * operations plus the traversal bound.
@@ -4628,5 +4790,5 @@ interface GetWorkspacePackagesSyncOptions extends WorkspacesSyncOptions {
4628
4790
  */
4629
4791
  export declare const getWorkspacePackagesSync: (root: string, options: GetWorkspacePackagesSyncOptions) => ReadonlyArray<WorkspacePackage>;
4630
4792
  //#endregion
4631
- 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 };
4632
4794
  //# sourceMappingURL=index.d.ts.map