@williamthorsen/release-kit 12.0.0 → 13.0.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.
Files changed (168) hide show
  1. package/.meta/changelog.json +212 -0
  2. package/.readyup/kits/default.js +18 -17
  3. package/.readyup/kits/npm-auto-publish.js +24 -23
  4. package/.readyup/manifest.json +16 -11
  5. package/CHANGELOG.md +58 -0
  6. package/README.md +55 -48
  7. package/bin/release-kit.js +1 -1
  8. package/dist/esm/assertCleanWorkingTree.d.ts +1 -5
  9. package/dist/esm/assertTaggedBaseline.d.ts +29 -0
  10. package/dist/esm/assertTaggedBaseline.js +41 -0
  11. package/dist/esm/attachChangelogDiagnostics.d.ts +1 -1
  12. package/dist/esm/attachChangelogDiagnostics.js +3 -0
  13. package/dist/esm/bin/release-kit.js +85 -22
  14. package/dist/esm/buildChangelogEntries.d.ts +23 -13
  15. package/dist/esm/buildChangelogEntries.js +36 -17
  16. package/dist/esm/buildDependencyGraph.d.ts +6 -6
  17. package/dist/esm/buildEmptyReleaseEntry.d.ts +1 -8
  18. package/dist/esm/buildReleaseSummary.d.ts +2 -8
  19. package/dist/esm/buildSyntheticChangelogEntry.d.ts +3 -5
  20. package/dist/esm/buildSyntheticChangelogEntry.js +2 -2
  21. package/dist/esm/bumpVersion.d.ts +1 -8
  22. package/dist/esm/changelogJsonFile.d.ts +5 -7
  23. package/dist/esm/changelogJsonFile.js +2 -17
  24. package/dist/esm/changelogJsonUtils.d.ts +7 -10
  25. package/dist/esm/changelogOverrides.d.ts +59 -98
  26. package/dist/esm/classifyChangelogCommit.d.ts +7 -9
  27. package/dist/esm/classifyChangelogCommit.js +3 -2
  28. package/dist/esm/commitCommand.d.ts +2 -4
  29. package/dist/esm/compareVersions.d.ts +10 -4
  30. package/dist/esm/compareVersions.js +16 -0
  31. package/dist/esm/configFlagSchema.d.ts +1 -1
  32. package/dist/esm/createGithubRelease.d.ts +7 -19
  33. package/dist/esm/createGithubReleaseCommand.d.ts +5 -5
  34. package/dist/esm/createGithubReleaseCommand.js +9 -9
  35. package/dist/esm/createTags.d.ts +2 -3
  36. package/dist/esm/decideRelease.d.ts +2 -6
  37. package/dist/esm/defaults.d.ts +5 -24
  38. package/dist/esm/defaults.js +4 -5
  39. package/dist/esm/defineConfig.d.ts +1 -1
  40. package/dist/esm/deleteFileIfExists.d.ts +1 -1
  41. package/dist/esm/deriveReleaseNotesConfig.d.ts +20 -0
  42. package/dist/esm/deriveReleaseNotesConfig.js +21 -0
  43. package/dist/esm/deriveWorkspaceConfig.d.ts +3 -5
  44. package/dist/esm/detectPackageManager.d.ts +2 -4
  45. package/dist/esm/detectUndeclaredTagPrefixes.d.ts +4 -6
  46. package/dist/esm/detectUndeclaredTagPrefixes.js +1 -1
  47. package/dist/esm/determineBumpType.d.ts +1 -1
  48. package/dist/esm/discoverWorkspaces.d.ts +14 -15
  49. package/dist/esm/discoverWorkspaces.js +12 -12
  50. package/dist/esm/enterRepoRoot.d.ts +12 -0
  51. package/dist/esm/enterRepoRoot.js +16 -0
  52. package/dist/esm/enumerateReleaseWindows.d.ts +8 -8
  53. package/dist/esm/extractMigration.d.ts +6 -8
  54. package/dist/esm/findUnroutedEntryScopes.d.ts +16 -0
  55. package/dist/esm/findUnroutedEntryScopes.js +57 -0
  56. package/dist/esm/formatExcludedSkip.d.ts +3 -0
  57. package/dist/esm/formatExcludedSkip.js +3 -0
  58. package/dist/esm/formatPrivateSkip.d.ts +1 -6
  59. package/dist/esm/generateChangelogs.d.ts +10 -5
  60. package/dist/esm/glyphs.d.ts +1 -1
  61. package/dist/esm/help/prepareHelp.d.ts +3 -4
  62. package/dist/esm/help/prepareHelp.js +4 -3
  63. package/dist/esm/init/checks.d.ts +3 -3
  64. package/dist/esm/init/checks.js +4 -1
  65. package/dist/esm/init/detectRepoType.d.ts +2 -3
  66. package/dist/esm/init/hasProvenance.d.ts +2 -0
  67. package/dist/esm/init/hasProvenance.js +3 -0
  68. package/dist/esm/init/initCommand.d.ts +3 -6
  69. package/dist/esm/init/initCommand.js +10 -10
  70. package/dist/esm/init/parseJsonRecord.d.ts +1 -4
  71. package/dist/esm/init/scaffold.d.ts +1 -1
  72. package/dist/esm/init/templates.d.ts +9 -11
  73. package/dist/esm/init/templates.js +12 -11
  74. package/dist/esm/init/updateTemplatesCommand.d.ts +13 -0
  75. package/dist/esm/init/updateTemplatesCommand.js +64 -0
  76. package/dist/esm/injectReleaseNotesIntoReadme.d.ts +12 -17
  77. package/dist/esm/injectSection.d.ts +3 -3
  78. package/dist/esm/loadConfig.d.ts +16 -14
  79. package/dist/esm/loadConfig.js +2 -2
  80. package/dist/esm/loadValidatedConfig.d.ts +16 -19
  81. package/dist/esm/loadValidatedConfig.js +6 -1
  82. package/dist/esm/packsPath.d.ts +9 -9
  83. package/dist/esm/parseChangeRecordBlock.d.ts +2 -2
  84. package/dist/esm/parseCommitMessage.d.ts +17 -32
  85. package/dist/esm/parseCommitMessage.js +28 -37
  86. package/dist/esm/parseRequestedTags.d.ts +3 -3
  87. package/dist/esm/planReleaseNotesPreviews.d.ts +6 -6
  88. package/dist/esm/planVersionBump.d.ts +5 -3
  89. package/dist/esm/planVersionBump.js +3 -0
  90. package/dist/esm/prepareCommand.d.ts +4 -7
  91. package/dist/esm/prepareCommand.js +13 -6
  92. package/dist/esm/previewTagPrefixes.d.ts +8 -10
  93. package/dist/esm/propagateBumps.d.ts +6 -7
  94. package/dist/esm/publish.d.ts +1 -1
  95. package/dist/esm/publishCommand.d.ts +4 -3
  96. package/dist/esm/publishCommand.js +8 -8
  97. package/dist/esm/pushCommand.d.ts +4 -3
  98. package/dist/esm/pushCommand.js +8 -2
  99. package/dist/esm/pushRelease.d.ts +2 -2
  100. package/dist/esm/readChangelogSections.d.ts +33 -0
  101. package/dist/esm/readChangelogSections.js +80 -0
  102. package/dist/esm/releaseFiles.d.ts +5 -7
  103. package/dist/esm/releasePlan.d.ts +11 -13
  104. package/dist/esm/releasePrepare.d.ts +23 -22
  105. package/dist/esm/releasePrepare.js +31 -11
  106. package/dist/esm/releasePrepareMono.d.ts +9 -7
  107. package/dist/esm/releasePrepareMono.js +85 -33
  108. package/dist/esm/releasePrepareProject.d.ts +19 -29
  109. package/dist/esm/releasePrepareProject.js +31 -12
  110. package/dist/esm/renderChangelogMarkdown.d.ts +10 -4
  111. package/dist/esm/renderChangelogMarkdown.js +27 -2
  112. package/dist/esm/renderReleaseNotes.d.ts +3 -11
  113. package/dist/esm/renderReleaseNotes.js +2 -6
  114. package/dist/esm/reportPrepare.d.ts +4 -5
  115. package/dist/esm/reportPrepare.js +33 -19
  116. package/dist/esm/resolveCommandTags.d.ts +15 -11
  117. package/dist/esm/resolveCommandTags.js +35 -5
  118. package/dist/esm/resolveConfigFlag.d.ts +7 -0
  119. package/dist/esm/resolveConfigFlag.js +4 -0
  120. package/dist/esm/resolveReleaseTags.d.ts +6 -8
  121. package/dist/esm/resolveReleaseTags.js +1 -1
  122. package/dist/esm/resolveStylesOrExit.d.ts +2 -2
  123. package/dist/esm/showTagPrefixesCommand.d.ts +7 -10
  124. package/dist/esm/showTagPrefixesCommand.js +4 -4
  125. package/dist/esm/stripEmojiPrefix.d.ts +1 -0
  126. package/dist/esm/stripScope.d.ts +2 -2
  127. package/dist/esm/stripScope.js +3 -3
  128. package/dist/esm/sync-labels/generateCommand.d.ts +11 -10
  129. package/dist/esm/sync-labels/generateCommand.js +11 -8
  130. package/dist/esm/sync-labels/initCommand.d.ts +6 -7
  131. package/dist/esm/sync-labels/initCommand.js +2 -2
  132. package/dist/esm/sync-labels/presets.d.ts +5 -4
  133. package/dist/esm/sync-labels/presets.js +9 -9
  134. package/dist/esm/sync-labels/resolveLabels.d.ts +7 -7
  135. package/dist/esm/sync-labels/resolveLabels.js +1 -1
  136. package/dist/esm/sync-labels/retiredConfig.d.ts +4 -5
  137. package/dist/esm/sync-labels/syncCommand.d.ts +1 -4
  138. package/dist/esm/sync-labels/templates.d.ts +3 -3
  139. package/dist/esm/sync-labels/templates.js +3 -3
  140. package/dist/esm/sync-labels/typeLabels.d.ts +14 -0
  141. package/dist/esm/sync-labels/typeLabels.js +38 -0
  142. package/dist/esm/sync-labels/types.d.ts +1 -1
  143. package/dist/esm/tagCommand.d.ts +1 -1
  144. package/dist/esm/typeGuards.d.ts +2 -2
  145. package/dist/esm/types.d.ts +116 -91
  146. package/dist/esm/types.js +1 -1
  147. package/dist/esm/validateConfig.d.ts +2 -11
  148. package/dist/esm/validateConfig.js +2 -2
  149. package/dist/esm/validateOnlyExcludesStrandedDependents.d.ts +14 -12
  150. package/dist/esm/validateOverridesCommand.d.ts +17 -20
  151. package/dist/esm/validateOverridesCommand.js +1 -1
  152. package/docs/api.md +6 -6
  153. package/docs/changelogs.md +63 -40
  154. package/docs/configuration.md +29 -27
  155. package/docs/editorial-overrides.md +23 -23
  156. package/docs/labels.md +21 -19
  157. package/docs/project-releases.md +13 -13
  158. package/docs/publishing.md +5 -5
  159. package/docs/readiness-checks.md +3 -3
  160. package/docs/releasing.md +50 -25
  161. package/docs/work-types.md +41 -57
  162. package/package.json +7 -6
  163. package/presets/labels/common.yaml +1 -41
  164. package/dist/esm/resolveReleaseNotesConfig.d.ts +0 -26
  165. package/dist/esm/resolveReleaseNotesConfig.js +0 -29
  166. package/dist/esm/workTypesData.d.ts +0 -50
  167. package/dist/esm/workTypesData.js +0 -145
  168. package/schemas/label-map.json +0 -24
