@williamthorsen/release-kit 11.0.0 → 12.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 (69) hide show
  1. package/.meta/changelog.json +62 -2
  2. package/.readyup/kits/default.js +1 -17
  3. package/.readyup/manifest.json +4 -4
  4. package/CHANGELOG.md +40 -0
  5. package/README.md +25 -44
  6. package/dist/esm/attachChangelogDiagnostics.d.ts +4 -0
  7. package/dist/esm/attachChangelogDiagnostics.js +11 -0
  8. package/dist/esm/bin/release-kit.js +4 -3
  9. package/dist/esm/buildChangelogEntries.d.ts +59 -17
  10. package/dist/esm/buildChangelogEntries.js +194 -111
  11. package/dist/esm/buildReleaseSummary.js +2 -2
  12. package/dist/esm/changelogOverrides.d.ts +31 -13
  13. package/dist/esm/changelogOverrides.js +138 -58
  14. package/dist/esm/classifyChangelogCommit.d.ts +38 -0
  15. package/dist/esm/classifyChangelogCommit.js +33 -0
  16. package/dist/esm/decideRelease.d.ts +11 -37
  17. package/dist/esm/decideRelease.js +5 -27
  18. package/dist/esm/defaults.d.ts +1 -1
  19. package/dist/esm/defaults.js +3 -0
  20. package/dist/esm/determineBumpType.d.ts +11 -11
  21. package/dist/esm/determineBumpType.js +6 -7
  22. package/dist/esm/enumerateReleaseWindows.d.ts +50 -0
  23. package/dist/esm/enumerateReleaseWindows.js +124 -0
  24. package/dist/esm/generateChangelogs.d.ts +5 -23
  25. package/dist/esm/generateChangelogs.js +0 -14
  26. package/dist/esm/glyphs.d.ts +1 -1
  27. package/dist/esm/help/prepareHelp.d.ts +1 -1
  28. package/dist/esm/help/prepareHelp.js +4 -6
  29. package/dist/esm/index.d.ts +1 -1
  30. package/dist/esm/init/initCommand.js +1 -1
  31. package/dist/esm/init/scaffold.d.ts +0 -2
  32. package/dist/esm/init/scaffold.js +2 -30
  33. package/dist/esm/loadConfig.d.ts +1 -1
  34. package/dist/esm/loadConfig.js +1 -4
  35. package/dist/esm/parseChangeRecordBlock.d.ts +37 -0
  36. package/dist/esm/parseChangeRecordBlock.js +134 -0
  37. package/dist/esm/parseCommitMessage.d.ts +31 -6
  38. package/dist/esm/parseCommitMessage.js +6 -5
  39. package/dist/esm/prepareCommand.js +4 -12
  40. package/dist/esm/releasePrepare.d.ts +7 -8
  41. package/dist/esm/releasePrepare.js +53 -73
  42. package/dist/esm/releasePrepareMono.js +48 -85
  43. package/dist/esm/releasePrepareProject.d.ts +1 -1
  44. package/dist/esm/releasePrepareProject.js +19 -37
  45. package/dist/esm/reportPrepare.js +39 -28
  46. package/dist/esm/types.d.ts +99 -55
  47. package/dist/esm/types.js +1 -1
  48. package/dist/esm/validateConfig.js +4 -0
  49. package/dist/esm/validateOverridesCommand.d.ts +6 -5
  50. package/dist/esm/validateOverridesCommand.js +16 -23
  51. package/docs/changelogs.md +64 -25
  52. package/docs/configuration.md +2 -3
  53. package/docs/editorial-overrides.md +75 -16
  54. package/docs/project-releases.md +4 -4
  55. package/docs/releasing.md +13 -8
  56. package/docs/work-types.md +13 -9
  57. package/package.json +1 -2
  58. package/schemas/changelog-overrides.json +39 -0
  59. package/cliff.toml.template +0 -65
  60. package/dist/esm/collectPolicyViolations.d.ts +0 -15
  61. package/dist/esm/collectPolicyViolations.js +0 -12
  62. package/dist/esm/determineBumpFromCommits.d.ts +0 -17
  63. package/dist/esm/determineBumpFromCommits.js +0 -28
  64. package/dist/esm/getCommitsSinceTarget.d.ts +0 -21
  65. package/dist/esm/getCommitsSinceTarget.js +0 -66
  66. package/dist/esm/resolveCliffConfigPath.d.ts +0 -11
  67. package/dist/esm/resolveCliffConfigPath.js +0 -20
  68. package/dist/esm/runGitCliff.d.ts +0 -38
  69. package/dist/esm/runGitCliff.js +0 -25
