@effected/workspaces 0.8.0 → 0.9.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.
@@ -103,7 +103,11 @@ const CHECKED = [
103
103
  "bun.lock",
104
104
  "bun.lockb",
105
105
  "yarn.lock",
106
- "package.json#workspaces"
106
+ "package.json#workspaces",
107
+ "pnpm-lock.yaml",
108
+ "package-lock.json",
109
+ "package.json#packageManager",
110
+ "package.json#devEngines.packageManager"
107
111
  ];
108
112
  /**
109
113
  * Detects which package manager owns a workspace root.
@@ -217,7 +221,8 @@ var PackageManagerDetector = class PackageManagerDetector extends Context.Servic
217
221
  version: versionFor(hints, "pnpm"),
218
222
  runtime: "node"
219
223
  });
220
- if (((yield* has(root, "bun.lock")) || (yield* has(root, "bun.lockb"))) && namesManager(hints, "bun")) return DetectedPackageManager.make({
224
+ const bunLock = (yield* has(root, "bun.lock")) || (yield* has(root, "bun.lockb"));
225
+ if (bunLock && namesManager(hints, "bun")) return DetectedPackageManager.make({
221
226
  name: "bun",
222
227
  version: versionFor(hints, "bun"),
223
228
  runtime: "bun"
@@ -233,6 +238,35 @@ var PackageManagerDetector = class PackageManagerDetector extends Context.Servic
233
238
  version: versionFor(hints, "npm"),
234
239
  runtime: "node"
235
240
  });
241
+ if (yield* has(root, "pnpm-lock.yaml")) return DetectedPackageManager.make({
242
+ name: "pnpm",
243
+ version: versionFor(hints, "pnpm"),
244
+ runtime: "node"
245
+ });
246
+ if (bunLock && namesManager(hints, "bun")) return DetectedPackageManager.make({
247
+ name: "bun",
248
+ version: versionFor(hints, "bun"),
249
+ runtime: "bun"
250
+ });
251
+ if ((yield* has(root, "yarn.lock")) && namesManager(hints, "yarn")) return DetectedPackageManager.make({
252
+ name: "yarn",
253
+ version: versionFor(hints, "yarn"),
254
+ runtime: "node"
255
+ });
256
+ if (yield* has(root, "package-lock.json")) return DetectedPackageManager.make({
257
+ name: "npm",
258
+ version: versionFor(hints, "npm"),
259
+ runtime: "node"
260
+ });
261
+ const declared = declaredName(hints);
262
+ if (Option.isSome(declared)) {
263
+ const name = declared.value;
264
+ if (name === "pnpm" || name === "npm" || name === "yarn" || name === "bun") return DetectedPackageManager.make({
265
+ name,
266
+ version: versionFor(hints, name),
267
+ runtime: name === "bun" ? "bun" : "node"
268
+ });
269
+ }
236
270
  return yield* Effect.fail(new PackageManagerDetectionError({
237
271
  root,
238
272
  checked: CHECKED
@@ -242,6 +276,53 @@ var PackageManagerDetector = class PackageManagerDetector extends Context.Servic
242
276
  });
243
277
  /** The live layer. */
244
278
  static layer = Layer.effect(PackageManagerDetector, PackageManagerDetector.make);