package/docs/api.md CHANGED
@@ -4,7 +4,7 @@ How to build a release config by hand with `deriveWorkspaceConfig()`, the script
4
4
 
5
5
  ## Using `deriveWorkspaceConfig()` for manual configuration
6
6
 
7
- If you need to build a `MonorepoReleaseConfig` manually (e.g., for the legacy script-based approach), the exported `deriveWorkspaceConfig()` helper creates a `WorkspaceConfig` from a workspace-relative path. It reads the workspace's `package.json` to derive the tag prefix from the package name:
7
+ For a `MonorepoReleaseConfig` built manually (e.g., for the legacy script-based approach), the exported `deriveWorkspaceConfig()` helper creates a `WorkspaceConfig` from a workspace-relative path. It reads the workspace's `package.json` to derive the tag prefix from the package name:
8
8
 
9
9
  ```typescript
10
10
  import { deriveWorkspaceConfig } from '@williamthorsen/release-kit';
@@ -22,9 +22,9 @@ deriveWorkspaceConfig('packages/arrays');
22
22
  // }
23
23
  ```
24
24
 
25
- `dir` is the basename of the workspace path and is the stable internal identifier used by `--only`, `WorkspaceOverride.dir`, and the dependency graph. `tagPrefix` is derived from the unscoped `package.json` `name` — any leading `@scope/` is stripped — so tags reflect the package identity rather than the directory layout. For example, a workspace at `packages/core` with `"name": "@williamthorsen/nmr-core"` produces `tagPrefix: 'nmr-core-v'`, yielding tags like `nmr-core-v1.3.0`.
25
+ `dir` is the basename of the workspace path and is the stable internal identifier used by `--only`, `WorkspaceOverride.dir`, and the dependency graph. `tagPrefix` is derived from the unscoped `package.json` `name` (with any leading `@scope/` stripped), so tags reflect the package identity rather than the directory layout. For example, a workspace at `packages/core` with `"name": "@williamthorsen/nmr-core"` produces `tagPrefix: 'nmr-core-v'`, yielding tags like `nmr-core-v1.3.0`.
26
26
 
27
- The workspace's `package.json` must declare a non-empty `name` field; `deriveWorkspaceConfig()` throws otherwise. If two workspaces produce the same `tagPrefix` (because their unscoped names collide), `mergeMonorepoConfig()` throws and names the colliding workspaces so you can rename one.
27
+ The workspace's `package.json` must declare a non-empty `name` field; `deriveWorkspaceConfig()` throws otherwise. If two workspaces produce the same `tagPrefix` (because their unscoped names collide), `mergeMonorepoConfig()` throws and names the colliding workspaces so that the developer can rename one.
28
28
 
29
29
  ## Legacy script-based approach
30
30
 
@@ -49,7 +49,7 @@ import { config } from './release.config.ts';
49
49
  runReleasePrepare(config);
50
50
  ```
51
51
 
52
- The key difference: the script-based approach requires manually listing every workspace, while the CLI auto-discovers them from `pnpm-workspace.yaml`.
52
+ The key difference: The script-based approach requires manually listing every workspace, while the CLI auto-discovers them from `pnpm-workspace.yaml`.
53
53
 
54
54
  ## `resolveReleaseTags` takes workspaces; `WorkspaceConfig` requires `workspacePath`
55
55
 
@@ -58,7 +58,7 @@ Tag resolution is now driven by workspace records rather than a caller-supplied
58
58
  - `resolveReleaseTags` signature changed from `(workspaceMap?: Map<string, string>)` to `(workspaces?: readonly WorkspaceConfig[])`.
59
59
  - `WorkspaceConfig` gained a required `workspacePath: string` field.
60
60
 
61
- Replace direct `Map`-based calls with `deriveWorkspaceConfig()`, which now populates `workspacePath` for you:
61
+ Replace direct `Map`-based calls with `deriveWorkspaceConfig()`, which now populates `workspacePath`:
62
62
 
63
63
  ```diff
64
64
  -import { resolveReleaseTags } from '@williamthorsen/release-kit';
@@ -70,4 +70,4 @@ Replace direct `Map`-based calls with `deriveWorkspaceConfig()`, which now popul
70
70
  +resolveReleaseTags([deriveWorkspaceConfig('packages/core')]);