@@ -1,145 +1,228 @@
1
1
  import { chainError } from '@williamthorsen/toolbelt.errors/candidate';
2
2
  import { extractVersion } from "./changelogJsonUtils.js";
3
- import { DEFAULT_BREAKING_POLICIES, DEFAULT_WORK_TYPES } from "./defaults.js";
3
+ import { classifyChangelogCommit, isNonChangeSubject, isReleaseSubject } from "./classifyChangelogCommit.js";
4
+ import { DEFAULT_BREAKING_POLICIES, DEFAULT_VERSION_PATTERNS, DEFAULT_WORK_TYPES } from "./defaults.js";
5
+ import { determineBumpType } from "./determineBumpType.js";
6
+ import { enumerateReleaseWindows } from "./enumerateReleaseWindows.js";
4
7
  import { extractMigration } from "./extractMigration.js";
5
- import { COMMIT_PREPROCESSOR_PATTERNS, parseCommitMessage, PIPE_SCOPE_SOURCE } from "./parseCommitMessage.js";
6
- import { resolveCliffConfigPath } from "./resolveCliffConfigPath.js";
7
- import { runGitCliff } from "./runGitCliff.js";
8
+ import { parseChangeRecordBlock, stripChangeRecordBlocks } from "./parseChangeRecordBlock.js";
9
+ import { COMMIT_PREPROCESSOR_PATTERNS, evaluateBreakingPolicy, PIPE_SCOPE_SOURCE, resolveType, } from "./parseCommitMessage.js";
8
10
  import { stripEmojiPrefix } from "./stripEmojiPrefix.js";
9
- import { isRecord, isUnknownArray } from "./typeGuards.js";
10
- const HTML_COMMENT_PREFIX_PATTERN = /^<!--[^>]*-->/;
11
+ const UNRELEASED_TAG = 'unreleased';
11
12
  const CANONICAL_SECTION_ORDER = new Map(Object.values(DEFAULT_WORK_TYPES).map((config, index) => [stripGroupDecorations(config.header), index]));
12
13
  function canonicalSectionPriority(title) {
13
14
  const index = CANONICAL_SECTION_ORDER.get(stripGroupDecorations(title));
14
15
  return index ?? Infinity;
15
16
  }
16
17
  export function stripGroupDecorations(group) {
17
- return stripEmojiPrefix(group.replace(HTML_COMMENT_PREFIX_PATTERN, ''));
18
+ return stripEmojiPrefix(group);
18
19
  }
