@williamthorsen/release-kit 10.7.0 → 10.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/.meta/changelog.json +71 -3
  2. package/.readyup/kits/default.js +3 -3
  3. package/.readyup/kits/npm-auto-publish.js +1 -1
  4. package/.readyup/manifest.json +9 -9
  5. package/CHANGELOG.md +49 -3
  6. package/README.md +21 -15
  7. package/dist/esm/bin/release-kit.js +20 -102
  8. package/dist/esm/buildChangelogEntries.d.ts +1 -1
  9. package/dist/esm/buildChangelogEntries.js +14 -5
  10. package/dist/esm/createGithubReleaseCommand.d.ts +2 -1
  11. package/dist/esm/createGithubReleaseCommand.js +4 -2
  12. package/dist/esm/createTags.d.ts +2 -0
  13. package/dist/esm/createTags.js +5 -4
  14. package/dist/esm/format.d.ts +5 -5
  15. package/dist/esm/format.js +5 -4
  16. package/dist/esm/glyphs.d.ts +3 -0
  17. package/dist/esm/glyphs.js +6 -0
  18. package/dist/esm/init/initCommand.d.ts +3 -1
  19. package/dist/esm/init/initCommand.js +15 -15
  20. package/dist/esm/loadValidatedConfig.d.ts +2 -1
  21. package/dist/esm/loadValidatedConfig.js +3 -3
  22. package/dist/esm/parseCommitMessage.d.ts +4 -5
  23. package/dist/esm/prepareCommand.d.ts +2 -1
  24. package/dist/esm/prepareCommand.js +17 -16
  25. package/dist/esm/publishCommand.d.ts +2 -1
  26. package/dist/esm/publishCommand.js +2 -2
  27. package/dist/esm/reportPrepare.d.ts +2 -0
  28. package/dist/esm/reportPrepare.js +44 -38
  29. package/dist/esm/resolveReleaseNotesConfig.d.ts +2 -1
  30. package/dist/esm/resolveReleaseNotesConfig.js +4 -4
  31. package/dist/esm/resolveStylesOrExit.d.ts +10 -0
  32. package/dist/esm/resolveStylesOrExit.js +10 -0
  33. package/dist/esm/showTagPrefixesCommand.d.ts +2 -1
  34. package/dist/esm/showTagPrefixesCommand.js +16 -10
  35. package/dist/esm/sync-labels/generateCommand.d.ts +4 -2
  36. package/dist/esm/sync-labels/generateCommand.js +4 -4
  37. package/dist/esm/sync-labels/initCommand.d.ts +3 -1
  38. package/dist/esm/sync-labels/initCommand.js +7 -7
  39. package/dist/esm/tagCommand.d.ts +2 -1
  40. package/dist/esm/tagCommand.js +2 -2
  41. package/dist/esm/types.d.ts +8 -7
  42. package/dist/esm/validateOverridesCommand.d.ts +4 -4
  43. package/dist/esm/validateOverridesCommand.js +6 -6
  44. package/dist/esm/workTypesData.d.ts +10 -9
  45. package/dist/esm/workTypesData.js +97 -11
  46. package/docs/changelogs.md +8 -8
  47. package/docs/configuration.md +23 -1
  48. package/docs/work-types.md +28 -30
  49. package/package.json +11 -11
  50. package/presets/labels/common.yaml +2 -2
  51. package/dist/esm/checkWorkTypesDrift.d.ts +0 -36
  52. package/dist/esm/checkWorkTypesDrift.js +0 -117
  53. package/dist/esm/syncWorkTypes.d.ts +0 -28
  54. package/dist/esm/syncWorkTypes.js +0 -94
  55. package/dist/esm/work-types.json +0 -133
  56. package/dist/esm/work-types.schema.json +0 -100
  57. package/dist/esm/workTypesUtils.d.ts +0 -22
  58. package/dist/esm/workTypesUtils.js +0 -16
@@ -1,15 +1,13 @@
1
1
  /**
2
- * Canonical work-types data — runtime mirror of `work-types.json`.
2
+ * Canonical work-types data: the runtime mirror of `work-types.json`.
3
3
  *
4
- * `work-types.json` is the canonical SSOT for the taxonomy and is the file
5
- * `release-kit work-types check` / `:sync` operate on. This `.ts` mirror exists for
6
- * runtime consumption: ESM JSON imports require an `import attributes` clause that
7
- * esbuild's `bundle: false` mode strips, so a plain TS module is the most reliable way
8
- * to ship the data to consumers.
4
+ * `work-types.json` is release-kit's copy of the codeassembly canonical. This `.ts` mirror exists for
5
+ * runtime consumption: The compiler baseline does not enable JSON module imports, and a TS constant keeps
6
+ * `breakingPolicy` typed as its three-value union.
9
7
  *
10
- * A drift test (`workTypesData.unit.test.ts`) asserts that this constant deep-equals the
11
- * parsed contents of `work-types.json`. To change the taxonomy, edit BOTH files (or run
12
- * `nmr work-types:sync` to pull from upstream).
8
+ * `nmr work-types:sync` updates only the JSON, so copy its change here by hand. A drift test
9
+ * (`workTypesData.unit.test.ts`) asserts that this constant deep-equals the parsed contents of
10
+ * `work-types.json`.
13
11
  */