71
71
  ```
72
72
 
73
- If you construct `WorkspaceConfig` objects directly, add `workspacePath` alongside the other required fields.
73
+ When constructing `WorkspaceConfig` objects directly, add `workspacePath` alongside the other required fields.
@@ -1,27 +1,27 @@
1
1
  # Changelogs
2
2
 
3
- What reaches a published tarball, what each `.meta/changelog.json` item contains, which commits reach a changelog, how a merge commit's change-record block becomes items, and how release notes reach a README.
3
+ What a published tarball includes, what each `.meta/changelog.json` item contains, which commits appear in a changelog, how a merge commit's change-record block becomes items, and how release notes are written into a README.
4
4
 
5
- Neither `CHANGELOG.md` nor `.meta/changelog.json` reaches a published tarball on its own. npm stopped including `CHANGELOG` automatically in npm 7, so a package that declares a `files` field ships a changelog only where that field names `CHANGELOG.md` and `.meta/changelog.json`. release-kit's readyup kit reports a publishable workspace whose `files` field omits either.
5
+ npm doesn't include either `CHANGELOG.md` or `.meta/changelog.json` in a published tarball on its own. npm stopped including `CHANGELOG` automatically in npm 7, so a package that declares a `files` field publishes a changelog only when that field names `CHANGELOG.md` and `.meta/changelog.json`. release-kit's readyup kit reports a publishable workspace whose `files` field omits either.
6
6
 
7
7
  ## `changelog.json` item schema
8
8
 
9
- Each item under a section in `.meta/changelog.json` carries one required field and five optional ones:
9
+ Each item under a section in `.meta/changelog.json` has one required field and five optional ones:
10
10
 
11
- | Field | Type | Meaning |
12
- | ------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
13
- | `description` | `string` | The bullet headline, taken from the commit subject with the ticket ID and type prefix stripped, or from a change-record entry's `text` (see [Change-record blocks](#change-record-blocks)). |
14
- | `body` | `string` | The commit body, with trailing trailer metadata and any `change-record` block stripped. Absent on an item derived from a change-record entry. |
15
- | `breaking` | `boolean` | Present and `true` where the commit subject carried the `!` prefix, or the change-record entry set `breaking: true`, and the type permits it. See [`!` (breaking change) policy](work-types.md#-breaking-change-policy). |
16
- | `migration` | `string` | The migration step for a consumer. See below. |
17
- | `hash` | `string` | The full commit SHA, which an override key matches by prefix. Absent on synthetic propagation entries. |
18
- | `entry` | `number` | The 1-based position of the change-record entry from which the item derives, which an override key `<hash>:<n>` matches. Absent on an item derived from the commit's title. |
11
+ | Field | Type | Meaning |
12
+ | ------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
13
+ | `description` | `string` | The bullet headline, taken from the commit subject with the ticket ID and type prefix stripped, or from a change-record entry's `text` (see [Change-record blocks](#change-record-blocks)). |
14
+ | `body` | `string` | The commit body, with trailing trailer metadata and any `change-record` block stripped. Absent on an item derived from a change-record entry. |
15
+ | `breaking` | `boolean` | Present and `true` when the commit subject had the `!` prefix, or the change-record entry set `breaking: true`, and the type permits it. See [`!` (breaking change) policy](work-types.md#-breaking-change-policy). |
16
+ | `migration` | `string` | The migration step for a consumer. See below. |
17
+ | `hash` | `string` | The full commit SHA, which an override key matches by prefix. Absent on synthetic propagation entries. |
18
+ | `entry` | `number` | The 1-based position of the change-record entry from which the item derives, which an override key `<hash>:<n>` matches. Absent on an item derived from the commit's title. |
19
19
 
20
20
  Every optional field is omitted rather than emitted as `null`, so a consumer tests for presence.
21
21
 
22
22
  ### The `migration` field
23
23
 
24
- A commit body states a migration step as a paragraph opening with the literal label `Migration:`. Where one is present, `migration` carries that paragraph with the label stripped. An item derived from a change-record entry takes `migration` from the entry instead, and has no `body`.
24
+ A commit body states a migration step as a paragraph opening with the literal label `Migration:`. When one is present, `migration` contains that paragraph with the label stripped. An item derived from a change-record entry takes `migration` from the entry instead, and doesn't have a `body`.
25
25
 
26
26
  A title-derived item looks like this:
27
27
 
@@ -35,68 +35,91 @@ A title-derived item looks like this:
35
35
 
36
36
  Three properties are worth knowing:
37
37
 
38
- - **It is independent of `breaking`.** A `deprecate` cannot carry `!` under the default breaking policy, and a `deprecate` still calls for a migration. Filter on `migration` to find every step; filter on `breaking` to find every breaking change.
38
+ - **It is independent of `breaking`.** A `deprecate` cannot have `!` under the default breaking policy, and a `deprecate` still calls for a migration. Filter on `migration` to find every step; filter on `breaking` to find every breaking change.
39
39
  - **`body` keeps the paragraph.** The field is an extraction, not a move, so `CHANGELOG.md` and `.meta/changelog.json` go on agreeing.
40
- - **An override cannot set it.** `migration` is not a field an override file can set (see [File shape](editorial-overrides.md#file-shape)). On a title-derived item it is re-derived from whatever `body` an override installs, and cleared where that body carries no labeled paragraph, so overriding `body` changes the migration text. An entry-derived item keeps its entry's `migration` whatever `body` an override installs.
40
+ - **An override cannot set it.** `migration` is not a field that an override file can set (see [File shape](editorial-overrides.md#file-shape)). On a title-derived item, overriding `body` changes the migration text: release-kit re-derives `migration` from whatever `body` an override installs, and clears it when that body doesn't contain a labeled paragraph. An entry-derived item keeps its entry's `migration` whatever `body` an override installs.
41
41
 
42
42
  The label match is exact: `migration:` and `**Migration:**` are not recognized.
43
43
 
44
- ## What reaches a changelog
44
+ ## Which commits appear in a changelog
45
45
 
46
46
  release-kit splits the history reachable from `HEAD` into one window per release tag that matches the scope's tag prefixes, restricted to commits that touch the scope's `paths`. The bump reads the same items: Its level is the highest that the unreleased window's items call for, so a commit that bumps is one that the changelog lists.
47
47
 
48
- A commit whose last `change-record` block records entries yields its items from those entries, as [Change-record blocks](#change-record-blocks) describes. Any other commit in a window reaches the changelog when its subject passes three checks:
48
+ A commit whose last `change-record` block records entries yields its items from those entries, as [Change-record blocks](#change-record-blocks) describes. Any other commit in a window appears in the changelog when its subject passes three checks:
49
49
 
50
50
  - **A ticket-ID prefix**, such as `#42 `, `TOOL-123 `, or `## `.
51
51
  - **A declared work type**, resolved against the merged [work types](work-types.md).
52
52
  - **A type not excluded from the changelog** by `excludedFromChangelog: true`.
53
53
 
54
- `release:` commits and merge commits whose subject git wrote (`Merge …`) never reach a changelog, whether or not they carry a block. A commit that fails a check yields no item and raises no bump, and a release left with no surviving commit gets no changelog entry. The prepare report lists a commit of the unreleased window that fails the first or the second check as unparseable, unless a malformed-block warning already reports it; it lists none that fails the third. The commit's type decides the section under which its item appears.
54
+ `release:` commits and merge commits whose subject git wrote (`Merge …`) never appear in a changelog, whether or not they contain a block. A commit that fails a check doesn't yield an item or raise a bump, so a window without an item doesn't call for a release. A release forced by `--force` or `--set-version` whose window doesn't yield an item records a "Forced version bump." entry under Notes for its version, whether or not the window has commits. Because history does not yield that entry again, `release-kit prepare` keeps it by merging each release's entries with the `changelog.json` on disk. The prepare report lists a commit of the unreleased window that fails the first or the second check as unparseable, unless a malformed-block warning already reports it; it lists none that fails the third. The commit's type decides the section under which its item appears.
55
55
 