19
20
  export function buildChangelogEntries(config, tag, options) {
20
- const resolvedConfigPath = resolveCliffConfigPath(config.cliffConfigPath, import.meta.url);
21
- const cliffArgs = ['--context', '--tag', tag];
22
- if (options?.tagPattern !== undefined) {
23
- cliffArgs.push('--tag-pattern', options.tagPattern);
24
- }
25
- const includePaths = options?.includePaths ?? [];
26
- for (const includePath of includePaths) {
27
- cliffArgs.push('--include-path', includePath);
28
- }
21
+ const history = readReleaseHistory(config, options);
22
+ return { entries: toChangelogEntries(history, tag), diagnostics: history.unreleased.diagnostics };
23
+ }
24
+ export function readReleaseHistory(config, options) {
29
25
  try {
30
- const contextJson = runGitCliff(resolvedConfigPath, cliffArgs);
31
- const releases = parseCliffContext(contextJson);
32
- const devOnlySections = new Set(config.changelogJson.devOnlySections);
33
- const workTypes = config.workTypes ?? DEFAULT_WORK_TYPES;
34
- const breakingPolicies = config.breakingPolicies ?? DEFAULT_BREAKING_POLICIES;
35
- return transformReleases(releases, devOnlySections, workTypes, breakingPolicies);
26
+ const windows = enumerateReleaseWindows({
27
+ ...(options.paths !== undefined && { paths: options.paths }),
28
+ tagPrefixes: options.tagPrefixes,
29
+ unreleasedTag: UNRELEASED_TAG,
30
+ });
31
+ const context = {
32
+ breakingPolicies: config.breakingPolicies ?? DEFAULT_BREAKING_POLICIES,
33
+ devOnlySections: new Set(config.changelogJson.devOnlySections.map(stripGroupDecorations)),
34
+ versionPatterns: config.versionPatterns ?? DEFAULT_VERSION_PATTERNS,
35
+ workTypes: config.workTypes ?? DEFAULT_WORK_TYPES,
36
+ };
37
+ return transformReleases(windows, context);
36
38
  }
37
39
  catch (error) {
38
- throw chainError(`Failed to build changelog entries for tag ${tag}`, error);
40
+ throw chainError(`Failed to read the release history for ${options.tagPrefixes.join(', ')}`, error);
39
41
  }
40
42
  }