279
+ /**
280
+ * The sanctioned in-memory double.
281
+ *
282
+ * @remarks
283
+ * **`detect` has no honest default, so an unstubbed call dies** — the
284
+ * `WorkspaceDiscovery.info` posture, for the same reason. A stand-in that
285
+ * answered `"pnpm"` would hand a consumer a fact nothing established, and it
286
+ * would contradict the very service it stands in for: the live detector's
287
+ * defining property is that it [refuses to
288
+ * guess](https://github.com/spencerbeggs/effected) when no evidence matches.
289
+ * A double that guesses is worse than no double.
290
+ *
291
+ * Failing typed would be the subtler mistake: `PackageManagerDetectionError`
292
+ * reads as a legitimate "no manager here" answer, so a consumer would branch
293
+ * on it and proceed, never learning that the test simply forgot to stub.
294
+ *
295
+ * @param overrides - Members to supply; anything omitted dies on use.
296
+ *
297
+ * @example
298
+ * ```ts
299
+ * import { DetectedPackageManager, PackageManagerDetector } from "@effected/workspaces";
300
+ * import { Effect, Option } from "effect";
301
+ *
302
+ * const TestDetector = PackageManagerDetector.layerTest({
303
+ * detect: () =>
304
+ * Effect.succeed(
305
+ * DetectedPackageManager.make({ name: "pnpm", version: Option.none(), runtime: "node" }),
306
+ * ),
307
+ * });
308
+ * ```
309
+ */
310
+ static makeTest = (overrides = {}) => ({
311
+ detect: () => Effect.die(/* @__PURE__ */ new Error("PackageManagerDetector.makeTest: detect() was called but not stubbed — no honest default DetectedPackageManager exists for a test double; pass a `detect` override.")),
312
+ ...overrides
313
+ });
314
+ /**
315
+ * {@link PackageManagerDetector.makeTest} behind `Layer.succeed`.
316
+ *
317
+ * @remarks
318
+ * A parameterized layer factory mints a **fresh reference per call**, and
319
+ * layers memoize by reference — bind the result to a `const` and reuse it
320
+ * rather than calling `layerTest(...)` at each composition site.
321
+ *
322
+ * Pairs with `WorkspaceRoot.layerTest` and `WorkspaceDiscovery.layerTest` to
323
+ * stand up the whole discovery path with no filesystem at all.
324
+ */
325
+ static layerTest = (overrides = {}) => Layer.succeed(PackageManagerDetector, PackageManagerDetector.makeTest(overrides));
245
326
  };
246
327
 
247
328
  //#endregion
package/Publishability.js CHANGED
@@ -84,9 +84,35 @@ var PublishTarget = class extends Schema.Class("PublishTarget")({
84
84
  *
85
85
  * @public
86
86
  */
87
- var PublishabilityDetector = class PublishabilityDetector extends Context.Service()("@effected/workspaces/PublishabilityDetector") {
88
- /** Standard npm publishing semantics. Pure — no filesystem, no platform services. */
89
- static layer = Layer.succeed(PublishabilityDetector, { detect: (pkg) => Effect.sync(() => {
87
+ var PublishabilityDetector = class extends Context.Service()("@effected/workspaces/PublishabilityDetector") {
88
+ /**
89
+ * Standard npm publishing semantics, **as a value**. Pure no filesystem,
90
+ * no platform services.
91
+ *
92
+ * @remarks
93
+ * Exposed as a shape and not only as a layer, because a consumer composing
94
+ * *around* these rules cannot reach them through a layer without re-entering
95
+ * the very tag it is replacing. `@savvy-web/silk-effects` had to write
96
+ * `Effect.provide(PublishabilityDetector, PublishabilityDetector.layer)`
97
+ * **inside its own implementation of that tag** to get at this function for
98
+ * its pass-through branch; with the value exposed that becomes
99
+ * `PublishabilityDetector.npm.detect(pkg)`.
100
+ *
101
+ * @example
102
+ * ```ts
103
+ * import { PublishabilityDetector } from "@effected/workspaces";
104
+ * import { Effect, Layer } from "effect";
105
+ *
106
+ * // A policy that defers to npm semantics for everything it does not veto.
107
+ * const withVeto = Layer.succeed(PublishabilityDetector, {
108
+ * detect: (pkg) =>
109
+ * pkg.name.endsWith("-private")
110
+ * ? Effect.succeed([])
111
+ * : PublishabilityDetector.npm.detect(pkg),
112
+ * });
113
+ * ```
114
+ */
115
+ static npm = { detect: (pkg) => Effect.sync(() => {
90
116
  const config = pkg.publishConfig;
91
117
  const access = config?.access;
92
118
  if (pkg.private && access === void 0) return [];
@@ -97,7 +123,33 @@ var PublishabilityDetector = class PublishabilityDetector extends Context.Servic
97
123
  access: access ?? "public",
98
124
  provenance: false
99
125
  })];
100
- }) });
126
+ }) };
127
+ /** Nothing publishes. */
128
+ static none = { detect: () => Effect.succeed([]) };
129
+ /**
130
+ * {@link PublishabilityDetector.npm} as a layer.
131
+ *
132
+ * @remarks
133
+ * Named for its policy rather than called `layer`, deliberately. **No
134
+ * composite in this package provides a publishability detector**: a
135
+ * `Workspaces.layer()` that quietly supplied npm semantics made the choice
136
+ * invisible, and worse, made a naively-ordered override lose to it in
137
+ * silence — `Layer.mergeAll(myDetector, Workspaces.layer())` resolved to the
138
+ * default, because `mergeAll` is last-wins. For a service that decides
139
+ * whether a package publishes and to which registry, that silent revert was
140
+ * the worst available failure. The requirement now sits in `R`, so the
141
+ * choice is made once, explicitly, and unmade wiring does not compile.
142
+ */
143
+ static layerNpm = Layer.succeed(this, this.npm);
144
+ /**
145
+ * {@link PublishabilityDetector.none} as a layer: a workspace where nothing
146
+ * publishes.
147
+ *
148
+ * @remarks
149
+ * For dry runs, and for a release tool whose configuration disables
150
+ * publishing wholesale — silk's changeset `mode: "none"` is exactly this.
151
+ */
152
+ static layerNone = Layer.succeed(this, this.none);
101
153
  };