56
- Every item of a commit reaches every workspace whose window contains the commit, because windows are selected by the paths that the commit touches.
56
+ ### Existing `CHANGELOG.md` sections
57
+
58
+ `prepare` also reads the `CHANGELOG.md` that it regenerates, so a version recorded only there, such as history written by hand before release-kit was adopted, survives. The first SemVer token in a `##` heading names the section's version: `## 1.2.0`, `## v1.2.0`, and `## [1.2.0] - 2024-01-01` all name 1.2.0, and so does `## Upgrading from 1.2.0`.
59
+
60
+ - **A version that neither the release windows nor `changelog.json` contain** keeps its section verbatim, placed among the rendered sections in version order.
61
+ - **A version that either contains** is rendered from its entries, and its old text is discarded.
62
+ - **A section whose heading doesn't name a version**, such as `## Unreleased`, is dropped.
63
+
64
+ The header above the first section and the footer comment are regenerated. The prepare report lists the versions kept for each changelog and warns about each section dropped. Kept sections never enter `changelog.json`.
65
+
66
+ ### Routing to workspaces
67
+
68
+ A workspace's window contains every commit that touches the workspace's paths. An item derived from a title is routed to every workspace whose window contains its commit. An item derived from a change-record entry is routed to a workspace in that window only when the entry's `scopes` route it there:
69
+
70
+ - **A scope that names the workspace's `dir`** routes the item to that workspace. release-kit resolves each scope through [`scopeAliases`](configuration.md) before it compares the scope with the `dir`.
71
+ - **`*`, or an empty or absent `scopes`,** routes the item to every workspace in the window.
72
+ - **`root`** doesn't name a workspace, so it routes the item nowhere and is not reported. A workspace whose `dir` is `root` is the exception: `root` then names that workspace.
73
+
74
+ A scope narrows the window and never widens it: A scope that names a workspace whose window does not contain the commit doesn't route the item to any workspace, and `prepare` reports it (see [Reported diagnostics](#reported-diagnostics)). The project changelog and a single-package repo take every entry, whatever its scopes.
75
+
76
+ A workspace's bump reads the items routed to it, so a breaking entry scoped to one workspace does not raise a major bump in another that the same commit touched. A workspace whose window has commits but lacks a routed item doesn't call for a release.
77
+
78
+ Each `prepare` rebuilds a workspace's released windows with the same routing. When an earlier release recorded an item under a workspace that the item's scopes do not name, the rebuild removes the item from that workspace's past section. A path window does not follow a renamed directory, and legacy identities do not rename scopes: An entry scoped to a workspace's old directory name is routed to the workspace only through a `scopeAliases` entry from the old name to the new one, and until one exists, `prepare` reports the scope for each such commit in the unreleased window.
57
79
 
58
80
  ## Change-record blocks
59
81
 
60
82
  A squash-merge commit composed from a pull request can end with a fenced block whose info string is exactly `change-record`. Its payload is YAML, with the grammar that codeassembly's `change-record.md` specifies. release-kit reads these keys and ignores any other:
61
83
 
62
- | Key | Type | Meaning |
63
- | --------------------- | -------- | ------------------------------------------------------------------------------- |
64
- | `entries` | list | The change entries, one per outcome of the change, in the order that they read. |
65
- | `entries[].type` | `string` | A work type, resolved through the declared types and their aliases. Required. |
66
- | `entries[].text` | `string` | The sentence that reports the outcome. Required. |
67
- | `entries[].breaking` | boolean | Whether the outcome breaks consumers. |
68
- | `entries[].scopes` | list | The scopes that the outcome touched, each a string. Validated but not yet used. |
69
- | `entries[].migration` | `string` | The migration step for a consumer. |
70
- | `pr_number` | integer | The merged pull request's number, a positive integer. |
71
- | `ticket_ref` | `string` | The ticket that the change serves. Validated but not yet used. |
84
+ | Key | Type | Meaning |
85
+ | --------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
86
+ | `entries` | list | The change entries, one per outcome of the change, in the order that they read. |
87
+ | `entries[].type` | `string` | A work type, resolved through the declared types and their aliases. Required. |
88
+ | `entries[].text` | `string` | The sentence that reports the outcome. Required. |
89
+ | `entries[].breaking` | boolean | Whether the outcome breaks consumers. |
90
+ | `entries[].scopes` | list | The scopes that the outcome touched, each a string. They decide which workspaces receive the entry's item; see [Routing to workspaces](#routing-to-workspaces). |
91
+ | `entries[].migration` | `string` | The migration step for a consumer. |
92
+ | `pr_number` | integer | The merged pull request's number, a positive integer. |
93
+ | `ticket_ref` | `string` | The ticket that the change serves. Validated but not yet used. |
72
94
 
73
95
  release-kit reads the **last** `change-record` fence in the message. A key whose value is null reads as absent.
74
96
 
75
97
  ### Items from entries
76
98
 
77
- Each entry yields one item, whether or not the commit subject carries a ticket-ID prefix or a declared type:
99
+ Each entry routed to the workspace being read yields one item, whether or not the commit subject has a ticket-ID prefix or a declared type:
78
100
 
79
- - **Section**: the header of the entry's `type`, so the section order and the audience follow as for a title.
80
- - **`description`**: the entry's `text` as written, followed by ` (#N)` when the block records `pr_number`.
81
- - **`breaking`**: set when the entry's `breaking` is `true` and the type's breaking policy permits it.
82
- - **`migration`**: the entry's `migration`, when present. There is no `body`.
83
- - **`hash`** and **`entry`**: the commit's hash, and the entry's 1-based position among all the block's entries, including those that yield no item.
101
+ - **Section**: The header of the entry's `type`, so the section order and the audience follow as for a title.
102
+ - **`description`**: The entry's `text` as written, followed by ` (#N)` when the block records `pr_number`.
103
+ - **`breaking`**: Set when the entry's `breaking` is `true` and the type's breaking policy permits it.
104
+ - **`migration`**: The entry's `migration`, when present. The item doesn't have a `body`.
105
+ - **`hash`** and **`entry`**: The commit's hash, and the entry's 1-based position among all the block's entries, including those that don't yield an item.
84
106
 
