@williamthorsen/release-kit 11.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 (189) hide show
  1. package/.meta/changelog.json +274 -2
  2. package/.readyup/kits/default.js +18 -33
  3. package/.readyup/kits/npm-auto-publish.js +24 -23
  4. package/.readyup/manifest.json +16 -11
  5. package/CHANGELOG.md +98 -0
  6. package/README.md +57 -69
  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 +4 -0
  12. package/dist/esm/attachChangelogDiagnostics.js +14 -0
  13. package/dist/esm/bin/release-kit.js +88 -24
  14. package/dist/esm/buildChangelogEntries.d.ts +69 -17
  15. package/dist/esm/buildChangelogEntries.js +222 -120
  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/buildReleaseSummary.js +2 -2
  20. package/dist/esm/buildSyntheticChangelogEntry.d.ts +3 -5
  21. package/dist/esm/buildSyntheticChangelogEntry.js +2 -2
  22. package/dist/esm/bumpVersion.d.ts +1 -8
  23. package/dist/esm/changelogJsonFile.d.ts +5 -7
  24. package/dist/esm/changelogJsonFile.js +2 -17
  25. package/dist/esm/changelogJsonUtils.d.ts +7 -10
  26. package/dist/esm/changelogOverrides.d.ts +79 -100
  27. package/dist/esm/changelogOverrides.js +138 -58
  28. package/dist/esm/classifyChangelogCommit.d.ts +36 -0
  29. package/dist/esm/classifyChangelogCommit.js +34 -0
  30. package/dist/esm/commitCommand.d.ts +2 -4
  31. package/dist/esm/compareVersions.d.ts +10 -4
  32. package/dist/esm/compareVersions.js +16 -0
  33. package/dist/esm/configFlagSchema.d.ts +1 -1
  34. package/dist/esm/createGithubRelease.d.ts +7 -19
  35. package/dist/esm/createGithubReleaseCommand.d.ts +5 -5
  36. package/dist/esm/createGithubReleaseCommand.js +9 -9
  37. package/dist/esm/createTags.d.ts +2 -3
  38. package/dist/esm/decideRelease.d.ts +11 -41
  39. package/dist/esm/decideRelease.js +5 -27
  40. package/dist/esm/defaults.d.ts +5 -24
  41. package/dist/esm/defaults.js +7 -5
  42. package/dist/esm/defineConfig.d.ts +1 -1
  43. package/dist/esm/deleteFileIfExists.d.ts +1 -1
  44. package/dist/esm/deriveReleaseNotesConfig.d.ts +20 -0
  45. package/dist/esm/deriveReleaseNotesConfig.js +21 -0
  46. package/dist/esm/deriveWorkspaceConfig.d.ts +3 -5
  47. package/dist/esm/detectPackageManager.d.ts +2 -4
  48. package/dist/esm/detectUndeclaredTagPrefixes.d.ts +4 -6
  49. package/dist/esm/detectUndeclaredTagPrefixes.js +1 -1
  50. package/dist/esm/determineBumpType.d.ts +11 -11
  51. package/dist/esm/determineBumpType.js +6 -7
  52. package/dist/esm/discoverWorkspaces.d.ts +14 -15
  53. package/dist/esm/discoverWorkspaces.js +12 -12
  54. package/dist/esm/enterRepoRoot.d.ts +12 -0
  55. package/dist/esm/enterRepoRoot.js +16 -0
  56. package/dist/esm/enumerateReleaseWindows.d.ts +50 -0
  57. package/dist/esm/enumerateReleaseWindows.js +124 -0
  58. package/dist/esm/extractMigration.d.ts +6 -8
  59. package/dist/esm/findUnroutedEntryScopes.d.ts +16 -0
  60. package/dist/esm/findUnroutedEntryScopes.js +57 -0
  61. package/dist/esm/formatExcludedSkip.d.ts +3 -0
  62. package/dist/esm/formatExcludedSkip.js +3 -0
  63. package/dist/esm/formatPrivateSkip.d.ts +1 -6
  64. package/dist/esm/generateChangelogs.d.ts +13 -26
  65. package/dist/esm/generateChangelogs.js +0 -14
  66. package/dist/esm/glyphs.d.ts +2 -2
  67. package/dist/esm/help/prepareHelp.d.ts +3 -4
  68. package/dist/esm/help/prepareHelp.js +7 -8
  69. package/dist/esm/index.d.ts +1 -1
  70. package/dist/esm/init/checks.d.ts +3 -3
  71. package/dist/esm/init/checks.js +4 -1
  72. package/dist/esm/init/detectRepoType.d.ts +2 -3
  73. package/dist/esm/init/hasProvenance.d.ts +2 -0
  74. package/dist/esm/init/hasProvenance.js +3 -0
  75. package/dist/esm/init/initCommand.d.ts +3 -6
  76. package/dist/esm/init/initCommand.js +11 -11
  77. package/dist/esm/init/parseJsonRecord.d.ts +1 -4
  78. package/dist/esm/init/scaffold.d.ts +1 -3
  79. package/dist/esm/init/scaffold.js +2 -30
  80. package/dist/esm/init/templates.d.ts +9 -11
  81. package/dist/esm/init/templates.js +12 -11
  82. package/dist/esm/init/updateTemplatesCommand.d.ts +13 -0
  83. package/dist/esm/init/updateTemplatesCommand.js +64 -0
  84. package/dist/esm/injectReleaseNotesIntoReadme.d.ts +12 -17
  85. package/dist/esm/injectSection.d.ts +3 -3
  86. package/dist/esm/loadConfig.d.ts +16 -14
  87. package/dist/esm/loadConfig.js +2 -5
  88. package/dist/esm/loadValidatedConfig.d.ts +16 -19
  89. package/dist/esm/loadValidatedConfig.js +6 -1
  90. package/dist/esm/packsPath.d.ts +9 -9
  91. package/dist/esm/parseChangeRecordBlock.d.ts +37 -0
  92. package/dist/esm/parseChangeRecordBlock.js +134 -0
  93. package/dist/esm/parseCommitMessage.d.ts +42 -32
  94. package/dist/esm/parseCommitMessage.js +33 -41
  95. package/dist/esm/parseRequestedTags.d.ts +3 -3
  96. package/dist/esm/planReleaseNotesPreviews.d.ts +6 -6
  97. package/dist/esm/planVersionBump.d.ts +5 -3
  98. package/dist/esm/planVersionBump.js +3 -0
  99. package/dist/esm/prepareCommand.d.ts +4 -7
  100. package/dist/esm/prepareCommand.js +17 -18
  101. package/dist/esm/previewTagPrefixes.d.ts +8 -10
  102. package/dist/esm/propagateBumps.d.ts +6 -7
  103. package/dist/esm/publish.d.ts +1 -1
  104. package/dist/esm/publishCommand.d.ts +4 -3
  105. package/dist/esm/publishCommand.js +8 -8
  106. package/dist/esm/pushCommand.d.ts +4 -3
  107. package/dist/esm/pushCommand.js +8 -2
  108. package/dist/esm/pushRelease.d.ts +2 -2
  109. package/dist/esm/readChangelogSections.d.ts +33 -0
  110. package/dist/esm/readChangelogSections.js +80 -0
  111. package/dist/esm/releaseFiles.d.ts +5 -7
  112. package/dist/esm/releasePlan.d.ts +11 -13
  113. package/dist/esm/releasePrepare.d.ts +25 -25
  114. package/dist/esm/releasePrepare.js +80 -80
  115. package/dist/esm/releasePrepareMono.d.ts +9 -7
  116. package/dist/esm/releasePrepareMono.js +114 -99
  117. package/dist/esm/releasePrepareProject.d.ts +19 -29
  118. package/dist/esm/releasePrepareProject.js +46 -45
  119. package/dist/esm/renderChangelogMarkdown.d.ts +10 -4
  120. package/dist/esm/renderChangelogMarkdown.js +27 -2
  121. package/dist/esm/renderReleaseNotes.d.ts +3 -11
  122. package/dist/esm/renderReleaseNotes.js +2 -6
  123. package/dist/esm/reportPrepare.d.ts +4 -5
  124. package/dist/esm/reportPrepare.js +67 -42
  125. package/dist/esm/resolveCommandTags.d.ts +15 -11
  126. package/dist/esm/resolveCommandTags.js +35 -5
  127. package/dist/esm/resolveConfigFlag.d.ts +7 -0
  128. package/dist/esm/resolveConfigFlag.js +4 -0
  129. package/dist/esm/resolveReleaseTags.d.ts +6 -8
  130. package/dist/esm/resolveReleaseTags.js +1 -1
  131. package/dist/esm/resolveStylesOrExit.d.ts +2 -2
  132. package/dist/esm/showTagPrefixesCommand.d.ts +7 -10
  133. package/dist/esm/showTagPrefixesCommand.js +4 -4
  134. package/dist/esm/stripEmojiPrefix.d.ts +1 -0
  135. package/dist/esm/stripScope.d.ts +2 -2
  136. package/dist/esm/stripScope.js +3 -3
  137. package/dist/esm/sync-labels/generateCommand.d.ts +11 -10
  138. package/dist/esm/sync-labels/generateCommand.js +11 -8
  139. package/dist/esm/sync-labels/initCommand.d.ts +6 -7
  140. package/dist/esm/sync-labels/initCommand.js +2 -2
  141. package/dist/esm/sync-labels/presets.d.ts +5 -4
  142. package/dist/esm/sync-labels/presets.js +9 -9
  143. package/dist/esm/sync-labels/resolveLabels.d.ts +7 -7
  144. package/dist/esm/sync-labels/resolveLabels.js +1 -1
  145. package/dist/esm/sync-labels/retiredConfig.d.ts +4 -5
  146. package/dist/esm/sync-labels/syncCommand.d.ts +1 -4
  147. package/dist/esm/sync-labels/templates.d.ts +3 -3
  148. package/dist/esm/sync-labels/templates.js +3 -3
  149. package/dist/esm/sync-labels/typeLabels.d.ts +14 -0
  150. package/dist/esm/sync-labels/typeLabels.js +38 -0
  151. package/dist/esm/sync-labels/types.d.ts +1 -1
  152. package/dist/esm/tagCommand.d.ts +1 -1
  153. package/dist/esm/typeGuards.d.ts +2 -2
  154. package/dist/esm/types.d.ts +185 -116
  155. package/dist/esm/types.js +2 -2
  156. package/dist/esm/validateConfig.d.ts +2 -11
  157. package/dist/esm/validateConfig.js +5 -1
  158. package/dist/esm/validateOnlyExcludesStrandedDependents.d.ts +14 -12
  159. package/dist/esm/validateOverridesCommand.d.ts +19 -21
  160. package/dist/esm/validateOverridesCommand.js +16 -23
  161. package/docs/api.md +6 -6
  162. package/docs/changelogs.md +90 -28
  163. package/docs/configuration.md +30 -29
  164. package/docs/editorial-overrides.md +90 -31
  165. package/docs/labels.md +21 -19
  166. package/docs/project-releases.md +14 -14
  167. package/docs/publishing.md +5 -5
  168. package/docs/readiness-checks.md +3 -3
  169. package/docs/releasing.md +54 -24
  170. package/docs/work-types.md +46 -58
  171. package/package.json +7 -7
  172. package/presets/labels/common.yaml +1 -41
  173. package/schemas/changelog-overrides.json +39 -0
  174. package/cliff.toml.template +0 -65
  175. package/dist/esm/collectPolicyViolations.d.ts +0 -15
  176. package/dist/esm/collectPolicyViolations.js +0 -12
  177. package/dist/esm/determineBumpFromCommits.d.ts +0 -17
  178. package/dist/esm/determineBumpFromCommits.js +0 -28
  179. package/dist/esm/getCommitsSinceTarget.d.ts +0 -21
  180. package/dist/esm/getCommitsSinceTarget.js +0 -66
  181. package/dist/esm/resolveCliffConfigPath.d.ts +0 -11
  182. package/dist/esm/resolveCliffConfigPath.js +0 -20
  183. package/dist/esm/resolveReleaseNotesConfig.d.ts +0 -26
  184. package/dist/esm/resolveReleaseNotesConfig.js +0 -29
  185. package/dist/esm/runGitCliff.d.ts +0 -38
  186. package/dist/esm/runGitCliff.js +0 -25
  187. package/dist/esm/workTypesData.d.ts +0 -50
  188. package/dist/esm/workTypesData.js +0 -145
  189. package/schemas/label-map.json +0 -24