14
12
  /** Schema for a single entry. */
15
13
  export interface WorkTypeEntry {
@@ -18,6 +16,7 @@ export interface WorkTypeEntry {
18
16
  aliases: string[];
19
17
  emoji: string;
20
18
  label: string;
19
+ description: string;
21
20
  breakingPolicy: 'forbidden' | 'optional' | 'required';
22
21
  excludedFromChangelog?: boolean;
23
22
  }
@@ -34,6 +33,8 @@ export interface MarkerEntry {
34
33
  }
35
34
  /** Schema for the full data. */
36
35
  export interface WorkTypesData {
36
+ /** Semantic version of the data shape, as declared by the upstream canonical. */
37
+ version: string;
37
38
  tiers: string[];
38
39
  types: WorkTypeEntry[];
39
40
  /**
@@ -1,18 +1,60 @@
1
1
  export const WORK_TYPES_DATA = {
2
+ version: '1.1.0',
2
3
  tiers: ['public', 'internal', 'process'],
3
4
  types: [
4
- { tier: 'public', key: 'feat', aliases: ['feature'], emoji: '🎉', label: 'Features', breakingPolicy: 'optional' },
5
- { tier: 'public', key: 'drop', aliases: [], emoji: '🪦', label: 'Removed', breakingPolicy: 'required' },
6
- { tier: 'public', key: 'deprecate', aliases: [], emoji: '🗑️', label: 'Deprecated', breakingPolicy: 'forbidden' },
7
- { tier: 'public', key: 'fix', aliases: ['bugfix'], emoji: '🐛', label: 'Bug fixes', breakingPolicy: 'forbidden' },
8
- { tier: 'public', key: 'sec', aliases: ['security'], emoji: '🔒', label: 'Security', breakingPolicy: 'optional' },
5
+ {
6
+ tier: 'public',
7
+ key: 'feat',
8
+ aliases: ['feature'],
9
+ emoji: '🎉',
10
+ label: 'Features',
11
+ description: 'A change that gives consumers a new capability or extends an existing one.',
12
+ breakingPolicy: 'optional',
13
+ },
14
+ {
15
+ tier: 'public',
16
+ key: 'drop',
17
+ aliases: [],
18
+ emoji: '🪦',
19
+ label: 'Removed',
20
+ description: 'A change that removes a capability or surface on which consumers depend.',
21
+ breakingPolicy: 'required',
22
+ },
23
+ {
24
+ tier: 'public',
25
+ key: 'deprecate',
26
+ aliases: [],
27
+ emoji: '🗑️',
28
+ label: 'Deprecated',
29
+ description: 'A change that marks a capability or surface for removal while the capability or surface keeps working.',
30
+ breakingPolicy: 'forbidden',
31
+ },
32
+ {
33
+ tier: 'public',
34
+ key: 'fix',
35
+ aliases: ['bugfix'],
36
+ emoji: '🐛',
37
+ label: 'Bug fixes',
38
+ description: 'A change that corrects behavior that consumers meet and that differs from what was intended.',
39
+ breakingPolicy: 'optional',
40
+ },
41
+ {
42
+ tier: 'public',
43
+ key: 'sec',
44
+ aliases: ['security'],
45
+ emoji: '🔒',
46
+ label: 'Security',
47
+ description: 'A change that closes a vulnerability or hardens a consumer-facing surface against attack.',
48
+ breakingPolicy: 'optional',
49
+ },
9
50
  {
10
51
  tier: 'public',
11
52
  key: 'perf',
12
53
  aliases: ['performance'],
13
54
  emoji: '⚡',
14
55
  label: 'Performance',
15
- breakingPolicy: 'forbidden',
56
+ description: 'A change that reduces the time, memory, or other resources that consumer-facing work uses, without changing its result.',
57
+ breakingPolicy: 'optional',
16
58
  },
17
59
  {
18
60
  tier: 'internal',
@@ -20,6 +62,7 @@ export const WORK_TYPES_DATA = {
20
62
  aliases: ['utility'],
21
63
  emoji: '🏗️',
22
64
  label: 'Internal features',
65
+ description: 'A change that adds or extends a capability that consumers do not use directly, such as a helper or an internal module.',
23
66
  breakingPolicy: 'forbidden',
24
67
  },
25
68
  {
@@ -28,19 +71,61 @@ export const WORK_TYPES_DATA = {
28
71
  aliases: [],
29
72
  emoji: '♻️',
30
73
  label: 'Refactoring',
74
+ description: 'A change that restructures existing code without changing its behavior.',
75
+ breakingPolicy: 'forbidden',
76
+ },
77
+ {
78
+ tier: 'internal',
79
+ key: 'tests',
80
+ aliases: ['test'],
81
+ emoji: '🧪',
82
+ label: 'Tests',
83
+ description: 'A change confined to tests, test fixtures, or test helpers, including one that unblocks a build or a type check.',
84
+ breakingPolicy: 'forbidden',
85
+ },
86
+ {
87
+ tier: 'process',
88
+ key: 'tooling',
89
+ aliases: [],
90
+ emoji: '⚙️',
91
+ label: 'Tooling',
92
+ description: 'A change to the tools used to develop the project: build, lint, formatting, and release configuration, and development scripts.',
93
+ breakingPolicy: 'forbidden',
94
+ },
95
+ {
96
+ tier: 'process',
97
+ key: 'ci',
98
+ aliases: [],
99
+ emoji: '👷',
100
+ label: 'CI',
101
+ description: 'A change to continuous-integration workflows and their configuration.',
102
+ breakingPolicy: 'forbidden',
103
+ },
104
+ {
105
+ tier: 'process',
106
+ key: 'deps',
107
+ aliases: ['dep'],
108
+ emoji: '📦',
109
+ label: 'Dependencies',
110
+ description: 'A change that adds, removes, or upgrades a dependency, including its lockfile and version-catalog entries.',
111
+ breakingPolicy: 'forbidden',
112
+ },
113
+ {
114
+ tier: 'process',
115
+ key: 'ai',
116
+ aliases: [],
117
+ emoji: '🤖',
118
+ label: 'Agentic support',
119
+ description: "A change to guidance on working in the repository itself, such as its `AGENTS.md` or a repo-local skill, or to the repository's agent configuration.",
31
120
  breakingPolicy: 'forbidden',
32
121
  },
33
- { tier: 'internal', key: 'tests', aliases: ['test'], emoji: '🧪', label: 'Tests', breakingPolicy: 'forbidden' },
34
- { tier: 'process', key: 'tooling', aliases: [], emoji: '⚙️', label: 'Tooling', breakingPolicy: 'forbidden' },
35
- { tier: 'process', key: 'ci', aliases: [], emoji: '👷', label: 'CI', breakingPolicy: 'forbidden' },
36
- { tier: 'process', key: 'deps', aliases: ['dep'], emoji: '📦', label: 'Dependencies', breakingPolicy: 'forbidden' },
37
- { tier: 'process', key: 'ai', aliases: [], emoji: '🤖', label: 'Agentic support', breakingPolicy: 'forbidden' },
38
122
  {
39
123
  tier: 'process',
40
124
  key: 'docs',
41
125
  aliases: ['doc'],
42
126
  emoji: '📚',
43
127
  label: 'Documentation',
128
+ description: 'A change to documentation for human readers, such as a README, a guide, or a code comment.',
44
129
  breakingPolicy: 'forbidden',
45
130
  },
46
131
  {
@@ -49,6 +134,7 @@ export const WORK_TYPES_DATA = {
49
134
  aliases: [],
50
135
  emoji: '🎨',
51
136
  label: 'Formatting',
137
+ description: 'A change to formatting alone, such as whitespace or line wrapping, that leaves the content unchanged.',
52
138
  breakingPolicy: 'forbidden',
53
139
  excludedFromChangelog: true,
54
140
  },
@@ -8,13 +8,13 @@ Neither `CHANGELOG.md` nor `.meta/changelog.json` reaches a published tarball on
8
8
 
9
9
  Each item under a section in `.meta/changelog.json` carries one required field and four 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. |
14
- | `body` | `string` | The commit body, with trailing trailer metadata stripped. |
15
- | `breaking` | `boolean` | Present and `true` where the commit subject carried the `!` prefix. 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, and the key on which an override entry matches. Absent on synthetic propagation entries. |
11
+ | Field | Type | Meaning |
12
+ | ------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
13
+ | `description` | `string` | The bullet headline, taken from the commit subject with the ticket ID and type prefix stripped. |
14
+ | `body` | `string` | The commit body, with trailing trailer metadata stripped. |
15
+ | `breaking` | `boolean` | Present and `true` where the commit subject carried the `!` prefix and the commit's 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, and the key on which an override entry matches. Absent on synthetic propagation entries. |
18
18
 
19
19
  Every optional field is omitted rather than emitted as `null`, so a consumer tests for presence.
20
20
 
@@ -32,7 +32,7 @@ A commit body states a migration step as a paragraph opening with the literal la
32
32
 
33
33
  Three properties are worth knowing:
34
34
 
35
- - **It is independent of `breaking`.** A `fix` cannot carry `!` under the default breaking policy, and a `fix` that tightens validation still imposes a migration. Filter on `migration` to find every step; filter on `breaking` to find every breaking change.
35
+ - **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.
36
36
  - **`body` keeps the paragraph.** The field is an extraction, not a move, so `CHANGELOG.md` and `.meta/changelog.json` go on agreeing.
37
37
  - **It is derived, not authored.** `migration` is not a field an override file can set (see [File shape](editorial-overrides.md#file-shape)); it is re-derived from whatever `body` an override installs, and cleared where that body carries no labeled paragraph. To change the migration text, override `body`.
38
38
 
@@ -1,6 +1,6 @@
1
1
  # Configuration
2
2
 
3
- The fields of `.config/release-kit.config.ts`, how a workspace declares its prior identities or a retired package, the rule against colliding tag prefixes, and the patterns that decide a bump.
3
+ The fields of `.config/release-kit.config.ts`, how a workspace declares its prior identities or a retired package, the rule against colliding tag prefixes, the patterns that decide a bump, and the environment variable that sets the output style.
4
4
 
5
5
  A minimal config file is in the [README](../README.md#configuration).
6
6
 
@@ -103,3 +103,25 @@ interface VersionPatterns {
103
103
  ```
104
104
 
105
105
  Default: `{ major: ['!'], minor: ['feat'] }`
106
+
107
+ ## Output style
108
+
109
+ release-kit prints in one of two styles. `rich` marks each status with an emoji, such as ✅ or 🟠. `plain` replaces each emoji with a word, such as `PASS` or `WARN`, which a reader of a log can search for. The style is decided separately for stdout and stderr, so redirecting one stream to a file leaves the other rich at a terminal.
110
+
111
+ The `RELEASE_KIT_OUTPUT_STYLE` environment variable sets the style for every command, `init` and `sync-labels init` included:
112
+
113
+ | Value | Effect |
114
+ | ------- | ------------------------------------------------------------------------ |
115
+ | `auto` | Detect the style of each stream. The same as leaving the variable unset. |
116
+ | `plain` | Print plain on both streams. |
117
+ | `rich` | Print rich on both streams, even in a pipe or under CI. |
118
+
119
+ Any other value is a usage error: release-kit reports it and exits with code 1.
120
+
121
+ The variable outranks detection. Under `auto`, release-kit defers to detection, which reads `CI`, then the stream, then `TERM`. A stream is plain when any of these holds, and rich otherwise:
122
+
123
+ 1. `CI` is set to anything other than an empty string or `false`.
124
+ 2. The stream is not a terminal, as when its output is piped or redirected.
125
+ 3. `TERM` is `linux`, the Linux virtual console, which draws no emoji.
126
+
127
+ The style governs emoji alone. Changelog section emoji are file content rather than terminal output, and they are unaffected.
@@ -1,17 +1,17 @@
1
1
  # Work types and tiers
2
2
 
3
- The taxonomy against which release-kit parses commits: its tiers, the breaking-change policy, section markers, customization, and the commands that compare it with its upstream.
3
+ The taxonomy against which release-kit parses commits: its tiers, the breaking-change policy, section markers, customization, and the maintainer scripts that keep release-kit's copy level with its upstream.
4
4
 
5
- The canonical taxonomy lives in `packages/release-kit/src/work-types.json` and is split into three tiers that drive section rendering and audience classification.
5
+ release-kit bundles a copy of the codeassembly canonical taxonomy in `packages/release-kit/src/work-types.json`, kept level by the [maintainer scripts](#maintaining-the-bundled-taxonomy). The taxonomy is split into three tiers that drive section rendering and audience classification.
6
6
 
7
7
  | Tier | Key | Header | Aliases | `!` policy |
8
8
  | -------- | ----------- | ------------------------- | ------------- | ------------ |
9
9
  | public | `feat` | 🎉 Features | `feature` | optional |
10
10
  | public | `drop` | 🪦 Removed | | **required** |
11
11
  | public | `deprecate` | 🗑️ Deprecated | | forbidden |
12
- | public | `fix` | 🐛 Bug fixes | `bugfix` | forbidden |
12
+ | public | `fix` | 🐛 Bug fixes | `bugfix` | optional |
13
13
  | public | `sec` | 🔒 Security | `security` | optional |
14
- | public | `perf` | ⚡ Performance | `performance` | forbidden |
14
+ | public | `perf` | ⚡ Performance | `performance` | optional |
15
15
  | internal | `internal` | 🏗️ Internal features | `utility` | forbidden |
16
16
  | internal | `refactor` | ♻️ Refactoring | | forbidden |
17
17
  | internal | `tests` | 🧪 Tests | `test` | forbidden |
@@ -38,41 +38,44 @@ Section render order is **tier order (`public` → `internal` → `process`), th
38
38
 
39
39
  Each work-type carries a `breakingPolicy` value:
40
40
 
41
- - `optional` (`feat`, `sec`) — `!` is allowed; both `type:` and `type!:` parse cleanly.
42
- - `forbidden` (most types) — `!` is a policy violation. The premise: types like `internal!`, `perf!`, `fix!` are contradictory; an internal change cannot break a consumer contract, a pure perf change preserves the contract, and a bug-fix is by definition not a contract change.
43
- - `required` (`drop`) — bare `drop:` is a policy violation; only `drop!:` is accepted. The premise: removing a feature always breaks consumers; the `!` form makes that explicit.
41
+ - `optional` (`feat`, `fix`, `sec`, `perf`): `!` is allowed; both `type:` and `type!:` parse cleanly. Any of these can break consumers, and the marker records when one does: A fix can break consumers who relied on the defective behavior, and a performance change can break a contract to achieve its gain.
42
+ - `forbidden` (`deprecate` and every `internal`- and `process`-tier type): `!` is a policy violation. Deprecating a surface keeps it working, and removing it is a `drop`. Internal- and process-tier work does not face consumers; a change that breaks consumers faces them and therefore takes a public-tier type.
43
+ - `required` (`drop`): Bare `drop:` is a policy violation; only `drop!:` is accepted. Removing a public surface always breaks consumers, and the `!` form makes that explicit.
44
44
 
45
- ### Two-tier policy enforcement
45
+ ### Policy enforcement
46
46
 
47
- The `!` policy operates at two distinct levels with different semantics:
47
+ `parseCommitMessage` enforces the `!` policy at release time and treats a violation as a warning. Commits already in the log cannot be rewritten, so a policy-violating commit is parsed using its canonical type with `breaking: false` (the `!` is dropped from the parse) and an `onPolicyViolation` callback fires. Callers (`decideRelease` etc.) can collect these warnings and surface them in the release report. A single legacy `internal!` in a year-old log does not block releases.
48
48
 
49
- - **Write-time** (commit-msg hook) — strict rejection. Policy violations are blocked at the gate where the author can act on them immediately. _Hook-based enforcement is tracked separately and is not yet shipped._
50
- - **Release-time** (`parseCommitMessage`) — tolerant warn-and-continue. Commits already in the log cannot be rewritten, so a policy-violating commit is parsed using its canonical type with `breaking: false` (the `!` is dropped from the parse) and a `onPolicyViolation` callback fires. Callers (`decideRelease` etc.) can collect these warnings and surface them in the release report. A single legacy `internal!` in a year-old log does not block releases.
51
-
52
- A `BREAKING CHANGE:` body footer on a `forbidden`-policy type triggers the same warning path as the prefix `!` does — the spirit of the policy is "internal/perf/etc. cannot be breaking", which must apply to both surfaces.
49
+ A `BREAKING CHANGE:` body footer on a `forbidden`-policy type triggers the same warning path as the prefix `!` does: A type that cannot be breaking is parsed as non-breaking whichever surface carries the signal.
53
50
 
54
51
  The release-prepare orchestrators (`releasePrepare`, `releasePrepareMono`, `releasePrepareProject`) apply `DEFAULT_BREAKING_POLICIES` automatically. Violations encountered while parsing each workspace's or project's commit window are collected onto the corresponding result's `policyViolations` field and rendered under the section in the prepare report:
55
52
 
56
53
  ```
57
54
  arrays
58
55
  Found 1 commits since arrays-v1.0.0
59
- ⚠️ 1 policy violation:
56
+ 🟠 1 policy violation:
60
57
  · def5678 'internal!: refactor cache' — type 'internal' at prefix surface
61
58
  Bumping versions (patch)...
62
59
  📦 1.0.0 → 1.0.1 (patch)
63
60
  ```
64
61
 
65
- To customize, set `breakingPolicies` in `release-kit.config.ts` — provide a partial map to override individual types, or `{}` to disable enforcement entirely (the parser falls back to `'optional'` for any missing type). Violations remain warnings, never failures.
62
+ To customize, set `breakingPolicies` in `release-kit.config.ts`. The map replaces the default policies rather than merging with them, and the parser treats any type that the map omits as `'optional'`: A config that changes one type lists every policy that it keeps, and `{}` disables enforcement entirely. Violations remain warnings, never failures.
66
63
 
67
64
  ## `🚨 **Breaking:**` bullet marker
68
65
 
69
- Items whose commit subject carries the `!` prefix (e.g. `feat!`, `drop!`, `feat(api)!`) are rendered with a `🚨 **Breaking:** ` prefix on the bullet:
66
+ Items whose commit subject carries the `!` prefix (e.g. `feat!`, `drop!`, `feat(api)!`) on a type whose policy permits it are rendered with a `🚨 **Breaking:** ` prefix on the bullet:
70
67
 
71
68
  ```markdown
72
69
  - 🚨 **Breaking:** Drop legacy /v1 endpoint
73
70
  ```
74
71
 
75
- Only the prefix `!` triggers this marker. A `BREAKING CHANGE:` body footer on its own does **not** retroactively mark a changelog item as breaking — the changelog signal is tied to the commit-prefix policy. This avoids surprise breaking-marker appearances for older commits written under earlier conventions.
72
+ The marker agrees with the version bump:
73
+
74
+ - A `forbidden`-policy type carrying `!`, such as `refactor!`, gets no marker, just as its `!` raises no bump; the prepare report lists it as a policy violation. An [editorial override](editorial-overrides.md) that sets `breaking: true` restores the marker for one entry.
75
+ - The configured `breakingPolicies` map, `{}` included, decides which types permit the marker.
76
+ - A commit whose type the parser cannot resolve has no policy, so its `!` prefix alone decides.
77
+
78
+ A `BREAKING CHANGE:` body footer on its own does **not** retroactively mark a changelog item as breaking, even on a type whose policy permits `!`; the changelog signal is tied to the commit prefix. This avoids surprise breaking-marker appearances for older commits written under earlier conventions.
76
79
 
77
80
  The emoji and label of this marker are sourced from the `markers.breaking` entry in `work-types.json` (see [Section markers](#section-markers)) so consumers that render their own breaking-changes section draw from the same SSOT.
78
81
 
@@ -100,22 +103,17 @@ Work types from your config are merged with these defaults by key — your entri
100
103
 
101
104
  The default `devOnlySections` (excluded from public release notes but still written to `CHANGELOG.md`) are derived from the `internal` and `process` tiers (excluding `fmt`). Override via `changelogJson.devOnlySections` in your config; matching is decorator-tolerant, so a bare-name override like `['Internal features']` keeps working against the emoji-prefixed and prefix-decorated default titles.
102
105
 
103
- ## `release-kit work-types`
106
+ ## Maintaining the bundled taxonomy
104
107
 
105
- Manage the canonical work-types taxonomy used by changelog and release-notes generation.
108
+ `src/workTypesData.ts` mirrors `src/work-types.json` for runtime use. Two maintainer scripts of the release-kit package compare that JSON with its codeassembly upstream and update it. They exist only in a checkout of this repository and are not part of the published `release-kit` CLI.
106
109
 
107
- The check is non-blocking initially: until codeassembly publishes its `work-types.json`, the upstream URL returns 404 and `check` exits 0 with a warning. CI flip to a blocking check is tracked as a follow-up once the upstream ships.
110
+ - `nmr work-types:check` compares the content of `src/work-types.json` with the upstream, ignoring the local `$schema` hint.
111
+ - `nmr work-types:sync` overwrites `src/work-types.json` with the upstream when their content differs.
108
112
 
109
- These commands are also exposed as `nmr work-types:check` / `nmr work-types:sync` from any package directory.
113
+ Both run from `packages/release-kit`, or from anywhere in the repository as `nmr -F @williamthorsen/release-kit work-types:check` and `nmr -F @williamthorsen/release-kit work-types:sync`. After `sync` writes the file, update `src/workTypesData.ts` to match; `workTypesData.unit.test.ts` fails until the two agree.
110
114
 
111
- ### Authenticated fetches
115
+ `check` exits 0 on a match, 1 on drift, 2 when the upstream fetch fails, and 3 when either file is not valid JSON or the upstream fails the shape check. When the upstream URL returns 404, `check` exits 0 with a warning.
112
116
 
113
- When the upstream codeassembly repo is private, both `check` and `sync` need a GitHub token to fetch the canonical `work-types.json`. Set `GITHUB_TOKEN` in the environment and the commands send `Authorization: Bearer <token>` automatically; without it, requests are unauthenticated and a private upstream will return 404.
114
-
115
- ```sh
116
- # Source from `gh auth` for local runs:
117
- export GITHUB_TOKEN=$(gh auth token)
118
- pnpm exec release-kit work-types check
119
- ```
117
+ ### Authenticated fetches
120
118
 
121
- The token needs `contents: read` on the codeassembly repo (fine-grained PAT scope) or the equivalent classic-PAT scope. A token without sufficient scope still produces a 404 — same response as a missing upstream — so a misconfigured token degrades to the transitional-warning path rather than failing loudly. CI wiring against private upstream is deferred until either codeassembly is publicly readable or a cross-repo PAT is provisioned as a workflow secret.
119
+ The upstream codeassembly repo is public, so `check` and `sync` need no token. When `GITHUB_TOKEN` is set in the environment, both scripts send it as `Authorization: Bearer <token>`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@williamthorsen/release-kit",
3
- "version": "10.7.0",
3
+ "version": "10.9.0",
4
4
  "description": "Version-bumping and changelog-generation toolkit for release workflows",
5
5
  "keywords": [
6
6
  "changelog",
@@ -51,20 +51,20 @@
51
51
  "schemas/**"
52
52
  ],
53
53
  "dependencies": {
54
- "@williamthorsen/nmr-core": "0.11.1",
55
- "@williamthorsen/toolbelt.errors": "0.7.0",
54
+ "@williamthorsen/nmr-core": "0.12.0",
55
+ "@williamthorsen/toolbelt.errors": "0.8.1",
56
56
  "glob": "13.0.6",
57
57
  "json-stringify-pretty-compact": "4.0.0",
58
58
  "semver": "7.8.5",
59
59
  "yaml": "2.9.0",
60
- "zod": "4.5.4"
60
+ "zod": "4.6.2"
61
61
  },
62
62
  "devDependencies": {
63
63
  "@types/semver": "7.8.0",
64
- "@williamthorsen/toolbelt.filesystem": "0.8.3",
65
- "@williamthorsen/toolbelt.testing": "0.5.1",
66
- "@williamthorsen/toolbelt.vitest": "0.9.0",
67
- "readyup": "0.36.0",
64
+ "@williamthorsen/toolbelt.filesystem": "0.9.1",
65
+ "@williamthorsen/toolbelt.testing": "0.6.1",
66
+ "@williamthorsen/toolbelt.vitest": "0.10.1",
67
+ "readyup": "0.38.0",
68
68
  "smol-toml": "1.8.0",
69
69
  "vitest": "4.1.11"
70
70
  },
@@ -76,9 +76,9 @@
76
76
  "registry": "https://registry.npmjs.org"
77
77
  },
78
78
  "scripts": {
79
- "build:post": "node scripts/copy-work-types.js && rdy compile",
79
+ "build:post": "rdy compile",
80
80
  "build:pre": "rdy verify",
81
- "work-types:check": "pnpm exec release-kit work-types check",
82
- "work-types:sync": "pnpm exec release-kit work-types sync"
81
+ "work-types:check": "node scripts/check-work-types.ts",
82
+ "work-types:sync": "node scripts/sync-work-types.ts"
83
83
  }
84
84
  }
@@ -4,7 +4,7 @@
4
4
  # Main categories
5
5
  - name: ai
6
6
  color: a2eeef
7
- description: Documentation and configs used by agents
7
+ description: Guidance on working in this repo, such as its AGENTS.md or a repo-local skill, or its agent config
8
8
  - name: breaking
9
9
  color: b60205
10
10
  description: Breaking API change or removal of functionality
@@ -16,7 +16,7 @@
16
16
  description: Marks functionality for future removal
17
17
  - name: documentation
18
18
  color: a2eeef
19
- description: Improvements or additions to documentation
19
+ description: Documentation for human readers, such as a README, a guide, or a code comment
20
20
  - name: feature
21
21
  color: '0075ca'
22
22
  description: Added or improved external functionality
@@ -1,36 +0,0 @@
1
- /** URL of the upstream canonical `work-types.json` published by codeassembly. */
2
- export declare const UPSTREAM_WORK_TYPES_URL = "https://raw.githubusercontent.com/williamthorsen/codeassembly/main/packages/agents/content/skills/_data/work-types.json";
3
- /** Outcome of a drift check, paired with a human-readable message. */
4
- export interface DriftCheckResult {
5
- /**
6
- * Process exit code semantics:
7
- * - `0` — match (or upstream missing/transitional, with a warning).
8
- * - `1` — drift detected.
9
- * - `2` — network error.
10
- * - `3` — schema mismatch (upstream JSON does not parse or fails the schema invariants).
11
- */
12
- exitCode: 0 | 1 | 2 | 3;
13
- message: string;
14
- }
15
- /** Minimal injection seam so unit tests can substitute a deterministic fetcher. */
16
- export interface CheckWorkTypesDriftDependencies {
17
- /** Absolute path of the local `work-types.json`. Defaults to the bundled file. */
18
- localPath?: string;
19
- /** HTTP fetcher. Defaults to global `fetch` (Node 18+). */
20
- fetch?: typeof globalThis.fetch;
21
- /** Override the upstream URL (used by tests; production callers should leave default). */
22
- upstreamUrl?: string;
23
- }
24
- /**
25
- * Compare the bundled `work-types.json` against the upstream codeassembly canonical and
26
- * report drift.
27
- *
28
- * Failure modes:
29
- * - Network error → exit 2 with a diagnostic.
30
- * - Upstream 404 → exit 0 with a transitional warning (codeassembly hasn't published the
31
- * JSON yet; the check is informational while migration is in flight).
32
- * - Schema mismatch → exit 3 with a diagnostic.
33
- * - Drift → exit 1 with a unified-diff-style message.
34
- * - Match → exit 0.
35
- */
36
- export declare function checkWorkTypesDrift(dependencies?: CheckWorkTypesDriftDependencies): Promise<DriftCheckResult>;
@@ -1,117 +0,0 @@
1
- import { readFileSync } from 'node:fs';
2
- import { dirname, resolve } from 'node:path';
3
- import { fileURLToPath } from 'node:url';
4
- import { formatErrorLine } from '@williamthorsen/nmr-core';
5
- import { describeError } from '@williamthorsen/toolbelt.errors';
6
- import { buildFetchInit, hasExpectedTopLevelShape } from "./workTypesUtils.js";
7
- export const UPSTREAM_WORK_TYPES_URL = 'https://raw.githubusercontent.com/williamthorsen/codeassembly/main/packages/agents/content/skills/_data/work-types.json';
8
- function resolveDefaultLocalPath() {
9
- const moduleDir = dirname(fileURLToPath(import.meta.url));
10
- return resolve(moduleDir, 'work-types.json');
11
- }
12
- export async function checkWorkTypesDrift(dependencies = {}) {
13
- const localPath = dependencies.localPath ?? resolveDefaultLocalPath();
14
- const fetcher = dependencies.fetch ?? globalThis.fetch;
15
- const url = dependencies.upstreamUrl ?? UPSTREAM_WORK_TYPES_URL;
16
- const localContent = readFileSync(localPath, 'utf8');
17
- let localJson;
18
- try {
19
- localJson = JSON.parse(localContent);
20
- }
21
- catch (error) {
22
- return {
23
- exitCode: 3,
24
- message: `Local work-types.json is not valid JSON: ${describeError(error)}`,
25
- };
26
- }
27
- const fetchInit = buildFetchInit();
28
- let response;
29
- try {
30
- response = fetchInit === undefined ? await fetcher(url) : await fetcher(url, fetchInit);
31
- }
32
- catch (error) {
33
- return {
34
- exitCode: 2,
35
- message: formatErrorLine(`Failed to fetch upstream work-types.json: ${describeError(error)}`),
36
- };
37
- }
38
- if (response.status === 404) {
39
- return {
40
- exitCode: 0,
41
- message: `Upstream work-types.json not yet published at ${url}; skipping drift check.`,
42
- };
43
- }
44
- if (!response.ok) {
45
- return {
46
- exitCode: 2,
47
- message: formatErrorLine(`Failed to fetch upstream work-types.json: HTTP ${response.status} ${response.statusText}`),
48
- };
49
- }
50
- const upstreamText = await response.text();
51
- let upstreamJson;
52
- try {
53
- upstreamJson = JSON.parse(upstreamText);
54
- }
55
- catch (error) {
56
- return {
57
- exitCode: 3,
58
- message: `Upstream work-types.json is not valid JSON: ${describeError(error)}`,
59
- };
60
- }
61
- if (!hasExpectedTopLevelShape(upstreamJson)) {
62
- return {
63
- exitCode: 3,
64
- message: 'Upstream work-types.json does not match the expected schema shape (missing `tiers` or `types`).',
65
- };
66
- }
67
- const normalisedLocal = stripLocalOnlyFields(localJson);
68
- if (deepEqual(normalisedLocal, upstreamJson)) {
69
- return {
70
- exitCode: 0,
71
- message: 'Local work-types.json matches upstream.',
72
- };
73
- }
74
- return {
75
- exitCode: 1,
76
- message: `Drift detected. Local and upstream work-types.json differ. Run \`nmr work-types:sync\` to update from upstream.\nLocal: ${localPath}\nUpstream: ${url}`,
77
- };
78
- }
79
- function stripLocalOnlyFields(value) {
80
- if (typeof value !== 'object' || value === null || Array.isArray(value)) {
81
- return value;
82
- }
83
- const record = { ...value };
84
- delete record['$schema'];
85
- return record;
86
- }
87
- function deepEqual(a, b) {
88
- if (a === b)
89
- return true;
90
- if (typeof a !== typeof b)
91
- return false;
92
- if (a === null || b === null)
93
- return a === b;
94
- if (Array.isArray(a) && Array.isArray(b)) {
95
- if (a.length !== b.length)
96
- return false;
97
- for (let i = 0; i < a.length; i++) {
98
- if (!deepEqual(a[i], b[i]))
99
- return false;
100
- }
101
- return true;
102
- }
103
- if (typeof a === 'object' && typeof b === 'object' && !Array.isArray(a) && !Array.isArray(b)) {
104
- const aRecord = { ...a };
105
- const bRecord = { ...b };
106
- const aKeys = Object.keys(aRecord);
107
- const bKeys = Object.keys(bRecord);
108
- if (aKeys.length !== bKeys.length)
109
- return false;
110
- for (const key of aKeys) {
111
- if (!deepEqual(aRecord[key], bRecord[key]))
112
- return false;
113
- }
114
- return true;
115
- }
116
- return false;
117
- }
@@ -1,28 +0,0 @@
1
- /** Outcome of a sync operation. */
2
- export interface SyncResult {
3
- /**
4
- * Process exit code semantics:
5
- * - `0` — sync succeeded (file may be unchanged).
6
- * - `2` — network error or local write failure.
7
- * - `3` — schema mismatch (upstream JSON does not parse or fails the shape check).
8
- */
9
- exitCode: 0 | 2 | 3;
10
- message: string;
11
- }
12
- /** Minimal injection seam so unit tests can substitute a deterministic fetcher. */
13
- export interface SyncWorkTypesDependencies {
14
- /** Absolute path of the local `work-types.json`. Defaults to the bundled file. */
15
- localPath?: string;
16
- /** HTTP fetcher. Defaults to global `fetch`. */
17
- fetch?: typeof globalThis.fetch;
18
- /** Override the upstream URL (used by tests). */
19
- upstreamUrl?: string;
20
- }
21
- /**
22
- * Fetch the upstream `work-types.json`, validate its top-level shape, and overwrite the
23
- * locally-bundled copy with the formatted upstream contents.
24
- *
25
- * Output is reformatted to match the local file's conventions (2-space indent, trailing
26
- * newline) so subsequent diffs are content-driven, not whitespace-driven.
27
- */
28
- export declare function syncWorkTypes(dependencies?: SyncWorkTypesDependencies): Promise<SyncResult>;