85
- An entry whose type is excluded from the changelog yields no item. An override targets one of these items by the key `<hash>:<n>`, where `n` is the item's `entry`. A bare `<hash>` key matches every item of the commit, and it fails when it sets `description` or `body` on a commit with several items; see [Keys](editorial-overrides.md#keys).
107
+ An entry whose type is excluded from the changelog doesn't yield an item. An override targets one of these items by the key `<hash>:<n>`, where `n` is the item's `entry`. A bare `<hash>` key matches every item of the commit, and it fails when it sets `description` or `body` on a commit with several items; see [Keys](editorial-overrides.md#keys).
86
108
 
87
109
  ### Fallback to the title
88
110
 
89
- A commit is read from its title, as [What reaches a changelog](#what-reaches-a-changelog) describes, when it has no block, when the block's `entries` is absent or empty, or when the block is malformed. A block is malformed when it never closes, when its payload is not valid YAML or not a mapping, when `entries` is not a list, when an entry is not a mapping, when an entry's `type` or `text` is missing, blank, or not a string, when `breaking` is not a boolean, when `scopes` is not a list of strings, when `migration` or `ticket_ref` is not a string, or when `pr_number` is not a positive integer. One defect makes the whole block malformed.
111
+ A commit is read from its title, as [Which commits appear in a changelog](#which-commits-appear-in-a-changelog) describes, when it doesn't have a block, when the block's `entries` is absent or empty, or when the block is malformed. A block is malformed when it never closes, when its payload is not valid YAML or not a mapping, when `entries` is not a list, when an entry is not a mapping, when an entry's `type` or `text` is missing, blank, or not a string, when `breaking` is not a boolean, when `scopes` is not a list of strings, when `migration` or `ticket_ref` is not a string, or when `pr_number` is not a positive integer. One defect makes the whole block malformed.
90
112
 
91
113
  ### Reported diagnostics
92
114
 
93
- `release-kit prepare` reports these as warnings for the release being prepared, from the commits that no earlier release contains:
115
+ `release-kit prepare` reports these as warnings for the release being prepared, from the commits that aren't in any earlier release:
94
116
 
95
117
  - **A malformed block**, with the defect that stopped the read.
96
- - **An entry whose type is not declared**, with its position. It yields no item.
118
+ - **An entry whose type is not declared**, with its position. It doesn't yield an item.
97
119
  - **An entry that violates its type's breaking policy**, as a policy violation at the entry's position. Its item is not marked breaking.
120
+ - **An entry scope that doesn't name a workspace in its commit's window**, with the commit's hash, the entry's position, and the scope as the entry declares it. release-kit reports it under every workspace whose window contains the commit. A scope that names nothing, or a workspace whose window does not contain the commit, is reported; `*`, `root`, and a scope that names a workspace in the window are not. Under `--only`, a scope that names a configured workspace left out of the run is not reported either. A workspace excluded with `shouldExclude: true` is not a configured workspace, so a scope that names one is reported.
98
121
 
99
- The result that `prepare` returns carries these as `malformedBlocks`, `undeclaredEntryTypes`, and `policyViolations` entries whose `surface` is `'entry'`, whether the target releases or is skipped. A block whose entries are all undeclared or excluded yields no item, so the commit raises no bump.
122
+ The result that `prepare` returns includes these as `malformedBlocks`, `undeclaredEntryTypes`, `policyViolations` entries whose `surface` is `'entry'`, and `unroutedEntryScopes`, whether the target releases or is skipped. Because a block whose entries are all undeclared or excluded doesn't yield an item, the commit doesn't raise a bump.
100
123
 
101
124
  ## Release-notes injection
102
125
 
@@ -106,4 +129,4 @@ With `releaseNotes.shouldInjectIntoReadme` set to `true`, `release-kit publish`
106
129
  <!-- section:release-notes --><!-- /section:release-notes -->
107
130
  ```
108
131
 
109
- A README without the marker pair receives the notes at its top, above its title, and release-kit's readyup kit reports a README that lacks the markers. Injection is skipped with a warning when `changelogJson.enabled` is `false`, when the version has no entry, or when the entry has no all-audience content. `release-kit prepare --with-release-notes` writes the same injected README as a [preview](releasing.md#previewing-release-notes-with---with-release-notes).
132
+ A README without the marker pair receives the notes at its top, above its title, and release-kit's readyup kit reports a README that lacks the markers. Injection is skipped with a warning when `changelogJson.enabled` is `false`, when the version doesn't have an entry, or when the entry doesn't have any all-audience content. `release-kit prepare --with-release-notes` writes the same injected README as a [preview](releasing.md#previewing-release-notes-with---with-release-notes).
@@ -6,29 +6,29 @@ A minimal config file is in the [README](../README.md#configuration).
6
6
 
7
7
  ## Config file location
8
8
 
9
- release-kit reads `.config/release-kit.config.ts`, resolved against the working directory. A run reads a different file when the subcommand is given `--config <path>`, which is also resolved against the working directory.
9
+ release-kit reads `.config/release-kit.config.ts`, resolved against the repo root, which release-kit locates from the directory from which it was invoked. A run reads a different file when the subcommand is given `--config <path>`, which is resolved against the invocation directory rather than the root, so that a relative path means what it means in the caller's shell.
10
10
 
11
- The flag is accepted by every subcommand that reads a config: `prepare`, `publish`, `create-github-release`, `show-tag-prefixes`, `overrides validate`, `sync-labels init`, and `sync-labels generate`. `commit`, `tag`, `push`, and `sync-labels sync` read no config and reject it.
11
+ The flag is accepted by every subcommand that reads a config: `prepare`, `push`, `publish`, `create-github-release`, `show-tag-prefixes`, `overrides validate`, `sync-labels init`, and `sync-labels generate`. `commit`, `tag`, and `sync-labels sync` don't read a config, and they reject the flag.
12
12
 
13
- The two paths differ in what an absent file means. An absent default path means the repo declares no config, and the run proceeds on discovered defaults. An absent `--config` path fails the command with an error naming the resolved path, because a caller who names a file expects that file to be read. A file that exists and fails validation is an error on both paths alike, and so is one that exists and fails to load.
13
+ The two paths differ in what an absent file means. An absent default path means that the repo doesn't declare a config, and the run proceeds on discovered defaults. An absent `--config` path fails the command with an error naming the resolved path, because a caller who names a file expects that file to be read. A file that exists and fails validation is an error on both paths alike, and so is one that exists and fails to load.
14
14
 
15
- `--config` names a file to read, never a write target. `release-kit init --with-config` scaffolds to `.config/release-kit.config.ts` and takes no `--config`, and `sync-labels init` writes its seeded `repoLabels` block to that same path.
15
+ `--config` names a file to read, never a write target. `release-kit init --with-config` scaffolds to `.config/release-kit.config.ts` and doesn't take `--config`, and `sync-labels init` writes its seeded `repoLabels` block to that same path.
16
16
 
17
17
  ## `ReleaseKitConfig` reference
18
18
 
19
- | Field | Type | Description |
20
- | ------------------ | --------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
21
- | `workspaces` | `WorkspaceOverride[]` | Override or exclude discovered workspaces (matched by `dir`) |
22
- | `formatCommand` | `string` | Shell command to run after changelog generation; modified file paths are appended as arguments |
23
- | `versionPatterns` | `VersionPatterns` | Rules for which commit types trigger major/minor bumps |
24
- | `scopeAliases` | `Record<string, string>` | Maps shorthand scope names to canonical names in commits |
25
- | `workTypes` | `Record<string, WorkTypeConfig>` | Work type definitions, merged with defaults by key |
26
- | `breakingPolicies` | `Record<string, 'forbidden' \| 'optional' \| 'required'>` | Per-type `!`-policy lookup. Defaults to `DEFAULT_BREAKING_POLICIES`. Replaces the default entirely when provided. Set to `{}` to disable enforcement |
27
- | `retiredPackages` | `RetiredPackage[]` | Packages that once lived in this repo but have been extracted or removed; suppresses undeclared-tag-prefix warnings |
28
- | `project` | `ProjectConfig` | Opt-in project-level release block. Declaring `project: {}` (even empty) enables a project-release stage in `prepare` |
29
- | `repoLabels` | `RepoLabelsConfig` | Declares the repository's label registry for `sync-labels`; see [Label configuration](labels.md#label-configuration) |
30
- | `changelogJson` | `ChangelogJsonConfig` | Controls `.meta/changelog.json`: `enabled` (default `true`), `outputPath` (default `'.meta/changelog.json'`), and `devOnlySections`; see [Custom work types](work-types.md#custom-work-types) |
31
- | `releaseNotes` | `ReleaseNotesConfig` | `shouldInjectIntoReadme` (default `false`) injects release notes into each package's README while it publishes; see [Release-notes injection](changelogs.md#release-notes-injection) |
19
+ | Field | Type | Description |
20
+ | ------------------ | ------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
21
+ | `workspaces` | `WorkspaceOverride[]` | Override or exclude discovered workspaces (matched by `dir`) |
22
+ | `formatCommand` | `string` | Shell command to run after changelog generation; modified file paths are appended as arguments |
23
+ | `versionPatterns` | `VersionPatterns` | Rules for which commit types trigger major/minor bumps |
24
+ | `scopeAliases` | `Record<string, string>` | Maps shorthand scope names to canonical names in commit titles and in change-record entry `scopes`, which [route items to workspaces](changelogs.md#routing-to-workspaces). A renamed workspace directory needs an entry from its old name to its new one |
25
+ | `workTypes` | `Record<string, WorkTypeConfig>` | Work type definitions, merged with defaults by key |
26
+ | `breakingPolicies` | `Record<string, 'forbidden' \| 'optional'>` | Per-type `!`-policy lookup. Defaults to `DEFAULT_BREAKING_POLICIES`. Replaces the default entirely when provided. Set to `{}` to disable enforcement |
27
+ | `retiredPackages` | `RetiredPackage[]` | Packages that once lived in this repo but have been extracted or removed; suppresses undeclared-tag-prefix warnings |
28
+ | `project` | `ProjectConfig` | Opt-in project-level release block. Declaring `project: {}` (even empty) enables a project-release stage in `prepare` |
29
+ | `repoLabels` | `RepoLabelsConfig` | Declares the repository's label registry for `sync-labels`; see [Label configuration](labels.md#label-configuration) |
30
+ | `changelogJson` | `ChangelogJsonConfig` | Controls `.meta/changelog.json`: `enabled` (default `true`), `outputPath` (default `'.meta/changelog.json'`), and `devOnlySections`; see [Custom work types](work-types.md#custom-work-types) |
31
+ | `releaseNotes` | `ReleaseNotesConfig` | `shouldInjectIntoReadme` (default `false`) injects release notes into each package's README while it publishes; see [Release-notes injection](changelogs.md#release-notes-injection) |
32
32
 
33
33
  All fields are optional.
34
34
 
@@ -47,7 +47,9 @@ interface LegacyIdentity {
47
47
  }
48
48
  ```
49
49
 
50
- `legacyIdentities` captures prior identities of a workspace as complete `(name, tagPrefix)` snapshots. The union of the current `tagPrefix` and each identity's `tagPrefix` is consulted when release-kit searches for the most recent baseline tag and when generating changelogs. Use it when a workspace's historical tags were published under a different npm name, a different tag prefix, or both — typically across a package rename. Both fields are required per identity: each entry must be a complete historical snapshot that stays valid regardless of subsequent renames. Run `release-kit show-tag-prefixes` to detect undeclared candidates and produce a paste-ready config snippet. Listing the current identity (full `(name, tagPrefix)` match) is rejected as a no-op duplicate; an identity whose `tagPrefix` matches the current but whose `name` differs is valid and documents a prior rename that reused the same tag shape. If the workspace no longer exists in this repo at all (the package was extracted or removed), use [`retiredPackages`](#retiredpackage) instead.
50
+ `prepare` never versions or tags an excluded workspace. A tag of one found on HEAD anyway, made by hand or before the exclusion, is skipped with a warning by `push`, `publish`, and `create-github-release`.
51
+
52
+ `legacyIdentities` captures prior identities of a workspace as complete `(name, tagPrefix)` snapshots. The union of the current `tagPrefix` and each identity's `tagPrefix` is consulted when release-kit searches for the most recent baseline tag and when generating changelogs. Use it when a workspace's historical tags were published under a different npm name, a different tag prefix, or both, typically across a package rename. Both fields are required per identity: Each entry must be a complete historical snapshot that stays valid regardless of subsequent renames. Run `release-kit show-tag-prefixes` to detect undeclared candidates and produce a paste-ready config snippet. Listing the current identity (full `(name, tagPrefix)` match) is rejected as a no-op duplicate; an identity whose `tagPrefix` matches the current but whose `name` differs is valid and documents a prior rename that reused the same tag shape. If the workspace no longer exists in this repo at all (the package was extracted or removed), use [`retiredPackages`](#retiredpackage) instead.
51
53
 
52
54
  ## `RetiredPackage`
53
55
 
@@ -59,9 +61,9 @@ interface RetiredPackage {
59
61
  }
60
62
  ```
61
63
 
62
- `retiredPackages` is the repo-level complement to `legacyIdentities`. Use `legacyIdentities` when the workspace still exists in this repo under a new identity; use `retiredPackages` when no workspace for this package exists in this repo anymore — the package was extracted to another repo or removed outright. Retired entries are inert: release-kit never consults them for baseline lookup or changelog attribution. Their declared `tagPrefix` values are recognized as historical, so `show-tag-prefixes` stops flagging them under "Undeclared tag prefixes."
64
+ `retiredPackages` is the repo-level complement to `legacyIdentities`. Use `legacyIdentities` when the workspace still exists in this repo under a new identity; use `retiredPackages` when this repo no longer contains a workspace for this package, because the package was extracted to another repo or removed outright. Retired entries are inert: release-kit never consults them for baseline lookup or changelog attribution. Their declared `tagPrefix` values are recognized as historical, so `show-tag-prefixes` stops flagging them under "Undeclared tag prefixes."
63
65
 
64
- Worked example — `preflight` was extracted from this monorepo and continues as the standalone `readyup` project. Its tags stay in this repo as historical anchors:
66
+ Worked example: `preflight` was extracted from this monorepo and continues as the standalone `readyup` project. Its tags stay in this repo as historical anchors:
65
67
 
66
68
  ```typescript
67
69
  import { defineConfig } from '@williamthorsen/release-kit/config';
@@ -76,7 +78,7 @@ Validation rules:
76
78
  - `name` and `tagPrefix` are required per entry and must be non-empty strings.
77
79
  - `successor` is optional; if present, it must be a non-empty string.
78
80
  - Full-tuple `(name, tagPrefix)` duplicates within `retiredPackages` are rejected.
79
- - Two entries sharing the same `tagPrefix` but different `name`s are accepted — this documents a package renamed within the repo before being retired.
81
+ - Two entries sharing the same `tagPrefix` but different `name`s are accepted: Such a pair documents a package renamed within the repo before being retired.
80
82
 
81
83
  `show-tag-prefixes` currently does not render a dedicated "Retired packages" section (deferred). Declaring a retired entry is verifiable by confirming that its `tagPrefix` stops appearing under "Undeclared tag prefixes" in the `show-tag-prefixes` output.
82
84
 
@@ -88,17 +90,17 @@ Tag prefixes from distinct owners must not be identical or be a strict prefix of
88
90
  - A `retiredPackages[]` entry (one owner per entry).
89
91
  - The `project` block, when configured.
90
92
 
91
- release-kit matches a tag to a prefix when the tag name starts with the prefix and continues with a digit, so a strict-prefix overlap between distinct owners can match the wrong owner's tags. For example, a project prefix of `v` collides with a workspace prefix of `v11y-check-v`, since `v11y-check-v1.0.0` starts with `v` followed by a digit. The rule rejects every strict-prefix overlap, including one such as `v` and `vue-helpers-v` that no tag of the longer prefix can reach.
93
+ Because release-kit matches a tag to a prefix when the tag name starts with the prefix and continues with a digit, a strict-prefix overlap between distinct owners can match the wrong owner's tags. For example, a project prefix of `v` collides with a workspace prefix of `v11y-check-v`, since `v11y-check-v1.0.0` starts with `v` followed by a digit. The rule rejects every strict-prefix overlap, including one such as `v` and `vue-helpers-v`, whose shorter prefix can't match any tag of the longer one.
92
94
 
93
95
  The rule is enforced at config load; the resulting error identifies both colliding declarations.
94
96
 
95
97
  ## `release-kit show-tag-prefixes`
96
98
 
97
- Print a per-workspace table of derived tag prefixes, tag counts, and declared legacy prefixes. Also surfaces any release-shaped tag prefix in the repo that is neither a derived prefix nor declared via `legacyIdentities`, along with a copy-pasteable `workspaces: [...]` config snippet. The snippet uses a `TODO-fill-in-legacy-npm-name` placeholder for each identity's `name`; replace it with the package's prior npm name before pasting.
99
+ Prints a per-workspace table of derived tag prefixes, tag counts, and declared legacy prefixes. Also reports any release-shaped tag prefix in the repo that is neither a derived prefix nor declared via `legacyIdentities`, along with a copy-pasteable `workspaces: [...]` config snippet. The snippet uses a `TODO-fill-in-legacy-npm-name` placeholder for each identity's `name`; replace it with the package's prior npm name before pasting.
98
100
 
99
- Exits `0` when every workspace derives a prefix and there are no cross-workspace collisions; exits `1` on any derivation failure or collision. Undeclared candidates do not affect the exit code — they surface as a warning via the `legacy tag prefixes are declared` readyup check.
101
+ Exits `0` when every workspace derives a prefix and the workspaces' prefixes don't collide; exits `1` on any derivation failure or collision. Undeclared candidates do not affect the exit code; the `legacy tag prefixes are declared` readyup check reports them as a warning.
100
102
 
101
- A config file that exists and either fails to load or fails schema validation reports the failure on stderr and exits `1`, printing no preview, because a config that cannot be read leaves the declared-versus-undeclared verdict wrong in the one dimension the command exists to report on. That holds on the default path and under `--config`, and in single-package mode as well as monorepo mode. An absent default config is a supported state and still previews against derived defaults.
103
+ When a config file exists and either fails to load or fails schema validation, the command reports the failure on stderr and exits `1` without printing a preview, because a config that cannot be read leaves the declared-versus-undeclared verdict wrong in the one dimension on which the command exists to report. That holds on the default path and under `--config`, and in single-package mode as well as monorepo mode. An absent default config is a supported state and still previews against derived defaults.
102
104
 
103
105
  In single-package mode, prints a single row with `workspacePath = .` and `derivedPrefix = v`; legacy entries and undeclared-candidate scanning are not applicable.
104
106
 
@@ -133,6 +135,6 @@ The variable outranks detection. Under `auto`, release-kit defers to detection,
133
135
 
134
136
  1. `CI` is set to anything other than an empty string or `false`.
135
137
  2. The stream is not a terminal, as when its output is piped or redirected.
136
- 3. `TERM` is `linux`, the Linux virtual console, which draws no emoji.
138
+ 3. `TERM` is `linux`, the Linux virtual console, which doesn't draw emoji.
137
139
 
138
- The style governs emoji alone. Changelog section emoji are file content rather than terminal output, and they are unaffected.
140
+ The style affects only emoji. Changelog section emoji are file content rather than terminal output, and they are unaffected.
@@ -2,9 +2,9 @@
2
2
 
3
3
  How to correct a generated changelog entry without rewriting history: where override files live, how they compose, how a key targets items, what an entry may set, and how to validate the files.
4
4
 
5
- Generated changelogs occasionally need editorial correction — typos, redacted scope, reworded entries, or historical commits whose bodies carry verbatim PR-template scaffolding (`## What`, `## Why`, etc.) that renders as literal text in user-facing release notes. Rewriting git history is not viable, and any in-place edit to `CHANGELOG.md` or `.meta/changelog.json` is overwritten on the next release because release-kit regenerates both artifacts from scratch.
5
+ Generated changelogs occasionally need editorial correction: typos, redacted scope, reworded entries, or historical commits whose bodies contain verbatim PR-template scaffolding (`## What`, `## Why`, etc.) that renders as literal text in user-facing release notes. Rewriting git history is not viable, and any in-place edit to a version that release-kit renders in `CHANGELOG.md` or `.meta/changelog.json` is overwritten on the next release, because release-kit regenerates that version in both artifacts from its entries.
6
6
 
7
- Override files are the supported escape hatch. Drop a checked-in JSON file at the conventional path for the scope you want to influence, keyed by commit hash or by commit hash and entry position, and `release-kit prepare` applies the overrides between `buildChangelogEntries` and serialization. Both `CHANGELOG.md` and `.meta/changelog.json` reflect the post-override view, so downstream consumers (the GitHub Release body, the in-app release-notes page, etc.) see the same content.
7
+ Override files are the supported escape hatch. Add a checked-in JSON file at the conventional path for the scope that it should affect, keyed by commit hash or by commit hash and entry position, and `release-kit prepare` applies the overrides between `buildChangelogEntries` and serialization. Both `CHANGELOG.md` and `.meta/changelog.json` reflect the post-override view, so downstream consumers (the GitHub Release body, the in-app release-notes page, etc.) see the same content.
8
8
 
9
9
  ## File-location convention
10
10
 
@@ -14,7 +14,7 @@ Override files are the supported escape hatch. Drop a checked-in JSON file at th
14
14
  | Workspace | `packages/<ws>/.meta/changelog-overrides.json` | Only that workspace's changelog |
15
15
  | Single-package mode | `.meta/changelog-overrides.json` | The package's changelog (collapses to the project case) |
16
16
 
17
- Filenames have no leading dot — the `.meta/` directory already provides the visibility property and parallels its sibling artifacts (`changelog.json`, `label-map.json`).
17
+ Filenames don't have a leading dot, because the `.meta/` directory already provides the visibility property and parallels its sibling artifacts (`changelog.json`, `label-map.json`).
18
18
 
19
19
  ## Composition: per-key shadowing
20
20
 
@@ -22,18 +22,18 @@ When a workspace's changelog is rendered, both files are consulted:
22
22
 
23
23
  - The root file's overrides apply globally.
24
24
  - The workspace file's overrides apply only to that workspace.
25
- - When the **same key** (string-equal, byte-for-byte) appears in both files, the **workspace entry wins entirely** for that workspace's changelog — no field-level merge, the workspace entry replaces the root entry.
25
+ - When the **same key** (string-equal, byte-for-byte) appears in both files, the **workspace entry wins entirely** for that workspace's changelog: release-kit doesn't merge the two entries field by field; the workspace entry replaces the root entry.
26
26
  - Non-identical keys that match the same item, such as a root `abc1234` beside a workspace `abc1234:2`, or two prefixes of one commit, do **not** shadow. The run fails with the [overlapping-keys error](#conflicting-keys), which names both keys.
27
27
  - Other keys in the root file still apply for that workspace.
28
28
 
29
- The project-level changelog applies only the root file. Per-workspace files describe per-workspace editorial intent and have no meaning at the aggregated project tier.
29
+ The project-level changelog applies only the root file. Per-workspace files describe per-workspace editorial intent and don't have any meaning at the aggregated project tier.
30
30
 
31
31
  ## Stale-key warnings
32
32
 
33
33
  A key that doesn't match any commit gets a stale-reference warning. The warning's scope mirrors the file's scope:
34
34
 
35
35
  - **Per-workspace files** are warned against their own apply context. A key in `packages/foo/.meta/changelog-overrides.json` that doesn't match any commit in foo's changelog is unambiguously stale and is warned immediately.
36
- - **Root file** keys are aggregated globally — a root key that matches in any workspace or in the project changelog is non-stale; a root key matched nowhere is warned exactly once after all batches complete.
36
+ - **Root file** keys are aggregated globally: A root key that matches in any workspace or in the project changelog is non-stale; a root key matched nowhere is warned exactly once after all batches complete.
37
37
 
38
38
  ## File shape
39
39
 
@@ -65,7 +65,7 @@ The top-level `$schema` key is optional and names the [published JSON Schema](#p
65
65
  | `body` | `string` | Replaces the entry's body (the prose that renders below the bullet). Other fields are preserved. |
66
66
  | `breaking` | `boolean` | Toggles the `🚨 **Breaking:** ` marker on the bullet. |
67
67
 
68
- There is no `migration` key: On a title-derived item the field is derived from `body`, so an override that replaces `body` re-derives it, and an item derived from a change-record entry keeps its entry's `migration`. See [The `migration` field](changelogs.md#the-migration-field).
68
+ An override entry doesn't take a `migration` key: On a title-derived item the field is derived from `body`, so an override that replaces `body` re-derives it, and an item derived from a change-record entry keeps its entry's `migration`. See [The `migration` field](changelogs.md#the-migration-field).
69
69
 
70
70
  ## Keys
71
71
 
@@ -76,13 +76,13 @@ A key takes one of two forms. In both, the hash part is lowercase hex and matche
76
76
  | `<hash>` | Every item of the commit |
77
77
  | `<hash>:<n>` | The one item derived from the commit's `n`th change-record entry |
78
78
 
79
- `n` counts from 1 over the `entries` of the commit's [change-record block](changelogs.md#change-record-blocks), in the order in which the PR lists them, including entries that yield no item. An item derived from the commit's title has no entry position, so `<hash>:<n>` never matches it. Any other key fails loading.
79
+ `n` counts from 1 over the `entries` of the commit's [change-record block](changelogs.md#change-record-blocks), in the order in which the PR lists them, including entries that don't yield an item. An item derived from the commit's title doesn't have an entry position, so `<hash>:<n>` never matches it. Any other key fails loading.
80
80
 
81
81
  The hash part can be the full 40-character SHA or a prefix. `prepare` resolves it against the distinct commit hashes in the changelog:
82
82
 
83
- - **One commit** — the override applies to the items that the key targets. A 7-character prefix is usually unambiguous within a single repo's history; longer prefixes are always safe.
84
- - **No commit** — the override is treated as a stale reference (probably from a rebase or branch deletion) and prepare reports a warning. The release continues. An ordinal key whose commit has no item at position `n` is stale in the same way.
85
- - **Several commits** — the release aborts with an error naming the key and the matching hashes. Lengthen the prefix or use the full SHA.
83
+ - **One commit**: The override applies to the items that the key targets. A 7-character prefix is usually unambiguous within a single repo's history; longer prefixes are always safe.
84
+ - **No commit**: The override is treated as a stale reference (probably from a rebase or branch deletion) and prepare reports a warning. The release continues. An ordinal key whose commit doesn't have an item at position `n` is stale in the same way.
85
+ - **Several commits**: The release aborts with an error naming the key and the matching hashes. Lengthen the prefix or use the full SHA.
86
86
 
87
87
  ### Conflicting keys
88
88
 
@@ -104,13 +104,13 @@ Suppose commit `3f0c9a1` merged a PR whose change record lists three entries. It
104
104
 
105
105
  A bare `"3f0c9a1": { "audience": "skip" }` would skip all three items instead. Adding it beside the keys above fails the run, because it overlaps each of them.
106
106
 
107
- Override application errors abort the run with a non-zero exit; warnings (zero-match keys) are non-fatal and surface on `PrepareResult.warnings`.
107
+ Override application errors abort the run with a non-zero exit; warnings (zero-match keys) are non-fatal and appear in `PrepareResult.warnings`.
108
108
 
109
109
  ## Validation
110
110
 
111
- The override file is validated when `release-kit prepare` loads it. Each error names the offending key so you can locate it in your file:
111
+ The override file is validated when `release-kit prepare` loads it. Each error names the offending key so that the developer can locate it in the file:
112
112
 
113
- - Missing file → empty map, no error (the no-op default — projects that do not need overrides skip the file entirely).
113
+ - Missing file → empty map, no error (the no-op default: Projects that do not need overrides skip the file entirely).
114
114
  - Malformed JSON → error.
115
115
  - A key that is neither `<hash>` nor `<hash>:<n>` (uppercase hex, non-hex characters, `:0`, a leading zero in `n`) → error.
116
116
  - A top-level `$schema` that is not a string → error.
@@ -118,7 +118,7 @@ The override file is validated when `release-kit prepare` loads it. Each error n
118
118
  - Unknown fields on an entry → error.
119
119
  - Wrong field types (e.g., `description` as a number) → error.
120
120
  - An entry with no fields set → error (a copy-paste mistake more often than not).
121
- - `audience: 'all'` or `audience: 'dev'` → error in the current release: only `'skip'` is supported (see below).
121
+ - `audience: 'all'` or `audience: 'dev'` → error in the current release: Only `'skip'` is supported (see below).
122
122
 
123
123
  Applying the overrides then aborts the run on an ambiguous prefix and on either of the [conflicting-key conditions](#conflicting-keys): a bare key setting `description` or `body` on a commit with several items, and two keys matching the same item.
124
124
 
@@ -137,9 +137,9 @@ This walks every `.meta/changelog-overrides.json` file across the project tier a
137
137
  | Schema/parse errors | malformed JSON, malformed keys, unknown fields, wrong field types, no-field entries, unsupported `audience` | `2` |
138
138
  | Ambiguous-prefix | an override key resolves to 2+ commit hashes | `2` |
139
139
  | Conflicting keys | a bare key sets `description` or `body` on a commit with several items; two keys match the same item | `2` |
140
- | Stale-key warnings | an override key resolves to no item in its applicable scope | `1` |
140
+ | Stale-key warnings | an override key doesn't resolve to any item in its applicable scope | `1` |
141
141
 
142
- Tier-aware stale-key semantics match `release-kit prepare`'s match-set exactly: a workspace-tier key is stale if it does not match in its own workspace's history; a root-tier key is stale only if it matches in **no** scope (no workspace AND not the project release window).
142
+ Tier-aware stale-key semantics match `release-kit prepare`'s match-set exactly: A workspace-tier key is stale if it does not match in its own workspace's history; a root-tier key is stale only if it doesn't match in **any** scope (neither any workspace nor the project release window).
143
143
 
144
144
  Conflicting keys are found within one file and across the root and workspace files, as `prepare` composes them. An overlap between a root key and a workspace key is reported against the workspace file.
145
145
 
@@ -166,13 +166,13 @@ The schema states the key form and the entry fields. It accepts `audience` value
166
166
 
167
167
  ## Audience semantics: v1 supports `'skip'` only
168
168
 
169
- The on-disk format declares the full `'all' | 'dev' | 'skip'` audience vocabulary so the file format will not need to change when the v2 reclassification feature ships. In the current release, only `'skip'` is supported at runtime; `'all'` and `'dev'` are rejected with an explicit "not yet supported" error.
169
+ The on-disk format declares the full `'all' | 'dev' | 'skip'` audience vocabulary so that the file format will not need to change when the v2 reclassification feature is released. In the current release, only `'skip'` is supported at runtime; `'all'` and `'dev'` are rejected with an explicit "not yet supported" error.
170
170
 
171
- The eventual v2 behavior will let an override move a single item to a different audience section (e.g., reclassifying a `Documentation` entry as `Internal features` to keep it out of public-facing release notes). v1 deliberately leaves that as a separate change so the override mechanism can ship now and the section-split logic can land additively later.
171
+ The eventual v2 behavior will let an override move a single item to a different audience section (e.g., reclassifying a `Documentation` entry as `Internal features` to keep it out of public-facing release notes). v1 deliberately leaves that as a separate change so that the override mechanism can be released now and the section-split logic can be added later.
172
172
 
173
173
  ## Worked example 1: cleaning up scaffolded historical commits (root file)
174
174
 
175
- Suppose a year-old commit `82962311` was authored from a PR template that left `## What` / `## Why` headings in the body, and that commit now appears in your in-app release notes as literal Markdown headings. Add an override at the project tier:
175
+ Suppose a year-old commit `82962311` was authored from a PR template that left `## What` / `## Why` headings in the body, and that commit now appears in the project's in-app release notes as literal Markdown headings. Add an override at the project tier:
176
176
 
177
177
  ```json
178
178
  // .meta/changelog-overrides.json
@@ -187,9 +187,9 @@ On the next `release-kit prepare` run, the matched item's body is replaced befor
187
187
 
188
188
  ## Worked example 2: suppressing a cross-attribution spillover (workspace file)
189
189
 
190
- Release-kit attributes commits to workspaces by file path, so a commit that primarily belongs to one workspace can land in another's changelog if it touched files there. Suppose commit `1ce3d2f` renamed the `audit-deps` package to `v11y-check` (scope `v11y-check`) but also edited `packages/nmr/src/default-scripts.ts` and `packages/nmr/README.md`. The commit correctly appears in `packages/v11y-check/CHANGELOG.md`, but it also spills into `packages/nmr/CHANGELOG.md` where it isn't the right editorial framing.
190
+ Release-kit attributes commits to workspaces by file path, so a commit that primarily belongs to one workspace can appear in another's changelog if it touched files there. Suppose commit `1ce3d2f` renamed the `audit-deps` package to `v11y-check` (scope `v11y-check`) but also edited `packages/nmr/src/default-scripts.ts` and `packages/nmr/README.md`. The commit correctly appears in `packages/v11y-check/CHANGELOG.md`, but it also appears in `packages/nmr/CHANGELOG.md`, where it isn't the right editorial framing.
191
191
 
192
- Drop a workspace-tier override at `packages/nmr/.meta/changelog-overrides.json`:
192
+ Add a workspace-tier override at `packages/nmr/.meta/changelog-overrides.json`:
193
193
 
194
194
  ```json
195
195
  // packages/nmr/.meta/changelog-overrides.json
@@ -200,4 +200,4 @@ Drop a workspace-tier override at `packages/nmr/.meta/changelog-overrides.json`:
200
200
  }
201
201
  ```
202
202
 
203
- The commit is now suppressed in nmr's changelog only — it still appears in v11y-check's, where it belongs. A root-tier `'skip'` would have removed it from both, which is the wrong outcome.
203
+ The commit is now suppressed in nmr's changelog only; it still appears in v11y-check's, where it belongs. A root-tier `'skip'` would have removed it from both, which is the wrong outcome.