rman 2.3.1 → 2.4.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.
@@ -27,9 +27,7 @@ export declare class NodeVersionPlanService extends VersionPlanService {
27
27
  * npm's dependency ranges, read as a release policy. What decides each case is whether a
28
28
  * dependent's **range floor** has to move for a consumer to get a correct install:
29
29
  *
30
- * - **patch** - only the changed packages. A dependent's `^1.2.0` already resolves to `1.2.1`,
31
- * and a patch adds nothing for the dependent to require, so nothing downstream has to ship for
32
- * consumers to receive it.
30
+ * - **patch** - the changed packages **and** every transitive in-group dependent.
33
31
  * - **minor** - also every transitive in-group dependent. A minor adds API; a dependent that uses
34
32
  * it is only correct once its own published range requires the new floor, and a range lives in
35
33
  * a manifest, which only a release puts on the registry.
@@ -28,9 +28,7 @@ export class NodeVersionPlanService extends VersionPlanService {
28
28
  * npm's dependency ranges, read as a release policy. What decides each case is whether a
29
29
  * dependent's **range floor** has to move for a consumer to get a correct install:
30
30
  *
31
- * - **patch** - only the changed packages. A dependent's `^1.2.0` already resolves to `1.2.1`,
32
- * and a patch adds nothing for the dependent to require, so nothing downstream has to ship for
33
- * consumers to receive it.
31
+ * - **patch** - the changed packages **and** every transitive in-group dependent.
34
32
  * - **minor** - also every transitive in-group dependent. A minor adds API; a dependent that uses
35
33
  * it is only correct once its own published range requires the new floor, and a range lives in
36
34
  * a manifest, which only a release puts on the registry.
@@ -41,6 +39,30 @@ export class NodeVersionPlanService extends VersionPlanService {
41
39
  * versions instead has to release every dependent for a patch as well, and one that resolves
42
40
  * dependencies from source may not need a release for any of it.
43
41
  */
42
+ /* **`patch` answered `'changed'` until 2.4, on reasoning that is correct and too narrow.** The
43
+ * range argument holds: `^1.2.0` resolves to `1.2.1`, so a consumer receives the fix with no
44
+ * dependent republished, and the floor a dependent declares stays true. What it leaves out is
45
+ * that a dependent's *published artifact* was built against the old code - anything that bundles,
46
+ * vendors, or type-checks against its dependency ships the pre-fix version until it is released
47
+ * again, and under this default nothing ever released it.
48
+ *
49
+ * So the two answers differ in which failure they prefer, and the cheaper one is now the default:
50
+ * releasing a dependent that strictly did not need it is visible churn, while not releasing one
51
+ * that did is invisible - the same asymmetry `cascade`'s own doc states and `cascadeFor` already
52
+ * resolves by taking the widest.
53
+ *
54
+ * **`patch` and `minor` are now the same answer, and that is a real loss of resolution**, not a
55
+ * tidy-up: the table no longer distinguishes them, so a future reason to treat a patch
56
+ * differently has nowhere to live. It is kept as two entries rather than collapsed for exactly
57
+ * that reason.
58
+ *
59
+ * **It also makes `'changed'` unreachable under npm.** Nothing here returns it any more, and a
60
+ * repository writing `version.cascade: changed` gets no narrowing, because that key is a floor
61
+ * and never a ceiling. The value stays meaningful - it says "no floor of my own", which is what
62
+ * an ecosystem answering `'changed'` would still honour - but in a Node repository it is a no-op.
63
+ *
64
+ * A repository wanting the whole group on a patch writes `version.cascade: group`; one wanting
65
+ * the old narrow behaviour cannot ask for it, and has to say so if that turns out to matter. */
44
66
  cascade(bump) {
45
67
  /** Only semver's three can arrive - `getPlan` validates against `bumpNames` and
46
68
  * `packageJsonManifest` leaves the scheme at the semver default. The fallback is for a scheme
@@ -52,7 +74,7 @@ export class NodeVersionPlanService extends VersionPlanService {
52
74
  }
53
75
  /** semver's bump names against how far each has to reach - see `NodeVersionPlanService.cascade`. */
54
76
  const CASCADE_BY_BUMP = {
55
- patch: 'changed',
77
+ patch: 'dependents',
56
78
  minor: 'dependents',
57
79
  major: 'group',
58
80
  };
@@ -73,6 +73,12 @@ declare const config: {
73
73
  describe: string;
74
74
  type: "string";
75
75
  };
76
+ cascade: {
77
+ target: "config";
78
+ describe: string;
79
+ type: "string";
80
+ choices: readonly ["changed", "dependents", "group"];
81
+ };
76
82
  releaseTagPattern: {
77
83
  target: "config";
78
84
  describe: string;
@@ -266,6 +272,12 @@ declare const versionCommand: (app: import("../index.js").RmanApplication) => {
266
272
  describe: string;
267
273
  type: "string";
268
274
  };
275
+ cascade: {
276
+ target: "config";
277
+ describe: string;
278
+ type: "string";
279
+ choices: readonly ["changed", "dependents", "group"];
280
+ };
269
281
  releaseTagPattern: {
270
282
  target: "config";
271
283
  describe: string;
@@ -94,6 +94,18 @@ const config = {
94
94
  'commit\'s own group shares one version. Default "chore(release): v{version}".',
95
95
  type: 'string',
96
96
  },
97
+ cascade: {
98
+ target: 'config',
99
+ describe: 'The narrowest this repository is willing to release a group: "changed" (only packages with ' +
100
+ 'commits of their own, i.e. no floor of your own), "dependents" (those plus in-group ' +
101
+ 'packages depending on them), or "group" (every member, so the group stays in lockstep). ' +
102
+ 'A floor, not a ceiling: the technology still widens it where a narrower release would ' +
103
+ "leave a dependent's published artifact or range floor behind - under npm that is " +
104
+ '"dependents" for a patch or a minor and the whole group for a major, so "changed" is a ' +
105
+ 'no-op there. Per-package cascaded; a group whose members disagree takes the widest.',
106
+ type: 'string',
107
+ choices: ['changed', 'dependents', 'group'],
108
+ },
97
109
  releaseTagPattern: {
98
110
  target: 'config',
99
111
  describe: "Tag naming the repository's own release, as opposed to the per-package tags " +
package/constants.js CHANGED
@@ -1 +1 @@
1
- export const version = '2.3.1';
1
+ export const version = '2.4.0';
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "rman",
3
3
  "description": "Repository manager",
4
- "version": "2.3.1",
4
+ "version": "2.4.0",
5
5
  "author": "Panates",
6
6
  "license": "MIT",
7
7
  "dependencies": {
@@ -123,6 +123,12 @@ export declare abstract class VersionPlanService {
123
123
  * a dependent that pins exact versions is a broken install.
124
124
  */
125
125
  protected cascadeFor(members: Package[], bump: string): VersionPlanService.Cascade;
126
+ /**
127
+ * `.rmanrc "version.cascade"` for a group, or `undefined` where no member declares one - in which
128
+ * case the technology's own `cascade` answers. Members disagreeing take the widest, the same rule
129
+ * and the same direction `cascadeFor` applies to two technologies.
130
+ */
131
+ protected declaredCascade(members: Package[]): VersionPlanService.Cascade | undefined;
126
132
  /**
127
133
  * The largest bump `commits` ask for, in `scheme`'s own names.
128
134
  *
@@ -157,8 +163,15 @@ export declare abstract class VersionPlanService {
157
163
  * bump (or `undefined` for one with no real commits since its last boundary) - `undefined`
158
164
  * here always means "unchanged", never "explicit version" (that path is handled separately).
159
165
  *
160
- * How wide the bump goes is `cascade`'s answer, and only its answer: an explicit
161
- * `rman version <v>` has no bump left to consult, so it reaches the changed members alone.
166
+ * How wide the bump goes is `cascade`'s answer, and only its answer.
167
+ *
168
+ * **An explicit `rman version <v>` reaches every member of the group, and the old wording here
169
+ * said the opposite.** It read "so it reaches the changed members alone", which is true of the
170
+ * code and false as a sentence: `getPlan` marks *every* eligible package changed with reason
171
+ * `explicit version <v>`, and a group's members are drawn from `eligible`, so "the changed
172
+ * members" is all of them. Measured while adding `version.cascade` - a spec asserting the
173
+ * narrow reading failed. A repository in lockstep therefore keeps it when the number is chosen
174
+ * by hand, with no cascade involved.
162
175
  */
163
176
  protected computeGroupPlan(key: string, members: Package[], changeByPackage: Map<string, VersionPlanService.Change>, explicitVersion: string | undefined, preid: string | undefined, entries: Map<string, VersionPlanService.Entry>): void;
164
177
  /**
@@ -218,12 +218,63 @@ export class VersionPlanService {
218
218
  * a dependent that pins exact versions is a broken install.
219
219
  */
220
220
  cascadeFor(members, bump) {
221
+ /** **A declared cascade is a floor, never a ceiling** - see `declaredCascade`. It joins the
222
+ * technologies' answers and the widest of the set wins, which is the rule already applied to
223
+ * two technologies disagreeing. */
221
224
  const answers = new Set(members.map(m => this.plannerFor(m).cascade(bump)));
225
+ const declared = this.declaredCascade(members);
226
+ if (declared)
227
+ answers.add(declared);
222
228
  for (const widest of CASCADE_WIDEST_FIRST)
223
229
  if (answers.has(widest))
224
230
  return widest;
225
231
  return 'changed';
226
232
  }
233
+ /**
234
+ * `.rmanrc "version.cascade"` for a group, or `undefined` where no member declares one - in which
235
+ * case the technology's own `cascade` answers. Members disagreeing take the widest, the same rule
236
+ * and the same direction `cascadeFor` applies to two technologies.
237
+ */
238
+ /* **Why a repository gets to say this at all.** `cascade` is abstract because two of its inputs
239
+ * are an ecosystem's: whether a caret range already carries a patch to a dependent, and whether
240
+ * a floor left behind is a broken install. Those npm answers are right. What they cannot answer
241
+ * is whether a repository wants **one number across its whole product** - which is a release
242
+ * identity decision, not a fact about a package manager, and there was nowhere to state it.
243
+ *
244
+ * Measured on `panates/sqb`: 17 packages, `group: true`, every one published at 6.0.10 since it
245
+ * moved off rman 1.x's `version.unified: true` - and a `fix:` in two of them planned
246
+ * `6.0.10 -> 6.0.11` for those two and `no-change` for the other fifteen. Correct on ranges (all
247
+ * 39 internal edges are caret peers, so the fix reaches dependents with no republish) and still
248
+ * the wrong answer for that repository: the divergence is permanent, since a group's next
249
+ * baseline is `highestVersion(members)`, and every README and issue there is written in terms of
250
+ * one number.
251
+ *
252
+ * **The default is unchanged**, so this costs nothing to a repository that never writes the key.
253
+ *
254
+ * **It is not a safety valve, and must not be documented as one.** A change that can break a
255
+ * dependent is a `feat:` or a `feat!:`; republishing the dependent under a new number does not
256
+ * make a behavioural break safe, it only renumbers it. The lever for that is the bump size, which
257
+ * comes from the commit message.
258
+ *
259
+ * **A floor, never a ceiling, and that asymmetry is the whole safety of the key.** `cascadeFor`
260
+ * adds this answer to the technologies' and takes the widest, so a repository can ask for a
261
+ * *wider* release than npm requires and cannot ask for a narrower one. The direction matters
262
+ * because the two mistakes are not symmetric, which this class's own doc already records: too
263
+ * wide republishes a package that did not need it - visible and harmless - while too narrow
264
+ * leaves a dependent's published range floor wrong, which is a broken install. Concretely, a
265
+ * repository writing `cascade: changed` still gets the whole group on a **major**, because
266
+ * `^1.0.0` does not admit `2.0.0` and an in-group dependent left at the old number would declare
267
+ * a range its own group no longer satisfies. So `changed` means "no floor of my own", not "never
268
+ * more than the changed packages". */
269
+ declaredCascade(members) {
270
+ const declared = new Set(members.map(m => m.config?.version?.cascade).filter((c) => !!c));
271
+ if (!declared.size)
272
+ return undefined;
273
+ for (const widest of CASCADE_WIDEST_FIRST)
274
+ if (declared.has(widest))
275
+ return widest;
276
+ return undefined;
277
+ }
227
278
  /**
228
279
  * The largest bump `commits` ask for, in `scheme`'s own names.
229
280
  *
@@ -284,8 +335,15 @@ export class VersionPlanService {
284
335
  * bump (or `undefined` for one with no real commits since its last boundary) - `undefined`
285
336
  * here always means "unchanged", never "explicit version" (that path is handled separately).
286
337
  *
287
- * How wide the bump goes is `cascade`'s answer, and only its answer: an explicit
288
- * `rman version <v>` has no bump left to consult, so it reaches the changed members alone.
338
+ * How wide the bump goes is `cascade`'s answer, and only its answer.
339
+ *
340
+ * **An explicit `rman version <v>` reaches every member of the group, and the old wording here
341
+ * said the opposite.** It read "so it reaches the changed members alone", which is true of the
342
+ * code and false as a sentence: `getPlan` marks *every* eligible package changed with reason
343
+ * `explicit version <v>`, and a group's members are drawn from `eligible`, so "the changed
344
+ * members" is all of them. Measured while adding `version.cascade` - a spec asserting the
345
+ * narrow reading failed. A repository in lockstep therefore keeps it when the number is chosen
346
+ * by hand, with no cascade involved.
289
347
  */
290
348
  computeGroupPlan(key, members, changeByPackage, explicitVersion, preid, entries) {
291
349
  const label = this.groupLabel(key);
@@ -313,6 +371,12 @@ export class VersionPlanService {
313
371
  to = scheme.next(current, bump, { preid });
314
372
  }
315
373
  const bumping = new Set(changed);
374
+ /** **`'changed'` with no bump is not a narrowing, because there is nothing left to narrow.** An
375
+ * explicit `rman version <v>` puts *every* eligible package into `changeByPackage` (see
376
+ * `getPlan`), and a group's members are drawn from `eligible` alone - so `changed` already
377
+ * holds all of them and no cascade could add one. Measured while wiring `version.cascade`: a
378
+ * declared cascade consulted here is dead code, and the spec written for it asserted a reason
379
+ * string that never renders. */
316
380
  const cascade = bump ? this.cascadeFor(members, bump) : 'changed';
317
381
  if (cascade === 'group') {
318
382
  for (const m of members)
@@ -334,6 +398,11 @@ export class VersionPlanService {
334
398
  }
335
399
  /** Why a member with no commits of its own is being bumped. `'group'` reaches members that
336
400
  * depend on nothing at all, so calling those a "dependent" was simply untrue. */
401
+ /* `bump` is always set where this string is *read*: a member reaches it only through
402
+ * `own?.reason ?? inherited`, and the one path with no bump - an explicit version - gives every
403
+ * member an `own.reason` of its own (`explicit version <v>`). So there is no
404
+ * `a undefined change` to guard against here. Checked rather than assumed, because the
405
+ * template reads as though there were. */
337
406
  const inherited = cascade === 'group' ? `in-group member of a ${bump} change` : `in-group dependent of a ${bump} change`;
338
407
  for (const m of members) {
339
408
  if (bumping.has(m)) {