102
154
 
103
155
  //#endregion
package/ReleaseTag.js ADDED
@@ -0,0 +1,260 @@
1
+ import { Schema } from "effect";
2
+
3
+ //#region src/ReleaseTag.ts
4
+ /**
5
+ * Whether one shared tag names a whole release, or one tag names each package.
6
+ *
7
+ * @remarks
8
+ * `single` is the shape of a single-package repo and of a monorepo whose
9
+ * publishable packages all version in lockstep; `scoped` is the shape of
10
+ * independent versioning, where a shared tag would be ambiguous.
11
+ *
12
+ * @public
13
+ */
14
+ const TagStyle = Schema.Literals(["single", "scoped"]);
15
+ /** Digits only, and no leading zeros beyond `0` itself — SemVer's numeric identifier. */
16
+ const NUMERIC_IDENTIFIER = /^(?:0|[1-9]\d*)$/;
17
+ /**
18
+ * Read a version's numeric core, or nothing when it is not `X.Y.Z[-pre][+build]`.
19
+ *
20
+ * Build metadata is stripped **before** the prerelease test: `+build` carries
21
+ * no precedence meaning in SemVer, so `1.2.3+sha.abc` is the stable `1.2.3`,
22
+ * and treating any `-`-or-`+` suffix as a prerelease is the obvious wrong
23
+ * reading.
24
+ */
25
+ const versionCore = (version) => {
26
+ const withoutBuild = version.split("+", 1)[0] ?? "";
27
+ const dash = withoutBuild.indexOf("-");
28
+ const prerelease = dash !== -1;
29
+ const segments = (prerelease ? withoutBuild.slice(0, dash) : withoutBuild).split(".");
30
+ if (segments.length !== 3) return void 0;
31
+ if (!segments.every((segment) => NUMERIC_IDENTIFIER.test(segment))) return void 0;
32
+ return {
33
+ major: Number(segments[0]),
34
+ minor: Number(segments[1]),
35
+ prerelease
36
+ };
37
+ };
38
+ /**
39
+ * A floating alias tag — `v1`, `v1.2` — that a repo re-points at its newest
40
+ * matching release.
41
+ *
42
+ * @remarks
43
+ * This is the GitHub Actions distribution convention: a consumer writes
44
+ * `uses: owner/repo@v1` and receives whatever 1.x the repo last pointed `v1` at.
45
+ *
46
+ * **Deliberately not SemVer, and deliberately not a {@link (TagStyle:variable)}.**
47
+ * A release tag names one immutable version; a tracking tag is an alias derived
48
+ * *from* a version, carrying a truncated number that is not a version at all.
49
+ * Folding it into `ReleaseTag` as a third style would put a mutable pointer and
50
+ * an immutable name behind one type.
51
+ *
52
+ * Everything here is derivation, formatting and parsing. **Actually moving a
53
+ * git tag is not this package's business** — a consumer does that through git,
54
+ * and the deliberate omission is what keeps this module a pure leaf.
55
+ *
56
+ * @example
57
+ * ```ts
58
+ * import { TrackingTag } from "@effected/workspaces";
59
+ *
60
+ * TrackingTag.forVersion("1.2.3").map((t) => t.value); // ["v1", "v1.2"]
61
+ * TrackingTag.forVersion("1.0.0-beta.3"); // [] — never float onto a beta
62
+ * TrackingTag.forVersion("1.2.3", { packageName: "@acme/cli" });
63
+ * // ["@acme/cli@v1", "@acme/cli@v1.2"]
64
+ * ```
65
+ *
66
+ * @public
67
+ */
68
+ var TrackingTag = class TrackingTag extends Schema.Class("TrackingTag")({
69
+ /** The tag string exactly as it appears in git. */
70
+ value: Schema.NonEmptyString,
71
+ /** The package the alias namespaces; absent on a bare `v1`. */
72
+ packageName: Schema.optionalKey(Schema.NonEmptyString),
73
+ /** The major version the alias tracks. */
74
+ major: Schema.Int,
75
+ /** The minor version, on a `v1.2`-precision alias; absent on `v1`. */
76
+ minor: Schema.optionalKey(Schema.Int)
77
+ }) {
78
+ /** Whether this alias tracks a whole major line, or one minor line inside it. */
79
+ get precision() {
80
+ return this.minor === void 0 ? "major" : "minor";
81
+ }
82
+ /**
83
+ * The tracking tags a release of `version` should be pointed at.
84
+ *
85
+ * @remarks
86
+ * **A prerelease derives nothing.** Anyone depending on `owner/repo@v1` is
87
+ * asking for the newest *stable* 1.x, so re-pointing that alias at
88
+ * `1.0.0-beta.3` would ship a prerelease to every such consumer with no
89
+ * signal at all. `includePrerelease` exists for callers who genuinely mean
90
+ * it — a prerelease-only distribution channel — and should be rare.
91
+ *
92
+ * **Total, never throwing.** A version that is not `X.Y.Z` derives nothing
93
+ * rather than failing: this is a query about a version, not a validation of
94
+ * one, and `WorkspacePackage.version` is deliberately tolerant, so odd
95
+ * versions reach here routinely.
96
+ *
97
+ * 0.x versions DO derive aliases. Floating `v0` across 0.x minors is a real
98
+ * hazard, but which aliases to publish is the caller's policy, decided where
99
+ * the tags are moved — not something a derivation should quietly withhold.
100
+ *
101
+ * @param version - The version being released.
102
+ * @param options - Package prefix, precision and the prerelease override.
103
+ * @returns The aliases, broadest first; empty when none apply.
104
+ */
105
+ static forVersion(version, options) {
106
+ const core = versionCore(version);
107
+ if (core === void 0) return [];
108
+ if (core.prerelease && options?.includePrerelease !== true) return [];
109
+ const packageName = options?.packageName;
110
+ const prefix = packageName === void 0 ? "" : `${packageName}@`;
111
+ const named = (suffix, fields) => TrackingTag.make({
112
+ value: `${prefix}v${suffix}`,
113
+ ...packageName !== void 0 && { packageName },
114
+ major: fields.major,
115
+ ...fields.minor !== void 0 && { minor: fields.minor }
116
+ });
117
+ const major = named(`${core.major}`, { major: core.major });
118
+ if (options?.precision === "major") return [major];
119
+ return [major, named(`${core.major}.${core.minor}`, {
120
+ major: core.major,
121
+ minor: core.minor
122
+ })];
123
+ }
124
+ };
125
+ /** A tag string split into an optional package prefix and the version part. */
126
+ const splitTag = (tag) => {
127
+ const at = tag.lastIndexOf("@");
128
+ if (at === -1) return { rest: tag };
129
+ const packageName = tag.slice(0, at);
130
+ const rest = tag.slice(at + 1);
131
+ if (packageName === "" || rest === "") return void 0;
132
+ return {
133
+ packageName,
134
+ rest
135
+ };
136
+ };
137
+ /**
138
+ * Decide whether a tag string is a release tag, a tracking alias, or neither.
139
+ *
140
+ * @remarks
141
+ * The two families are told apart by **segment count**, not by the `v` prefix:
142
+ * three numeric segments is a version (so `1.0.0` and `v1.0.0` are both release
143
+ * tags), while one or two segments is a truncated alias. The `v` *is* required
144
+ * on an alias — a bare `1` is neither valid SemVer nor the tracking convention,
145
+ * and accepting it would make this function guess.
146
+ *
147
+ * The package prefix splits at the **last** `@`, so a leading npm scope
148
+ * survives: `@scope/pkg@1.0.0` is package `@scope/pkg` at version `1.0.0`.
149
+ *
150
+ * Round-tripping is a tested property: every tag {@link ReleaseTag} and
151
+ * {@link TrackingTag} format classifies back to the family that produced it,
152
+ * with its fields intact.
153
+ *
154
+ * @param tag - Any tag string.
155
+ * @returns The classification.
156
+ *
157
+ * @public
158
+ */
159
+ const classifyTag = (tag) => {
160
+ const split = splitTag(tag);
161
+ if (split === void 0) return { kind: "unrecognized" };
162
+ const { packageName, rest } = split;
163
+ const withoutV = rest.startsWith("v") ? rest.slice(1) : rest;
164
+ const hasV = rest.startsWith("v");
165
+ if (versionCore(withoutV) !== void 0) return {
166
+ kind: "release",
167
+ tag: ReleaseTag.make({
168
+ value: tag,
169
+ ...packageName !== void 0 && { packageName },
170
+ version: withoutV,
171
+ style: packageName === void 0 ? "single" : "scoped"
172
+ })
173
+ };
174
+ if (!hasV) return { kind: "unrecognized" };
175
+ const segments = withoutV.split(".");
176
+ if (segments.length > 2 || !segments.every((segment) => NUMERIC_IDENTIFIER.test(segment))) return { kind: "unrecognized" };
177
+ const major = Number(segments[0]);
178
+ const minor = segments.length === 2 ? Number(segments[1]) : void 0;
179
+ return {
180
+ kind: "tracking",
181
+ tag: TrackingTag.make({
182
+ value: tag,
183
+ ...packageName !== void 0 && { packageName },
184
+ major,
185
+ ...minor !== void 0 && { minor }
186
+ })
187
+ };
188
+ };
189
+ /**
190
+ * A git tag naming a release, and the parts it was built from.
191
+ *
192
+ * @remarks
193
+ * `value` is the tag exactly as it appears in git; `version` stays **bare**
194
+ * even when `value` carries a prefix, so a consumer comparing versions never
195
+ * has to strip one back off.
196
+ *
197
+ * Formatting is **total**: there is no error channel, because the only failure
198
+ * v3 modelled — an empty version — is caught by `Schema.NonEmptyString` when
199
+ * the value is constructed. A bad version reaching these statics is developer
200
+ * wiring rather than untrusted input, so it dies as a defect, the same posture
201
+ * as an uncompilable glob literal in `WorkspacePackage.matchesDependency`.
202
+ *
203
+ * @example
204
+ * ```ts
205
+ * import { ReleaseTag } from "@effected/workspaces";
206
+ *
207
+ * ReleaseTag.single("1.2.3").value; // "1.2.3"
208
+ * ReleaseTag.scoped("@acme/cli", "1.2.3").value; // "@acme/cli@1.2.3"
209
+ * ReleaseTag.scoped("cli", "1.2.3").value; // "cli@1.2.3"
210
+ * ReleaseTag.scoped("cli", "1.2.3", { versionPrefix: "v" }).value; // "cli@v1.2.3"
211
+ * ```
212
+ *
213
+ * @public
214
+ */
215
+ var ReleaseTag = class ReleaseTag extends Schema.Class("ReleaseTag")({
216
+ /** The tag string exactly as it appears in git. */
217
+ value: Schema.NonEmptyString,
218
+ /** The package the tag names; absent on a workspace-wide single tag. */
219
+ packageName: Schema.optionalKey(Schema.NonEmptyString),
220
+ /** The version the tag names, without any prefix. */
221
+ version: Schema.NonEmptyString,
222
+ /** Which style produced it. */
223
+ style: TagStyle
224
+ }) {
225
+ /**
226
+ * One shared tag for a whole release: `1.2.3`.
227
+ *
228
+ * @param version - The version being released. Must not be empty.
229
+ * @param options - Formatting overrides.
230
+ */
231
+ static single(version, options) {
232
+ const prefix = options?.versionPrefix ?? "";
233
+ return ReleaseTag.make({
234
+ value: `${prefix}${version}`,
235
+ version,
236
+ style: "single"
237
+ });
238
+ }
239
+ /**
240
+ * A per-package tag: `<packageName>@<version>` — `@scope/pkg@1.2.3` for a
241
+ * scoped name, `pkg@1.2.3` for an unscoped one, uniformly, unless
242
+ * `options.versionPrefix` says otherwise.
243
+ *
244
+ * @param packageName - The package being released. Must not be empty.
245
+ * @param version - The version being released. Must not be empty.
246
+ * @param options - Formatting overrides.
247
+ */
248
+ static scoped(packageName, version, options) {
249
+ const prefix = options?.versionPrefix ?? "";
250
+ return ReleaseTag.make({
251
+ value: `${packageName}@${prefix}${version}`,
252
+ packageName,
253
+ version,
254
+ style: "scoped"
255
+ });
256
+ }
257
+ };
258
+
259
+ //#endregion
260
+ export { ReleaseTag, TagStyle, TrackingTag, classifyTag };
@@ -0,0 +1,122 @@
1
+ import { WorkspaceDiscovery } from "./WorkspaceDiscovery.js";
2
+ import { PublishabilityDetector } from "./Publishability.js";
3
+ import { ReleaseTag } from "./ReleaseTag.js";
4
+ import { Effect, Schema } from "effect";
5
+
6
+ //#region src/VersioningStrategy.ts
7
+ /**
8
+ * How a workspace assigns versions across its publishable packages.
9
+ *
10
+ * @remarks
11
+ * - `single` — zero or one publishable package, so one tag names the release.
12
+ * - `fixed-group` — every publishable package sits inside one group that
13
+ * versions in lockstep, so one tag still names the release.
14
+ * - `independent` — publishable packages version separately, so a shared tag
15
+ * would be ambiguous and each package needs its own.
16
+ *
17
+ * @public
18
+ */
19
+ const VersioningStrategyType = Schema.Literals([
20
+ "single",
21
+ "fixed-group",
22
+ "independent"
23
+ ]);
24
+ /**
25
+ * How a workspace versions, and the tagging that follows from it.
26
+ *
27
+ * @remarks
28
+ * Built either purely with {@link VersioningStrategy.classify}, or from a live
29
+ * workspace with {@link VersioningStrategy.detect}.
30
+ *
31
+ * @example
32
+ * ```ts
33
+ * import { VersioningStrategy } from "@effected/workspaces";
34
+ * import { Effect } from "effect";
35
+ *
36
+ * const program = Effect.gen(function* () {
37
+ * const strategy = yield* VersioningStrategy.detect({ fixedGroups });
38
+ * return strategy.tagsFor(released).map((tag) => tag.value);
39
+ * });
40
+ * ```
41
+ *
42
+ * @public
43
+ */
44
+ var VersioningStrategy = class VersioningStrategy extends Schema.Class("VersioningStrategy")({
45
+ /** The classification. */
46
+ type: VersioningStrategyType,
47
+ /** The groups classification was performed against, as supplied. */
48
+ fixedGroups: Schema.Array(Schema.Array(Schema.String)),
49
+ /** The publishable package names, sorted and de-duplicated. */
50
+ publishablePackages: Schema.Array(Schema.String)
51
+ }) {
52
+ /**
53
+ * Whether a release needs one tag per package rather than one shared tag.
54
+ */
55
+ get perPackageTags() {
56
+ return this.type === "independent";
57
+ }
58
+ /** The tag style this strategy implies. */
59
+ get tagStyle() {
60
+ return this.perPackageTags ? "scoped" : "single";
61
+ }
62
+ /**
63
+ * Classify a workspace from its publishable package names and fixed groups.
64
+ *
65
+ * @remarks
66
+ * Pure and total — no IO, no error channel. `packages` is sorted and
67
+ * de-duplicated first, so a name listed twice cannot inflate a one-package
68
+ * repo into an independent one.
69
+ */
70
+ static classify(options) {
71
+ const fixedGroups = options.fixedGroups ?? [];
72
+ const packages = [...new Set(options.packages)].sort();
73
+ const type = packages.length <= 1 ? "single" : fixedGroups.some((group) => packages.every((name) => group.includes(name))) ? "fixed-group" : "independent";
74
+ return VersioningStrategy.make({
75
+ type,
76
+ fixedGroups,
77
+ publishablePackages: packages
78
+ });
79
+ }
80
+ /**
81
+ * Classify the ambient workspace: enumerate its packages, keep the ones the
82
+ * {@link PublishabilityDetector} says publish somewhere, and classify those.
83
+ *
84
+ * @remarks
85
+ * The publishability question is asked through the service precisely so a
86
+ * consumer with its own rules — honouring a release tool's ignore list, say —
87
+ * swaps the layer instead of filtering afterwards.
88
+ */
89
+ static detect = Effect.fn("VersioningStrategy.detect")(function* (options) {
90
+ const discovery = yield* WorkspaceDiscovery;
91
+ const publishability = yield* PublishabilityDetector;
92
+ const packages = yield* discovery.listPackages();
93
+ const publishable = [];
94
+ for (const candidate of packages) if ((yield* publishability.detect(candidate)).length > 0) publishable.push(candidate.name);
95
+ return VersioningStrategy.classify({
96
+ packages: publishable,
97
+ ...options?.fixedGroups !== void 0 && { fixedGroups: options.fixedGroups }
98
+ });
99
+ });
100
+ /**
101
+ * The tags a release of `releases` produces under this strategy.
102
+ *
103
+ * @remarks
104
+ * Under `independent` this is one {@link ReleaseTag} per release, in the
105
+ * order given. Under `single` and `fixed-group` it is exactly one shared tag
106
+ * carrying the **first** release's version — every release in a lockstep
107
+ * batch shares a version by construction, so the choice is only visible on a
108
+ * batch that should not exist. Whether a batch actually agreed is a property
109
+ * of that batch rather than of the workspace, so it stays the caller's
110
+ * one-line check rather than a field here.
111
+ *
112
+ * An empty batch produces no tags under either style.
113
+ */
114
+ tagsFor(releases, options) {
115
+ if (releases.length === 0) return [];
116
+ if (this.perPackageTags) return releases.map((release) => ReleaseTag.scoped(release.name, release.version, options));
117
+ return [ReleaseTag.single(releases[0].version, options)];
118
+ }
119
+ };
120
+
121
+ //#endregion
122
+ export { VersioningStrategy, VersioningStrategyType };