@@ -1,4 +1,8 @@
1
1
  import type { ChangelogEntry, ChangelogOverride, WorkspaceConfig } from './types.js';
2
+ /** Known fields on a single override entry; presence of any other field is a validation error. */
3
+ export declare const KNOWN_OVERRIDE_FIELDS: Set<string>;
4
+ /** Form of an override key: a lowercase hex commit hash or prefix, optionally followed by `:<n>`, a 1-based entry position. */
5
+ export declare const OVERRIDE_KEY_PATTERN: RegExp;
2
6
  /** Result of loading an override file: either parsed overrides or a list of structured errors. */
3
7
  export type LoadChangelogOverridesResult = {
4
8
  overrides: Map<string, ChangelogOverride>;
@@ -6,55 +10,47 @@ export type LoadChangelogOverridesResult = {
6
10
  errors: string[];
7
11
  };
8
12
  /**
9
- * Load and validate the editorial overrides file at `path`.
10
- *
11
- * - Missing file resolves to an empty map (no-op default; matches "absent file → unchanged behavior").
12
- * - Malformed JSON, wrong top-level shape, or any per-entry validation failure surfaces as
13
- * structured errors. Callers decide how to surface them.
14
- *
15
- * Returns either `{ overrides: Map }` on success or `{ errors: string[] }` on failure. Pure
16
- * except for the single file read; performs no other I/O.
13
+ * Loads and validates the editorial overrides file at `path`. A missing file yields an empty map; an unreadable file,
14
+ * malformed JSON, a wrong top-level shape, or any per-entry validation failure yields errors.
17
15
  */
18
16
  export declare function loadChangelogOverrides(path: string): LoadChangelogOverridesResult;
19
17
  /**
20
- * Validate a parsed override record. Returns the parsed `Map` along with any error messages.
18
+ * Validates a parsed override record, returning the valid overrides and an error message for each problem.
21
19
  *
22
- * Each error names the offending key (e.g. `overrides['abc']: 'audience' must be one of …`)
23
- * so the consumer can locate it in their override file.
20
+ * Each error names the offending key (e.g. `overrides['abc']: 'audience' must be one of …`) so that the user can find
21
+ * it in the override file.
24
22
  */
25
23
  export declare function validateChangelogOverrides(raw: unknown): {
26
24
  overrides: Map<string, ChangelogOverride>;
27
25
  errors: string[];
28
26
  };
29
- /**
30
- * Format the standard "stale override key" warning. Callers compute the set of stale keys
31
- * (keys that didn't match anywhere) and use this helper to surface them uniformly. Centralized
32
- * so single-package and monorepo flows produce identical warning text.
33
- */
27
+ /** Splits a valid override key into its hash prefix and, for an ordinal key, its 1-based entry position. */
28
+ export declare function parseOverrideKey(key: string): {
29
+ hashPrefix: string;
30
+ entry?: number;
31
+ };
32
+ /** Formats the warning for an override key that did not match any item in the changelog. */
34
33
  export declare function formatStaleOverrideKeyWarning(key: string): string;
35
34
  /**
36
- * Apply overrides to a `ChangelogEntry[]`, returning a new array. Pure: no mutation, no I/O.
35
+ * Applies overrides to changelog entries, returning new entries without mutating the input.
37
36
  *
38
- * Match algorithm: each override key is treated as a string-prefix against `ChangelogItem.hash`.
39
- * - 0 matches → key is recorded as unmatched in this batch (no warning emitted here).
40
- * - 1 match → apply each present override field to the matched item, record key as matched.
41
- * - 2+ matches → error (ambiguous prefix).
37
+ * Each key's hash prefix is matched against the distinct `ChangelogItem.hash` values:
38
+ * - No match: The key is unmatched in this batch, and this function doesn't emit a warning.
39
+ * - One match: A bare key resolves to every item of that commit, and an ordinal key `<hash>:<n>` to the items whose
40
+ * `entry` is `n`. A key that doesn't resolve to any item is unmatched, so an ordinal key on a title-derived item
41
+ * goes stale.
42
+ * - Several matches: an error (ambiguous prefix).
42
43
  *
43
- * `matchedKeys` lists the override keys that resolved to exactly one item in this batch.
44
- * Callers are responsible for computing stale-key warnings: in a monorepo run, an override
45
- * may target a commit that lives in another workspace, so the per-batch zero-match signal is
46
- * insufficient on its own. Single-package and monorepo orchestrators format warnings using
47
- * `formatStaleOverrideKeyWarning` after aggregating `matchedKeys` across all apply calls.
44
+ * Two more conditions are errors, and the keys involved apply nowhere: a bare key that sets `description` or `body` on a
45
+ * commit with several items, and two keys that resolve to the same item.
48
46
  *
49
- * v1 audience semantics:
50
- * - `'skip'` removes the matched item from its containing section. Empty sections are pruned.
51
- * Versions with zero sections still appear (matches existing "empty workspace" behavior).
52
- * - `'all'` and `'dev'` are validated out before reaching the applier.
47
+ * `matchedKeys` lists the keys that resolved to at least one item without error. The caller computes stale-key
48
+ * warnings, because in a monorepo a key may target a commit in another workspace: A key unmatched in one batch is not
49
+ * necessarily stale.
53
50
  *
54
- * Items without a `hash` (synthetic propagation entries) are never matched and pass through.
55
- *
56
- * The function does not throw; warnings/errors accumulate and are returned alongside the
57
- * transformed entries so the caller decides whether to abort or log-and-continue.
51
+ * An override whose audience is `'skip'` removes its item, and a section left empty is dropped; an entry left without
52
+ * sections is kept. An item without a `hash`, such as a synthetic propagation item, passes through unmatched. Errors
53
+ * are returned rather than thrown, and the caller decides whether to abort.
58
54
  */
59
55
  export declare function applyChangelogOverrides(entries: ChangelogEntry[], overrides: Map<string, ChangelogOverride>): {
60
56
  entries: ChangelogEntry[];
@@ -63,10 +59,8 @@ export declare function applyChangelogOverrides(entries: ChangelogEntry[], overr
63
59
  matchedKeys: string[];
64
60
  };
65
61
  /**
66
- * Resolve the conventional override-file path for a given scope root.
67
- *
68
- * `scopeRoot` is the directory at which a scope's other artifacts live (e.g., `'.'` for the
69
- * project, `'packages/foo'` for a workspace). The returned path is repo-relative.
62
+ * Resolves the conventional override-file path for a scope root: the directory in which a scope's other artifacts live
63
+ * (e.g., `'.'` for the project, `'packages/foo'` for a workspace). The returned path is repo-relative.
70
64
  */
71
65
  export declare function resolveOverridePath(scopeRoot: string): string;
72
66
  /** Result of {@link loadOverridesForScopes}: per-scope maps plus accumulated load/validation errors. */
@@ -79,40 +73,27 @@ export interface LoadOverridesForScopesResult {
79
73
  errors: string[];
80
74
  }
81
75
  /**
82
- * Load and validate every requested override file in one pass.
83
- *
84
- * Iterates the configured scope roots, calls {@link loadChangelogOverrides} for each, and
85
- * aggregates errors across all files (rather than throwing on the first failure) so a
86
- * consumer who edits multiple files at once sees every problem in one report. Missing files
87
- * resolve to empty maps — matches the existing single-file behavior.
88
- *
89
- * The caller is expected to surface a combined error when `errors.length > 0` and abort the
90
- * release before any workspace begins writing.
76
+ * Loads and validates the override file of every requested scope, collecting the errors of all files rather than
77
+ * stopping at the first, so that a user who edits several files sees every problem in one report. A missing file
78
+ * yields an empty map.
91
79
  */
92
80
  export declare function loadOverridesForScopes(scopes: {
93
81
  project?: string;
94
82
  workspaces?: string[];
95
83
  }): LoadOverridesForScopesResult;
96
84
  /**
97
- * Compose root and workspace override maps into a single effective map for one workspace.
85
+ * Composes root and workspace override maps into the effective map for one workspace, without mutating either.
98
86
  *
99
- * Byte-equal-key shadowing: when a workspace key string-equals a root key, the workspace
100
- * entry wins entirely (no field-level merge) and supplants the root entry in the result.
101
- * Different-prefix keys that happen to resolve to the same commit do NOT shadow here — they
102
- * fall through to the existing ambiguous-prefix error in {@link applyChangelogOverrides}.
103
- *
104
- * Pure: never mutates inputs.
87
+ * A workspace key identical to a root key replaces the root entry entirely, without a field-level merge. Distinct keys
88
+ * that resolve to the same item do not shadow each other here: {@link applyChangelogOverrides} reports them as
89
+ * overlapping.
105
90
  */
106
91
  export declare function composeOverrides(rootEntries: Map<string, ChangelogOverride>, workspaceEntries: Map<string, ChangelogOverride> | undefined): Map<string, ChangelogOverride>;
107
92
  /**
108
- * Shared override-application context threaded through the per-workspace and project apply
109
- * sites. Bundles the loaded per-scope maps with the run-scoped warning aggregators so each
110
- * call site can both consume the maps it needs and report into the same warning channel.
93
+ * The loaded per-scope override maps and the run's warning aggregators, shared by every apply of a prepare run.
111
94
  *
112
- * `globalMatchedRootKeys` tracks ROOT keys matched in any apply call. Workspace-sourced
113
- * matched keys are not added here — they're tracked locally per workspace because their
114
- * stale-key semantics are local (a workspace key that doesn't match in its own workspace is
115
- * unambiguously stale and is warned immediately, not aggregated to end-of-run).
95
+ * `globalMatchedRootKeys` contains the root keys matched in any apply. Workspace keys are not added: A workspace key
96
+ * that matches nothing in its own workspace is stale, and it is warned about at once rather than at the end of the run.
116
97
  */
117
98
  export interface OverrideContext {
118
99
  project: Map<string, ChangelogOverride>;
@@ -121,18 +102,25 @@ export interface OverrideContext {
121
102
  globalMatchedRootKeys: Set<string>;
122
103
  }
123
104
  /**
124
- * Load all per-scope override files (root + every workspace) in one upfront pass, validate
125
- * them, and bundle the results with run-scoped warning aggregators. Aborts the release with
126
- * a clear error when any file is malformed — checked-in editorial config that does not parse
127
- * is a bug, and we want the prepare run to fail before any workspace begins writing.
105
+ * Loads and validates the override files of the root and every workspace, and bundles them with empty warning
106
+ * aggregators. Throws if any file fails to load, so that a malformed checked-in override file stops `prepare` before
107
+ * any workspace writes.
128
108
  */
129
109
  export declare function createOverrideContext(workspaces: WorkspaceConfig[]): OverrideContext;
110
+ /** A changelog item as override matching sees it. `entry` is absent for a title-derived item. */
111
+ export interface OverrideTargetItem {
112
+ hash: string;
113
+ entry?: number;
114
+ }
130
115
  /** Per-scope input to {@link validateAllChangelogOverrides}. */
131
116
  export interface ChangelogOverrideScope {
132
- /** Path to the override file (relative to the repo root). Used to load the file and to attribute findings. */
117
+ /**
118
+ * Path to the override file, used to load the file and to attribute findings. A relative path resolves against the
119
+ * process working directory.
120
+ */
133
121
  filePath: string;
134
- /** Commit hashes in this scope's history window. Each override key is matched against these. */
135
- hashes: readonly string[];
122
+ /** Items in this scope's history window. Each override key is matched against these. */
123
+ items: readonly OverrideTargetItem[];
136
124
  }
137
125
  /** Inputs to {@link validateAllChangelogOverrides}. */
138
126
  export interface ValidateAllChangelogOverridesInputs {
@@ -141,56 +129,47 @@ export interface ValidateAllChangelogOverridesInputs {
141
129
  *
142
130
  * The file at `filePath` is loaded once and used in two ways:
143
131
  * - Its overrides are composed into every workspace's apply (root-tier overrides apply globally).
144
- * - When `hashes` is provided, the project map is also applied directly to that hash universe
132
+ * - When `items` is provided, the project map is also applied directly to those items
145
133
  * (project release in monorepo mode, or the package's history in single-package mode).
146
134
  *
147
- * Omit when no project file exists (rare — most repos have a root file even if empty).
135
+ * Omit when the project file doesn't exist.
148
136
  */
149
137
  project?: {
150
138
  filePath: string;
151
- hashes?: readonly string[];
139
+ items?: readonly OverrideTargetItem[];
152
140
  };
153
- /** Per-workspace scopes. Each workspace's file applies only to its own hash universe. */
141
+ /** Per-workspace scopes. Each workspace's file applies only to its own items. */
154
142
  workspaces?: readonly ChangelogOverrideScope[];
155
143
  }
156
- /** Result of {@link validateAllChangelogOverrides}: aggregated errors and warnings, each prefixed with the file path it pertains to. */
144
+ /**
145
+ * Result of {@link validateAllChangelogOverrides}: aggregated errors and warnings, each prefixed with the path of the
146
+ * file to which it pertains.
147
+ */
157
148
  export interface ValidateAllChangelogOverridesResult {
158
149
  errors: string[];
159
150
  warnings: string[];
160
151
  }
161
152
  /**
162
- * End-to-end health check across every override file and scope. Pure: takes already-collected
163
- * hash universes and returns aggregated findings. The CLI command and any other consumer
164
- * (programmatic library callers, future composite checks) wrap this with discovery and I/O.
153
+ * Validates every override file against the items of its scopes and returns the aggregated findings. It reads the
154
+ * override files but doesn't collect any items: The caller supplies each scope's items.
155
+ *
156
+ * Items built by `buildChangelogEntries`, as `release-kit prepare` builds them, yield the matches that `prepare`
157
+ * computes, tier asymmetry included: A workspace-tier key is stale when it matches nothing in its own workspace, and a
158
+ * root-tier key only when it matches nothing in any scope, the project release window included.
165
159
  *
166
- * When invoked via the standard `validateOverridesCommand` entry point, each scope's
167
- * `hashes` is built by `buildChangelogEntries` — the same path `release-kit prepare` walks —
168
- * so the match-set is byte-equal to what `prepare` would compute. The tier asymmetry is part
169
- * of that contract: workspace-tier keys are stale if they don't match in their own workspace;
170
- * root-tier keys are stale only if they don't match in any scope (no workspace AND not the
171
- * project release window). Library callers that construct `inputs` directly are responsible
172
- * for supplying the same hash universes if they want this guarantee.
160
+ * Every returned string is prefixed with the path of the override file to which it pertains.
173
161
  *
174
- * Every returned string is prefixed with the relative override-file path it pertains to so
175
- * consumers can locate the offending file without further structuring.
162
+ * A relative `filePath`, in any scope, resolves against the process working directory, not against the repo root.
163
+ * A caller running from elsewhere passes absolute paths or changes directory first.
176
164
  */
177
165
  export declare function validateAllChangelogOverrides(inputs: ValidateAllChangelogOverridesInputs): ValidateAllChangelogOverridesResult;
178
166
  /**
179
- * Apply the composed (root + workspace) override map to a workspace's changelog entries and
180
- * report stale-key warnings tier-by-tier.
181
- *
182
- * The composed map is applied in a single {@link applyChangelogOverrides} call. Matched keys
183
- * are demultiplexed by source via `Map.has` lookups on the original (uncomposed) maps so the
184
- * stale-key semantics stay tier-aware:
185
- *
186
- * - Workspace-sourced keys (those present in the per-workspace map, regardless of whether
187
- * the root map also has the same byte-equal key) that did NOT match are unambiguously
188
- * stale in their own apply context — push an immediate stale warning naming the key.
189
- * - Root-sourced matched keys (present in root, absent from this workspace's map) are added
190
- * to `globalMatchedRootKeys` so the orchestrator's end-of-run loop can dedupe across
191
- * batches and warn only on root keys that matched nowhere.
167
+ * Applies the composed root and workspace override map to a workspace's changelog entries, and records stale keys by
168
+ * tier.
192
169
  *
193
- * Throws when any apply call surfaces an error (e.g. ambiguous prefix). Returns the applied
194
- * result so the caller can consume `applied.entries` for downstream rendering.
170
+ * A workspace key that matched nothing, whether or not the root map has the same key, is stale, and it is warned about
171
+ * at once. The function adds each root key that matched without being shadowed to `globalMatchedRootKeys`, from which
172
+ * the caller warns, at the end of the run, about the root keys that matched nowhere. Throws if applying reports any
173
+ * error, such as an ambiguous prefix.
195
174
  */
196
175
  export declare function applyWorkspaceOverrides(newEntries: ChangelogEntry[], workspacePath: string, overrideContext: OverrideContext): ReturnType<typeof applyChangelogOverrides>;
@@ -6,7 +6,9 @@ import { isRecord } from "./typeGuards.js";
6
6
  const OVERRIDES_FILENAME = '.meta/changelog-overrides.json';
7
7
  const VALID_AUDIENCE_VALUES = new Set(['all', 'dev', 'skip']);
8
8
  const V1_SUPPORTED_AUDIENCE_VALUES = new Set(['skip']);
9
- const KNOWN_OVERRIDE_FIELDS = new Set(['audience', 'description', 'body', 'breaking']);
9
+ export const KNOWN_OVERRIDE_FIELDS = new Set(['audience', 'description', 'body', 'breaking']);
10
+ export const OVERRIDE_KEY_PATTERN = /^[0-9a-f]+(:[1-9][0-9]*)?$/;
11
+ const SCHEMA_KEY = '$schema';
10
12
  export function loadChangelogOverrides(path) {
11
13
  if (!existsSync(path)) {
12
14
  return { overrides: new Map() };
@@ -43,8 +45,14 @@ export function validateChangelogOverrides(raw) {
43
45
  return { overrides, errors };
44
46
  }
45
47
  for (const [key, rawEntry] of Object.entries(raw)) {
46
- if (key === '') {
47
- errors.push('Override file: empty-string key is not a valid commit hash');
48
+ if (key === SCHEMA_KEY) {
49
+ if (typeof rawEntry !== 'string') {
50
+ errors.push(`Override file: '${SCHEMA_KEY}' must be a string`);
51
+ }
52
+ continue;
53
+ }
54
+ if (!OVERRIDE_KEY_PATTERN.test(key)) {
55
+ errors.push(`overrides['${key}']: key must be a lowercase hex commit hash or prefix, optionally followed by ':<n>'`);
48
56
  continue;
49
57
  }
50
58
  const validated = validateSingleOverride(key, rawEntry, errors);
@@ -54,6 +62,13 @@ export function validateChangelogOverrides(raw) {
54
62
  }
55
63
  return { overrides, errors };
56
64
  }
65
+ export function parseOverrideKey(key) {
66
+ const separatorIndex = key.indexOf(':');
67
+ if (separatorIndex === -1) {
68
+ return { hashPrefix: key };
69
+ }
70
+ return { hashPrefix: key.slice(0, separatorIndex), entry: Number(key.slice(separatorIndex + 1)) };
71
+ }
57
72
  function validateSingleOverride(key, rawEntry, errors) {
58
73
  if (!isRecord(rawEntry)) {
59
74
  errors.push(`overrides['${key}']: must be an object`);
@@ -125,53 +140,27 @@ function validateAudience(key, value, errors) {
125
140
  return 'skip';
126
141
  }
127
142
  export function formatStaleOverrideKeyWarning(key) {
128
- return `Override key '${key}' did not match any commit hash in the changelog (likely a stale reference)`;
143
+ return `Override key '${key}' did not match any item in the changelog (likely a stale reference)`;
129
144
  }
130
145
  export function applyChangelogOverrides(entries, overrides) {
131
146
  const warnings = [];
132
- const errors = [];
133
- const matchedKeys = [];
134
147
  if (overrides.size === 0) {
135
- return { entries: entries.map(cloneEntry), warnings, errors, matchedKeys };
136
- }
137
- const allHashes = [];
138
- for (const entry of entries) {
139
- for (const section of entry.sections) {
140
- for (const item of section.items) {
141
- if (item.hash !== undefined) {
142
- allHashes.push(item.hash);
143
- }
144
- }
145
- }
148
+ return { entries: entries.map(cloneEntry), warnings, errors: [], matchedKeys: [] };
146
149
  }
147
- const keyToMatchedHashes = new Map();
148
- for (const overrideKey of overrides.keys()) {
149
- const matches = allHashes.filter((hash) => hash.startsWith(overrideKey));
150
- if (matches.length === 0) {
151
- continue;
152
- }
153
- if (matches.length > 1) {
154
- errors.push(`Override key '${overrideKey}' is ambiguous: matches multiple commits (${matches.join(', ')}). ` +
155
- 'Use a longer prefix or the full commit hash.');
156
- continue;
157
- }
158
- keyToMatchedHashes.set(overrideKey, matches);
159
- matchedKeys.push(overrideKey);
160
- }
161
- const hashToOverride = new Map();
162
- for (const [overrideKey, matchedHashes] of keyToMatchedHashes) {
150
+ const { errors, itemIdToKey } = resolveOverrideKeys(indexEntryPositions(entries), overrides);
151
+ const itemIdToOverride = new Map();
152
+ for (const [itemId, overrideKey] of itemIdToKey) {
163
153
  const override = overrides.get(overrideKey);
164
- if (override === undefined)
165
- continue;
166
- for (const hash of matchedHashes) {
167
- hashToOverride.set(hash, override);
154
+ if (override !== undefined) {
155
+ itemIdToOverride.set(itemId, override);
168
156
  }
169
157
  }
158
+ const matchedKeys = [...new Set(itemIdToKey.values())];
170
159
  const transformedEntries = [];
171
160
  for (const entry of entries) {
172
161
  const transformedSections = [];
173
162
  for (const section of entry.sections) {
174
- const transformedItems = applyOverridesToItems(section.items, hashToOverride);
163
+ const transformedItems = applyOverridesToItems(section.items, itemIdToOverride);
175
164
  if (transformedItems.length === 0) {
176
165
  continue;
177
166
  }
@@ -181,14 +170,93 @@ export function applyChangelogOverrides(entries, overrides) {
181
170
  }
182
171
  return { entries: transformedEntries, warnings, errors, matchedKeys };
183
172
  }
184
- function applyOverridesToItems(items, hashToOverride) {
173
+ function resolveOverrideKeys(positionsByHash, overrides) {
174
+ const errors = [];
175
+ const failedKeys = new Set();
176
+ const itemIdToKey = new Map();
177
+ for (const [overrideKey, override] of overrides) {
178
+ const resolution = resolveOverrideKey(overrideKey, positionsByHash);
179
+ if (resolution.kind === 'ambiguous') {
180
+ errors.push(`Override key '${overrideKey}' is ambiguous: matches multiple commits (${resolution.hashes.join(', ')}). ` +
181
+ 'Use a longer prefix or the full commit hash.');
182
+ continue;
183
+ }
184
+ if (resolution.kind === 'none') {
185
+ continue;
186
+ }
187
+ const { hash, positions } = resolution;
188
+ if (parseOverrideKey(overrideKey).entry === undefined && positions.length > 1) {
189
+ const fieldsSet = ['description', 'body'].filter((field) => override[field] !== undefined);
190
+ if (fieldsSet.length > 0) {
191
+ const ordinalKeys = positions.map((position) => `${overrideKey}:${position}`).join(', ');
192
+ errors.push(`Override key '${overrideKey}' sets ${fieldsSet.join(' and ')} on a commit with several items; use ${ordinalKeys}`);
193
+ continue;
194
+ }
195
+ }
196
+ for (const position of positions) {
197
+ const itemId = formatItemId(hash, position);
198
+ const claimingKey = itemIdToKey.get(itemId);
199
+ if (claimingKey === undefined) {
200
+ itemIdToKey.set(itemId, overrideKey);
201
+ continue;
202
+ }
203
+ const target = position === undefined ? `commit ${hash}` : `the item at ${hash}:${position}`;
204
+ errors.push(`Override keys '${claimingKey}' and '${overrideKey}' both match ${target}; keep one`);
205
+ failedKeys.add(claimingKey);
206
+ failedKeys.add(overrideKey);
207
+ }
208
+ }
209
+ for (const [itemId, overrideKey] of itemIdToKey) {
210
+ if (failedKeys.has(overrideKey)) {
211
+ itemIdToKey.delete(itemId);
212
+ }
213
+ }
214
+ return { errors, itemIdToKey };
215
+ }
216
+ function resolveOverrideKey(overrideKey, positionsByHash) {
217
+ const { hashPrefix, entry } = parseOverrideKey(overrideKey);
218
+ const hashes = positionsByHash
219
+ .keys()
220
+ .filter((hash) => hash.startsWith(hashPrefix))
221
+ .toArray();
222
+ const [hash] = hashes;
223
+ if (hash === undefined) {
224
+ return { kind: 'none' };
225
+ }
226
+ if (hashes.length > 1) {
227
+ return { kind: 'ambiguous', hashes };
228
+ }
229
+ const available = [...(positionsByHash.get(hash) ?? [])];
230
+ const positions = entry === undefined
231
+ ? available.toSorted((left, right) => (left ?? 0) - (right ?? 0))
232
+ : available.filter((position) => position === entry);
233
+ return positions.length === 0 ? { kind: 'none' } : { kind: 'items', hash, positions };
234
+ }
235
+ function indexEntryPositions(entries) {
236
+ return indexItemPositions(entries.flatMap((entry) => entry.sections.flatMap((section) => section.items)));
237
+ }
238
+ function indexItemPositions(items) {
239
+ const positionsByHash = new Map();
240
+ for (const item of items) {
241
+ if (item.hash === undefined)
242
+ continue;
243
+ const positions = positionsByHash.get(item.hash) ?? new Set();
244
+ positions.add(item.entry);
245
+ positionsByHash.set(item.hash, positions);
246
+ }
247
+ return positionsByHash;
248
+ }
249
+ function formatItemId(hash, entry) {
250
+ return entry === undefined ? hash : `${hash}:${entry}`;
251
+ }
252
+ function applyOverridesToItems(items, itemIdToOverride) {
185
253
  const result = [];
186
254
  for (const item of items) {
187
255
  if (item.hash === undefined) {
188
256
  result.push(cloneItem(item));
189
257
  continue;
190
258
  }
191
- const override = hashToOverride.get(item.hash);
259
+ const override = itemIdToOverride.get(formatItemId(item.hash, item.entry));
192
260
  if (override === undefined) {
193
261
  result.push(cloneItem(item));
194
262
  continue;
@@ -207,6 +275,8 @@ function applyOverrideToItem(item, override) {
207
275
  }
208
276
  if (override.body !== undefined) {
209
277
  result.body = override.body;
278
+ }
279
+ if (override.body !== undefined && item.entry === undefined) {
210
280
  const migration = extractMigration(override.body);
211
281
  if (migration === undefined) {
212
282
  delete result.migration;
@@ -288,7 +358,7 @@ export function validateAllChangelogOverrides(inputs) {
288
358
  const projectMap = loadScopeMap(projectFilePath, errors);
289
359
  const workspaceMaps = (inputs.workspaces ?? []).map((scope) => ({
290
360
  filePath: scope.filePath,
291
- hashes: scope.hashes,
361
+ items: scope.items,
292
362
  map: loadScopeMap(scope.filePath, errors),
293
363
  }));
294
364
  const globalMatchedRootKeys = new Set();
@@ -302,9 +372,9 @@ export function validateAllChangelogOverrides(inputs) {
302
372
  globalMatchedRootKeys,
303
373
  });
304
374
  }
305
- const projectHashes = inputs.project?.hashes;
306
- if (projectFilePath !== undefined && projectHashes !== undefined) {
307
- processProjectScope({ projectFilePath, projectMap, projectHashes, errors, globalMatchedRootKeys });
375
+ const projectItems = inputs.project?.items;
376
+ if (projectFilePath !== undefined && projectItems !== undefined) {
377
+ processProjectScope({ projectFilePath, projectMap, projectItems, errors, globalMatchedRootKeys });
308
378
  }
309
379
  if (projectFilePath !== undefined) {
310
380
  collectRootStaleWarnings(projectFilePath, projectMap, globalMatchedRootKeys, warnings);
@@ -313,39 +383,49 @@ export function validateAllChangelogOverrides(inputs) {
313
383
  }
314
384
  function processWorkspaceScope(args) {
315
385
  const { workspace, projectFilePath, projectMap, errors, warnings, globalMatchedRootKeys } = args;
316
- const { filePath, hashes, map } = workspace;
317
- const workspaceApplied = applyChangelogOverrides(makeValidationEntries(hashes), map);
386
+ const { filePath, items, map } = workspace;
387
+ const validationEntries = makeValidationEntries(items);
388
+ const positionsByHash = indexItemPositions(items);
389
+ const workspaceApplied = applyChangelogOverrides(validationEntries, map);
318
390
  for (const message of workspaceApplied.errors) {
319
391
  errors.push(prefixWithFilePath(filePath, message));
320
392
  }
321
393
  if (projectFilePath !== undefined && projectMap.size > 0) {
322
394
  const projectMinusShadowed = filterShadowedKeys(projectMap, map);
323
- const projectApplied = applyChangelogOverrides(makeValidationEntries(hashes), projectMinusShadowed);
395
+ const projectApplied = applyChangelogOverrides(validationEntries, projectMinusShadowed);
324
396
  for (const message of projectApplied.errors) {
325
397
  errors.push(prefixWithFilePath(projectFilePath, message));
326
398
  }
399
+ const perFileErrors = new Set([...workspaceApplied.errors, ...projectApplied.errors]);
400
+ const composedApplied = applyChangelogOverrides(validationEntries, composeOverrides(projectMap, map));
401
+ for (const message of composedApplied.errors) {
402
+ if (!perFileErrors.has(message)) {
403
+ errors.push(prefixWithFilePath(filePath, message));
404
+ }
405
+ }
327
406
  }
328
407
  for (const key of map.keys()) {
329
- if (!hasAnyMatch(key, hashes)) {
408
+ if (!hasAnyMatch(key, positionsByHash)) {
330
409
  warnings.push(formatWorkspaceStaleWarning(filePath, key));
331
410
  }
332
411
  }
333
412
  for (const key of projectMap.keys()) {
334
413
  if (map.has(key))
335
414
  continue;
336
- if (hasAnyMatch(key, hashes)) {
415
+ if (hasAnyMatch(key, positionsByHash)) {
337
416
  globalMatchedRootKeys.add(key);
338
417
  }
339
418
  }
340
419
  }
341
420
  function processProjectScope(args) {
342
- const { projectFilePath, projectMap, projectHashes, errors, globalMatchedRootKeys } = args;
343
- const applied = applyChangelogOverrides(makeValidationEntries(projectHashes), projectMap);
421
+ const { projectFilePath, projectMap, projectItems, errors, globalMatchedRootKeys } = args;
422
+ const applied = applyChangelogOverrides(makeValidationEntries(projectItems), projectMap);
344
423
  for (const message of applied.errors) {
345
424
  errors.push(prefixWithFilePath(projectFilePath, message));
346
425
  }
426
+ const positionsByHash = indexItemPositions(projectItems);
347
427
  for (const key of projectMap.keys()) {
348
- if (hasAnyMatch(key, projectHashes)) {
428
+ if (hasAnyMatch(key, positionsByHash)) {
349
429
  globalMatchedRootKeys.add(key);
350
430
  }
351
431
  }
@@ -357,8 +437,8 @@ function collectRootStaleWarnings(projectFilePath, projectMap, globalMatchedRoot
357
437
  }
358
438
  }
359
439
  }
360
- function hasAnyMatch(key, hashes) {
361
- return hashes.some((hash) => hash.startsWith(key));
440
+ function hasAnyMatch(key, positionsByHash) {
441
+ return resolveOverrideKey(key, positionsByHash).kind !== 'none';
362
442
  }
363
443
  function filterShadowedKeys(projectMap, workspaceMap) {
364
444
  const result = new Map();
@@ -382,7 +462,7 @@ function loadScopeMap(filePath, errors) {
382
462
  }
383
463
  return result.overrides;
384
464
  }
385
- function makeValidationEntries(hashes) {
465
+ function makeValidationEntries(items) {
386
466
  return [
387
467
  {
388
468
  version: '0.0.0',
@@ -391,7 +471,7 @@ function makeValidationEntries(hashes) {
391
471
  {
392
472
  title: 'Validation',
393
473
  audience: 'all',
394
- items: hashes.map((hash) => ({ description: '', hash })),
474
+ items: items.map((item) => ({ ...item, description: '' })),
395
475
  },
396
476
  ],
397
477
  },
@@ -401,10 +481,10 @@ function prefixWithFilePath(filePath, message) {
401
481
  return `${filePath}: ${message}`;
402
482
  }
403
483
  function formatWorkspaceStaleWarning(filePath, key) {
404
- return `${filePath}: Override key '${key}' did not match any commit in this workspace's history (likely a stale reference)`;
484
+ return `${filePath}: Override key '${key}' did not match any item in this workspace's history (likely a stale reference)`;
405
485
  }
406
486
  function formatRootStaleWarning(filePath, key) {
407
- return `${filePath}: Override key '${key}' did not match any commit in any scope (likely a stale reference)`;
487
+ return `${filePath}: Override key '${key}' did not match any item in any scope (likely a stale reference)`;
408
488
  }
409
489
  export function applyWorkspaceOverrides(newEntries, workspacePath, overrideContext) {
410
490
  const { project, perWorkspace, overrideWarnings, globalMatchedRootKeys } = overrideContext;
@@ -0,0 +1,36 @@
1
+ import { type ParseCommitMessageOptions } from './parseCommitMessage.js';
2
+ import type { Commit, WorkTypeConfig } from './types.js';
3
+ /**
4
+ * How a commit's title places it in a changelog: under a section header, excluded by design, or unreadable.
5
+ *
6
+ * `breaking` is the parser's flag under the breaking policies passed in, which a `BREAKING CHANGE:` footer can set.
7
+ */
8
+ export type ChangelogClassification = {
9
+ kind: 'header';
10
+ header: string;
11
+ type: string;
12
+ breaking: boolean;
13
+ } | {
14
+ kind: 'excluded';
15
+ } | {
16
+ kind: 'unparseable';
17
+ };
18
+ /**
19
+ * Classifies a commit by its title.
20
+ *
21
+ * Four gates decide, in order: A `release:` or `Merge` subject is excluded; a subject without a ticket-ID prefix is
22
+ * unparseable; a type that `parseCommitMessage` cannot resolve against `workTypes` is unparseable; and a type that
23
+ * `workTypes` excludes from the changelog is excluded. A commit that passes all four takes the header that its type
24
+ * declares.
25
+ *
26
+ * The function passes `options` to the parse, so a policy violation is reported for every ticketed commit whose type
27
+ * resolves, excluded types included.
28
+ */
29
+ export declare function classifyChangelogCommit(commit: Pick<Commit, 'hash' | 'message'>, workTypes: Record<string, WorkTypeConfig>, options?: ParseCommitMessageOptions): ChangelogClassification;
30
+ /**
31
+ * Reports whether a subject belongs to a commit that is never included in a changelog: a `release:` commit, which
32
+ * records version bumps, or a git merge commit, whose content the merged commits already report.
33
+ */
34
+ export declare function isNonChangeSubject(subject: string): boolean;
35
+ /** Reports whether a subject belongs to a `release:` commit, which records version bumps rather than a change. */
36
+ export declare function isReleaseSubject(subject: string): boolean;