41
- function parseCliffContext(json) {
42
- const parsed = JSON.parse(json);
43
- if (!isUnknownArray(parsed)) {
44
- throw new TypeError('Expected git-cliff --context output to be an array');
43
+ export function toChangelogEntries(history, tag) {
44
+ const { unreleased } = history;
45
+ if (unreleased.sections.length === 0) {
46
+ return [...history.releasedEntries];
45
47
  }
46
- return parsed.map(toCliffContextRelease);
48
+ return [
49
+ { version: extractVersion(tag), date: unreleased.date, sections: unreleased.sections },
50
+ ...history.releasedEntries,
51
+ ];
47
52
  }
48
- function toCliffContextRelease(value) {
49
- if (!isRecord(value)) {
50
- return {};
53
+ function transformReleases(windows, context) {
54
+ const [unreleasedWindow, baselineWindow] = windows;
55
+ const diagnostics = { malformedBlocks: [], policyViolations: [], undeclaredEntryTypes: [] };
56
+ const signals = [];
57
+ const unparseable = [];
58
+ let parsedCommitCount = 0;
59
+ const sectionMap = new Map();
60
+ const unreleasedCommits = unreleasedWindow?.commits ?? [];
61
+ for (const commit of unreleasedCommits) {
62
+ const reading = readCommit(commit, context, diagnostics);
63
+ for (const derived of reading.items) {
64
+ appendItem(sectionMap, derived.header, derived.item);
65
+ signals.push(derived.signal);
66
+ }
67
+ if (reading.items.length > 0) {
68
+ parsedCommitCount += 1;
69
+ }
70
+ else if (reading.isUnparseable) {
71
+ unparseable.push(commit);
72
+ }
51
73
  }
52
- const release = {};
53
- if (typeof value['version'] === 'string') {
54
- release.version = value['version'];
74
+ const releasedEntries = [];
75
+ for (const release of windows.slice(1)) {
76
+ const releaseSections = new Map();
77
+ for (const commit of release.commits) {
78
+ for (const derived of readCommit(commit, context, undefined).items) {
79
+ appendItem(releaseSections, derived.header, derived.item);
80
+ }
81
+ }
82
+ const sections = buildSections(releaseSections, context.devOnlySections);
83
+ if (sections.length > 0) {
84
+ releasedEntries.push({ version: extractVersion(release.version), date: formatDate(release.timestamp), sections });
85
+ }
55
86
  }
56
- if (typeof value['timestamp'] === 'number') {
57
- release.timestamp = value['timestamp'];
87
+ return {
88
+ previousTag: baselineWindow?.version,
89
+ releasedEntries,
90
+ unreleased: {
91
+ bump: determineBumpType(signals, context.workTypes, context.versionPatterns),
92
+ commits: unreleasedCommits.filter((commit) => !isReleaseSubject(commit.subject)).toReversed(),
93
+ date: formatDate(unreleasedWindow?.timestamp ?? Math.floor(Date.now() / 1_000)),
94
+ diagnostics,
95
+ parsedCommitCount,
96
+ sections: buildSections(sectionMap, context.devOnlySections),
97
+ unparseableCommits: unparseable.length > 0 ? unparseable.toReversed() : undefined,
98
+ },
99
+ };
100
+ }
101
+ function readCommit(commit, context, diagnostics) {
102
+ if (isNonChangeSubject(commit.subject)) {
103
+ return { items: [], isUnparseable: false };
58
104
  }
59
- if (isUnknownArray(value['commits'])) {
60
- release.commits = value['commits'].map(toCliffContextCommit);
105
+ const reading = parseChangeRecordBlock(commit.message);
106
+ if (reading.kind === 'malformed') {
107
+ diagnostics?.malformedBlocks.push({
108
+ commitHash: commit.hash,
109
+ commitSubject: commit.subject,
110
+ reason: reading.reason,
111
+ });
61
112
  }
62
- return release;
63
- }
64
- function toCliffContextCommit(value) {
65
- if (!isRecord(value)) {
66
- return { message: '' };
113
+ if (reading.kind === 'read' && reading.entries.length > 0) {
114
+ const items = reading.entries.flatMap((entry, entryIndex) => buildEntryItem(commit, entry, entryIndex + 1, reading.prNumber, context, diagnostics) ?? []);
115
+ return { items, isUnparseable: false };
67
116
  }
68
- const commit = {
69
- message: typeof value['message'] === 'string' ? value['message'] : '',
70
- };
71
- if (typeof value['group'] === 'string') {
72
- commit.group = value['group'];
117
+ const classification = classifyChangelogCommit(commit, context.workTypes, {
118
+ breakingPolicies: context.breakingPolicies,
119
+ ...(diagnostics !== undefined && {
120
+ onPolicyViolation: (violating, type, surface) => {
121
+ diagnostics.policyViolations.push({
122
+ commitHash: violating.hash,
123
+ commitSubject: violating.subject,
124
+ type,
125
+ surface,
126
+ });
127
+ },
128
+ }),
129
+ });
130
+ if (classification.kind !== 'header') {
131
+ return { items: [], isUnparseable: classification.kind === 'unparseable' && reading.kind !== 'malformed' };
73
132
  }
74
- if (typeof value['id'] === 'string') {
75
- commit.id = value['id'];
133
+ const item = buildTitleItem(commit, classification.breaking);
134
+ return {
135
+ items: [
136
+ { header: classification.header, item, signal: { type: classification.type, breaking: item.breaking === true } },
137
+ ],
138
+ isUnparseable: false,
139
+ };
140
+ }
141
+ function buildSections(sectionMap, devOnlySections) {
142
+ const sections = [];
143
+ for (const [title, items] of sectionMap) {
144
+ sections.push({
145
+ title,
146
+ audience: devOnlySections.has(stripGroupDecorations(title)) ? 'dev' : 'all',
147
+ items,
148
+ });
76
149
  }
77
- return commit;
150
+ return sections.toSorted((a, b) => canonicalSectionPriority(a.title) - canonicalSectionPriority(b.title));
78
151
  }
79
- function transformReleases(releases, devOnlySections, workTypes, breakingPolicies) {
80
- const entries = [];
81
- const devOnlyNormalised = new Set([...devOnlySections].map(stripGroupDecorations));
82
- for (const release of releases) {
83
- if (release.version === undefined) {
84
- continue;
85
- }
86
- const version = extractVersion(release.version);
87
- const date = release.timestamp !== undefined ? new Date(release.timestamp * 1_000).toISOString().slice(0, 10) : 'unreleased';
88
- const sectionMap = new Map();
89
- const commits = release.commits ?? [];
90
- for (const commit of commits) {
91
- const group = stripCommentPrefix(commit.group ?? 'Other');
92
- const description = extractDescription(commit.message);
93
- const body = extractBody(commit.message);
94
- const breaking = isBreakingUnderPolicy(commit, workTypes, breakingPolicies);
95
- let items = sectionMap.get(group);
96
- if (items === undefined) {
97
- items = [];
98
- sectionMap.set(group, items);
99
- }
100
- const item = { description };
101
- if (body !== undefined) {
102
- item.body = body;
103
- }
104
- if (breaking) {
105
- item.breaking = true;
106
- }
107
- const migration = extractMigration(body);
108
- if (migration !== undefined) {
109
- item.migration = migration;
110
- }
111
- if (commit.id !== undefined && commit.id !== '') {
112
- item.hash = commit.id;
113
- }
114
- items.push(item);
115
- }
116
- const sections = [];
117
- for (const [title, items] of sectionMap) {
118
- if (items.length === 0) {
119
- continue;
120
- }
121
- sections.push({
122
- title,
123
- audience: devOnlyNormalised.has(stripGroupDecorations(title)) ? 'dev' : 'all',
124
- items,
125
- });
126
- }
127
- sections.sort((a, b) => canonicalSectionPriority(a.title) - canonicalSectionPriority(b.title));
128
- if (sections.length > 0) {
129
- entries.push({ version, date, sections });
130
- }
152
+ function formatDate(timestamp) {
153
+ return new Date(timestamp * 1_000).toISOString().slice(0, 10);
154
+ }
155
+ function appendItem(sectionMap, header, item) {
156
+ let items = sectionMap.get(header);
157
+ if (items === undefined) {
158
+ items = [];
159
+ sectionMap.set(header, items);
131
160
  }
132
- return entries;
161
+ items.push(item);
133
162
  }
134
- function stripCommentPrefix(group) {
135
- return group.replace(HTML_COMMENT_PREFIX_PATTERN, '');
163
+ function buildEntryItem(commit, entry, position, prNumber, context, diagnostics) {
164
+ const { breakingPolicies, workTypes } = context;
165
+ const type = resolveType(entry.type, workTypes);
166
+ if (type === undefined) {
167
+ diagnostics?.undeclaredEntryTypes.push({
168
+ commitHash: commit.hash,
169
+ commitSubject: commit.subject,
170
+ entryPosition: position,
171
+ type: entry.type,
172
+ });
173
+ return undefined;
174
+ }
175
+ const config = workTypes[type];
176
+ if (config === undefined || config.excludedFromChangelog === true) {
177
+ return undefined;
178
+ }
179
+ const breaking = evaluateBreakingPolicy({
180
+ commit: { message: commit.message, subject: commit.subject, hash: commit.hash },
181
+ resolvedType: type,
182
+ hasPrefixBreaking: entry.breaking,
183
+ hasFooterBreaking: false,
184
+ policy: breakingPolicies[type] ?? 'optional',
185
+ prefixSurface: 'entry',
186
+ onPolicyViolation: diagnostics === undefined
187
+ ? undefined
188
+ : (violating, violatedType, surface) => {
189
+ diagnostics.policyViolations.push({
190
+ commitHash: violating.hash,
191
+ commitSubject: violating.subject,
192
+ type: violatedType,
193
+ surface,
194
+ entryPosition: position,
195
+ });
196
+ },
197
+ });
198
+ const item = {
199
+ description: prNumber === undefined ? entry.text : `${entry.text} (#${prNumber})`,
200
+ };
201
+ if (breaking) {
202
+ item.breaking = true;
203
+ }
204
+ if (entry.migration !== undefined) {
205
+ item.migration = entry.migration;
206
+ }
207
+ item.hash = commit.hash;
208
+ item.entry = position;
209
+ return { header: config.header, item, signal: { type, breaking } };
136
210
  }
137
- function isBreakingUnderPolicy(commit, workTypes, breakingPolicies) {
138
- if (!subjectHasBreakingMarker(commit.message)) {
139
- return false;
211
+ function buildTitleItem(commit, isParsedBreaking) {
212
+ const body = extractBody(stripChangeRecordBlocks(commit.message));
213
+ const item = { description: extractDescription(commit.message) };
214
+ if (body !== undefined) {
215
+ item.body = body;
216
+ }
217
+ if (isParsedBreaking && subjectHasBreakingMarker(commit.message)) {
218
+ item.breaking = true;
219
+ }
220
+ const migration = extractMigration(body);
221
+ if (migration !== undefined) {
222
+ item.migration = migration;
140
223
  }
141
- const parsed = parseCommitMessage(commit.message, commit.id ?? '', workTypes, undefined, { breakingPolicies });
142
- return parsed?.breaking ?? true;
224
+ item.hash = commit.hash;
225
+ return item;
143
226
  }
144
227
  const SUBJECT_BREAKING_MARKER_PATTERN = new RegExp(String.raw `^(?:${PIPE_SCOPE_SOURCE}\|)?\w+(?:\([^)]+\))?!:`);
145
228
  function subjectHasBreakingMarker(message) {
@@ -11,7 +11,7 @@ export function buildReleaseSummary(result) {
11
11
  }
12
12
  const lines = [workspace.tag];
13
13
  for (const commit of commits) {
14
- lines.push(`- ${stripScope(commit.message)}`);
14
+ lines.push(`- ${stripScope(commit.subject)}`);
15
15
  }
16
16
  sections.push(lines.join('\n'));
17
17
  }
@@ -19,7 +19,7 @@ export function buildReleaseSummary(result) {
19
19
  if (project !== undefined && project.status === 'released' && project.commits.length > 0) {
20
20
  const lines = [project.tag];
21
21
  for (const commit of project.commits) {
22
- lines.push(`- ${stripScope(commit.message)}`);
22
+ lines.push(`- ${stripScope(commit.subject)}`);
23
23
  }
24
24
  sections.push(lines.join('\n'));
25
25
  }
@@ -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>;
@@ -26,6 +30,11 @@ export declare function validateChangelogOverrides(raw: unknown): {
26
30
  overrides: Map<string, ChangelogOverride>;
27
31
  errors: string[];
28
32
  };
33
+ /** Split a valid override key into its hash prefix and, for an ordinal key, its 1-based entry position. */
34
+ export declare function parseOverrideKey(key: string): {
35
+ hashPrefix: string;
36
+ entry?: number;
37
+ };
29
38
  /**
30
39
  * Format the standard "stale override key" warning. Callers compute the set of stale keys
31
40
  * (keys that didn't match anywhere) and use this helper to surface them uniformly. Centralized
@@ -35,12 +44,16 @@ export declare function formatStaleOverrideKeyWarning(key: string): string;
35
44
  /**
36
45
  * Apply overrides to a `ChangelogEntry[]`, returning a new array. Pure: no mutation, no I/O.
37
46
  *
38
- * Match algorithm: each override key is treated as a string-prefix against `ChangelogItem.hash`.
47
+ * Match algorithm: each key's hash prefix is matched against the distinct `ChangelogItem.hash` values.
39
48
  * - 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.
49
+ * - 1 match → a bare key resolves to every item of that commit, and an ordinal key `<hash>:<n>` to the items whose
50
+ * `entry` is `n`. A key that resolves to no item is unmatched, so an ordinal key on a title-derived item goes stale.
41
51
  * - 2+ matches → error (ambiguous prefix).
42
52
  *
43
- * `matchedKeys` lists the override keys that resolved to exactly one item in this batch.
53
+ * Two more conditions are errors, and the keys involved apply nowhere: a bare key that sets `description` or `body` on a
54
+ * commit with several items, and two keys that resolve to the same item.
55
+ *
56
+ * `matchedKeys` lists the override keys that resolved to at least one item without error in this batch.
44
57
  * Callers are responsible for computing stale-key warnings: in a monorepo run, an override
45
58
  * may target a commit that lives in another workspace, so the per-batch zero-match signal is
46
59
  * insufficient on its own. Single-package and monorepo orchestrators format warnings using
@@ -98,8 +111,8 @@ export declare function loadOverridesForScopes(scopes: {
98
111
  *
99
112
  * Byte-equal-key shadowing: when a workspace key string-equals a root key, the workspace
100
113
  * 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}.
114
+ * Non-identical keys that resolve to the same item do NOT shadow here: {@link applyChangelogOverrides}
115
+ * reports them as overlapping.
103
116
  *
104
117
  * Pure: never mutates inputs.
105
118
  */
@@ -127,12 +140,17 @@ export interface OverrideContext {
127
140
  * is a bug, and we want the prepare run to fail before any workspace begins writing.
128
141
  */
129
142
  export declare function createOverrideContext(workspaces: WorkspaceConfig[]): OverrideContext;
143
+ /** A changelog item as override matching sees it. `entry` is absent for a title-derived item. */
144
+ export interface OverrideTargetItem {
145
+ hash: string;
146
+ entry?: number;
147
+ }
130
148
  /** Per-scope input to {@link validateAllChangelogOverrides}. */
131
149
  export interface ChangelogOverrideScope {
132
150
  /** Path to the override file (relative to the repo root). Used to load the file and to attribute findings. */
133
151
  filePath: string;
134
- /** Commit hashes in this scope's history window. Each override key is matched against these. */
135
- hashes: readonly string[];
152
+ /** Items in this scope's history window. Each override key is matched against these. */
153
+ items: readonly OverrideTargetItem[];
136
154
  }
137
155
  /** Inputs to {@link validateAllChangelogOverrides}. */
138
156
  export interface ValidateAllChangelogOverridesInputs {
@@ -141,16 +159,16 @@ export interface ValidateAllChangelogOverridesInputs {
141
159
  *
142
160
  * The file at `filePath` is loaded once and used in two ways:
143
161
  * - 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
162
+ * - When `items` is provided, the project map is also applied directly to those items
145
163
  * (project release in monorepo mode, or the package's history in single-package mode).
146
164
  *
147
165
  * Omit when no project file exists (rare — most repos have a root file even if empty).
148
166
  */
149
167
  project?: {
150
168
  filePath: string;
151
- hashes?: readonly string[];
169
+ items?: readonly OverrideTargetItem[];
152
170
  };
153
- /** Per-workspace scopes. Each workspace's file applies only to its own hash universe. */
171
+ /** Per-workspace scopes. Each workspace's file applies only to its own items. */
154
172
  workspaces?: readonly ChangelogOverrideScope[];
155
173
  }
156
174
  /** Result of {@link validateAllChangelogOverrides}: aggregated errors and warnings, each prefixed with the file path it pertains to. */
@@ -160,16 +178,16 @@ export interface ValidateAllChangelogOverridesResult {
160
178
  }
161
179
  /**
162
180
  * 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
181
+ * item universes and returns aggregated findings. The CLI command and any other consumer
164
182
  * (programmatic library callers, future composite checks) wrap this with discovery and I/O.
165
183
  *
166
184
  * When invoked via the standard `validateOverridesCommand` entry point, each scope's
167
- * `hashes` is built by `buildChangelogEntries` — the same path `release-kit prepare` walks —
185
+ * `items` is built by `buildChangelogEntries` — the same path `release-kit prepare` walks —
168
186
  * so the match-set is byte-equal to what `prepare` would compute. The tier asymmetry is part
169
187
  * of that contract: workspace-tier keys are stale if they don't match in their own workspace;
170
188
  * root-tier keys are stale only if they don't match in any scope (no workspace AND not the
171
189
  * project release window). Library callers that construct `inputs` directly are responsible
172
- * for supplying the same hash universes if they want this guarantee.
190
+ * for supplying the same item universes if they want this guarantee.
173
191
  *
174
192
  * Every returned string is prefixed with the relative override-file path it pertains to so
175
193
  * consumers can locate the offending file without further structuring.