aronrepo 2.2.1 → 3.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.
package/dist/index.js CHANGED
@@ -1,12 +1,759 @@
1
- //#region src/index.ts
2
- const packages = [
3
- "@aronrepo/commitlint-config",
4
- "@aronrepo/conventional-commits",
5
- "@aronrepo/conventional-changelog-config",
6
- "@aronrepo/semantic-release-config",
7
- "@aronrepo/semantic-release-pnpm"
8
- ];
9
- //#endregion
10
- export { packages };
1
+ import picomatch from "picomatch";
2
+ import semver from "semver";
3
+ // src/commits.ts
4
+ /** Release levels in descending order. */
5
+ const releaseLevels = [
6
+ "major",
7
+ "minor",
8
+ "patch"
9
+ ];
10
+ function compareRelease(a, b) {
11
+ const rank = (release) => release ? 3 - releaseLevels.indexOf(release) : 0;
12
+ return rank(a) - rank(b);
13
+ }
14
+ function maxRelease(...releases) {
15
+ return releases.reduce((highest, release) => compareRelease(release, highest) > 0 ? release : highest, false);
16
+ }
17
+ /**
18
+ * The Aronrepo commit types. The table is fixed: repositories choose a release
19
+ * mode and release paths, not their own types.
20
+ *
21
+ * Release notes list the groups in this order.
22
+ */
23
+ const commits = [
24
+ {
25
+ type: "Bump",
26
+ scope: "Major",
27
+ release: "major",
28
+ description: "Release a major version. Only the repository owner decides it."
29
+ },
30
+ {
31
+ type: "Bump",
32
+ scope: "Minor",
33
+ release: "minor",
34
+ description: "Release a minor version. Only the repository owner decides it."
35
+ },
36
+ {
37
+ type: "Bump",
38
+ scope: "Patch",
39
+ release: "patch",
40
+ description: "Release a patch version without another release commit."
41
+ },
42
+ {
43
+ type: "Add",
44
+ release: "minor",
45
+ group: "Additions",
46
+ description: "A new capability: a new API, export, option, command, or package."
47
+ },
48
+ {
49
+ type: "Improve",
50
+ release: "patch",
51
+ group: "Improvements",
52
+ description: "A better existing capability without new API: behavior, output, messages, or defaults."
53
+ },
54
+ {
55
+ type: "Perf",
56
+ release: "patch",
57
+ group: "Performance",
58
+ description: "Faster or lighter published code with the same behavior."
59
+ },
60
+ {
61
+ type: "Fix",
62
+ release: "patch",
63
+ group: "Bug Fixes",
64
+ description: "A bug fix in published behavior."
65
+ },
66
+ {
67
+ type: "Deprecate",
68
+ release: "patch",
69
+ group: "Deprecations",
70
+ description: "Mark a published capability as deprecated while it keeps working."
71
+ },
72
+ {
73
+ type: "Drop",
74
+ release: "patch",
75
+ group: "Removals",
76
+ description: "Remove a published capability."
77
+ },
78
+ {
79
+ type: "Upgrade",
80
+ release: "patch",
81
+ group: "Dependencies",
82
+ description: "Change a runtime dependency of a published package."
83
+ },
84
+ {
85
+ type: "Revert",
86
+ release: "patch",
87
+ group: "Reverts",
88
+ description: "Revert a commit. It releases what the reverted commit released."
89
+ },
90
+ {
91
+ type: "Docs",
92
+ scope: "README",
93
+ release: "patch",
94
+ group: "Documentation",
95
+ description: "Change a published package README."
96
+ },
97
+ {
98
+ type: "Docs",
99
+ release: false,
100
+ description: "Documentation that is not a published package README."
101
+ },
102
+ {
103
+ type: "Test",
104
+ release: false,
105
+ description: "Tests that cover existing behavior."
106
+ },
107
+ {
108
+ type: "Benchmark",
109
+ release: false,
110
+ description: "Benchmark harnesses, fixtures, and reports."
111
+ },
112
+ {
113
+ type: "Example",
114
+ release: false,
115
+ description: "Examples that do not ship in a package."
116
+ },
117
+ {
118
+ type: "Build",
119
+ release: false,
120
+ description: "Build tooling and development configuration."
121
+ },
122
+ {
123
+ type: "CI",
124
+ release: false,
125
+ description: "CI workflows and automation."
126
+ },
127
+ {
128
+ type: "Style",
129
+ release: false,
130
+ description: "Formatting and lint-only changes."
131
+ },
132
+ {
133
+ type: "Refactor",
134
+ release: false,
135
+ description: "Code changes that keep published behavior."
136
+ },
137
+ {
138
+ type: "Chore",
139
+ release: false,
140
+ description: "Maintenance: development dependencies, lockfiles, metadata, agent guidance, and unpublished workspaces."
141
+ }
142
+ ];
143
+ /**
144
+ * Types from earlier Aronrepo versions. They still resolve in history so old
145
+ * commits release as before, but `aronrepo check` rejects them in new commits.
146
+ */
147
+ const legacyCommits = [
148
+ {
149
+ type: "Feat",
150
+ replacement: "Add"
151
+ },
152
+ {
153
+ type: "New",
154
+ replacement: "Add"
155
+ },
156
+ {
157
+ type: "Update",
158
+ replacement: "Improve"
159
+ },
160
+ {
161
+ type: "Misc",
162
+ replacement: "Chore"
163
+ }
164
+ ];
165
+ const types = [...new Set(commits.map(({ type }) => type))];
166
+ const legacyTypes = legacyCommits.map(({ type }) => type);
167
+ const releaseCommits = commits.filter(({ release }) => release !== false);
168
+ const nonReleaseCommits = commits.filter(({ release }) => release === false);
169
+ const releaseTypes = [...new Set(releaseCommits.map(({ type }) => type))];
170
+ const nonReleaseTypes = [...new Set(nonReleaseCommits.map(({ type }) => type))];
171
+ /** Release notes groups in display order. */
172
+ const releaseGroups = [...new Set(commits.flatMap(({ group }) => group ? [group] : []))];
173
+ function normalizeType(type) {
174
+ return legacyCommits.find((legacy) => legacy.type === type)?.replacement ?? type;
175
+ }
176
+ function findCommitRule(type, scope) {
177
+ const current = normalizeType(type);
178
+ if (!current) return void 0;
179
+ return commits.find((commit) => commit.type === current && commit.scope === scope) ?? commits.find((commit) => commit.type === current && commit.scope === void 0);
180
+ }
181
+ // src/policy.ts
182
+ const releaseModes = ["explicit", "auto"];
183
+ const defaultReleaseMode = "explicit";
184
+ const defaultReleasePathExcludes = [
185
+ "**/tests/**",
186
+ "**/test/**",
187
+ "**/__tests__/**",
188
+ "**/*.{test,spec}.*",
189
+ "**/benchmarks/**",
190
+ "**/examples/**",
191
+ "**/CHANGELOG.md",
192
+ ".github/**",
193
+ ".husky/**"
194
+ ];
195
+ function defineConfig(config) {
196
+ return config;
197
+ }
198
+ function getReleasePolicy(config = {}) {
199
+ const mode = config.release?.mode ?? "explicit";
200
+ if (!releaseModes.includes(mode)) throw new Error(`Unknown Aronrepo release mode "${String(mode)}". Use one of: ${releaseModes.join(", ")}.`);
201
+ return {
202
+ mode,
203
+ ...config.release?.paths !== void 0 ? { paths: config.release.paths } : {}
204
+ };
205
+ }
206
+ const defaultReleasePolicy = { mode: defaultReleaseMode };
207
+ /** Returns a matcher for release paths, or `undefined` when every path may release. */
208
+ function createReleasePathMatcher(paths) {
209
+ if (!paths || !paths.include) return void 0;
210
+ const include = picomatch(paths.include.length ? paths.include : ["!**"], { dot: true });
211
+ const exclude = picomatch(paths.exclude ?? defaultReleasePathExcludes, { dot: true });
212
+ return (file) => include(file) && !exclude(file);
213
+ }
214
+ /**
215
+ * Release level of a commit type in the given mode, ignoring breaking markers and paths.
216
+ * Use `resolveCommitRelease()` for the full decision.
217
+ */
218
+ function getReleaseType(type, scope, policy = defaultReleasePolicy) {
219
+ const release = findCommitRule(type, scope)?.release;
220
+ if (release === void 0) return void 0;
221
+ return capRelease(release, type, policy);
222
+ }
223
+ function capRelease(release, type, policy) {
224
+ if (policy.mode === "explicit" && type !== "Bump" && (release === "major" || release === "minor")) return "patch";
225
+ return release;
226
+ }
227
+ const headerPattern = /^([A-Z]\w*)(?:\(([0-9A-Z`_#~].*)\))?: ([0-9A-Z`_#~].*)$/;
228
+ const breakingHeaderPattern = /^([A-Z]\w*)(?:\(([^)]*)\))?!: (.+)$/;
229
+ const breakingNotePattern = /^BREAKING[ -]CHANGE:/m;
230
+ const footerLinePattern = /^BREAKING[ -]CHANGE:/;
231
+ const githubRevertPattern = /^Revert "([\s\S]+)"$/;
232
+ const revertHashPattern = /This reverts commit ([0-9a-f]{7,40})/i;
233
+ const trailerPattern = /^[\w-]+: /;
234
+ /** `BREAKING CHANGE:` footer texts; each runs until a blank line, another footer, or a git trailer. */
235
+ function readBreakingNotes(lines) {
236
+ const notes = [];
237
+ let current;
238
+ for (const line of lines) {
239
+ const start = /^BREAKING[ -]CHANGE:\s*(.*)$/.exec(line);
240
+ if (start) {
241
+ if (current) notes.push(current.join(" ").trim());
242
+ current = [start[1]];
243
+ } else if (current && line.trim() && !trailerPattern.test(line)) current.push(line.trim());
244
+ else if (current) {
245
+ notes.push(current.join(" ").trim());
246
+ current = void 0;
247
+ }
248
+ }
249
+ if (current) notes.push(current.join(" ").trim());
250
+ return notes.filter(Boolean);
251
+ }
252
+ function parseCommit(message, hash) {
253
+ const [header = "", ...rest] = message.replace(/\r\n/g, "\n").trim().split("\n");
254
+ const body = rest.join("\n").trim();
255
+ const parsed = {
256
+ header,
257
+ body,
258
+ ...hash ? { hash } : {}
259
+ };
260
+ const layout = [];
261
+ if (rest.length && rest[0].trim()) layout.push("body-leading-blank");
262
+ if (rest.some((line, index) => index > 0 && footerLinePattern.test(line) && rest[index - 1].trim())) layout.push("footer-leading-blank");
263
+ if (layout.length) parsed.layout = layout;
264
+ const breakingNotes = readBreakingNotes(rest);
265
+ if (breakingNotes.length) parsed.breakingNotes = breakingNotes;
266
+ const regular = headerPattern.exec(header);
267
+ const match = regular ?? breakingHeaderPattern.exec(header);
268
+ if (match) {
269
+ parsed.type = match[1];
270
+ if (match[2]) parsed.scope = match[2];
271
+ parsed.subject = match[3];
272
+ if (!regular) parsed.breaking = "header";
273
+ }
274
+ if (!parsed.breaking && breakingNotePattern.test(body)) parsed.breaking = "footer";
275
+ const githubRevert = githubRevertPattern.exec(header);
276
+ if (githubRevert || parsed.type === "Revert") {
277
+ const revertedHeader = githubRevert?.[1] ?? (parsed.subject && headerPattern.test(parsed.subject) ? parsed.subject : void 0);
278
+ const revertedHash = revertHashPattern.exec(body)?.[1];
279
+ parsed.revert = {
280
+ ...revertedHeader ? { header: revertedHeader } : {},
281
+ ...revertedHash ? { hash: revertedHash } : {}
282
+ };
283
+ }
284
+ return parsed;
285
+ }
286
+ const nonReleaseAlternatives = [
287
+ "Chore",
288
+ "Refactor",
289
+ "Style",
290
+ "Test",
291
+ "Docs",
292
+ "Build",
293
+ "CI",
294
+ "Benchmark",
295
+ "Example"
296
+ ];
297
+ function suggestNonReleaseHeader(commit) {
298
+ const subject = commit.subject ?? "";
299
+ const scope = commit.scope ? `(${commit.scope})` : "";
300
+ switch (commit.type) {
301
+ case "Docs": return `Docs: ${subject}`;
302
+ case "Upgrade": return `Chore(Deps): ${subject}`;
303
+ default: return `Chore${scope}: ${subject}`;
304
+ }
305
+ }
306
+ const looseHeaderPattern = /^(\w+)(?:\(([^)]*)\))?(!)?: ?(.*)$/;
307
+ function diagnoseHeader(header) {
308
+ const format = "Use `Type(Scope): Summary` or `Type: Summary` with an Aronrepo commit type.";
309
+ const match = looseHeaderPattern.exec(header);
310
+ if (!match) return format;
311
+ const [, type, scope, bang, subject] = match;
312
+ if (!/^[A-Z]/.test(type)) return `The type \`${type}\` must be PascalCase, such as \`${type[0].toUpperCase()}${type.slice(1)}\`. ${format}`;
313
+ if (bang) return `Aronrepo headers do not use \`!\`. ${format}`;
314
+ if (scope !== void 0 && !/^[0-9A-Z`_#~]/.test(scope)) return `The scope \`${scope}\` must start with an uppercase letter or digit. ${format}`;
315
+ if (!subject) return `The summary is empty. ${format}`;
316
+ if (!/^[0-9A-Z`_#~]/.test(subject)) return `The summary must start with an uppercase letter or digit. ${format}`;
317
+ return format;
318
+ }
319
+ function formatRelease(release) {
320
+ return release ? release : "none";
321
+ }
322
+ function resolveCommitRelease(input, policy = defaultReleasePolicy, options = {}) {
323
+ const commit = typeof input === "string" ? parseCommit(input) : input;
324
+ const decision = {
325
+ ...commit.hash ? { hash: commit.hash } : {},
326
+ header: commit.header,
327
+ ...commit.type ? { type: commit.type } : {},
328
+ ...commit.scope ? { scope: commit.scope } : {},
329
+ ...commit.subject ? { subject: commit.subject } : {},
330
+ ...commit.breakingNotes ? { breakingNotes: commit.breakingNotes } : {},
331
+ release: false,
332
+ reasons: [],
333
+ violations: []
334
+ };
335
+ if (commit.revert?.header && !commit.type) decision.type = "Revert";
336
+ else if (!commit.type) {
337
+ decision.reasons.push("Header does not follow `Type(Scope): Summary`; no release.");
338
+ decision.violations.push({
339
+ rule: "header-format",
340
+ message: diagnoseHeader(commit.header)
341
+ });
342
+ return decision;
343
+ } else if (!types.includes(commit.type) && !legacyTypes.includes(commit.type)) {
344
+ decision.reasons.push(`Unknown type \`${commit.type}\`; no release.`);
345
+ decision.violations.push({
346
+ rule: "unknown-type",
347
+ message: `\`${commit.type}\` is not an Aronrepo commit type. Use one of: ${types.join(", ")}.`
348
+ });
349
+ return decision;
350
+ } else if (!findCommitRule(commit.type, commit.scope)) {
351
+ decision.reasons.push(`\`${commit.type}${commit.scope ? `(${commit.scope})` : ""}\` is not an Aronrepo commit type; no release.`);
352
+ decision.violations.push({
353
+ rule: "unknown-type",
354
+ message: `\`${commit.type}\` needs a scope: ${commits.filter(({ type }) => type === commit.type).map(({ type, scope }) => `\`${type}(${scope})\``).join(", ")}.`
355
+ });
356
+ return decision;
357
+ }
358
+ if (commit.subject?.endsWith(".")) decision.violations.push({
359
+ rule: "subject-full-stop",
360
+ message: "The summary must not end with a period.",
361
+ suggestion: commit.header.replace(/\.+$/, "")
362
+ });
363
+ for (const rule of commit.layout ?? []) decision.violations.push({
364
+ rule,
365
+ severity: "warning",
366
+ message: rule === "body-leading-blank" ? "Leave a blank line between the header and the body." : "Leave a blank line before a `BREAKING CHANGE:` footer."
367
+ });
368
+ if (commit.type && legacyTypes.includes(commit.type)) {
369
+ const replacement = normalizeType(commit.type);
370
+ const scope = commit.scope ? `(${commit.scope})` : "";
371
+ decision.reasons.push(`\`${commit.type}\` is a legacy type; it releases like \`${replacement}\`.`);
372
+ decision.violations.push({
373
+ rule: "legacy-type",
374
+ message: `\`${commit.type}\` is a legacy Aronrepo type. Use \`${replacement}\`.`,
375
+ suggestion: `${replacement}${scope}: ${commit.subject ?? ""}`
376
+ });
377
+ }
378
+ let release;
379
+ if (decision.type === "Revert" && commit.revert?.header) {
380
+ release = resolveCommitRelease(commit.revert.header, policy, options).release;
381
+ decision.reasons.push(`Reverts \`${commit.revert.header}\`, which releases ${formatRelease(release)}.`);
382
+ } else {
383
+ const rule = findCommitRule(decision.type, decision.scope);
384
+ release = capRelease(rule.release, decision.type, policy);
385
+ decision.reasons.push(`\`${decision.type}${decision.scope ? `(${decision.scope})` : ""}\` releases ${formatRelease(release)} in ${policy.mode} mode.`);
386
+ if (release !== rule.release) decision.reasons.push(`Explicit mode caps \`${decision.type}\` at patch; only \`Bump(Minor)\` or \`Bump(Major)\` raise the version further.`);
387
+ }
388
+ const isBumpMajor = decision.type === "Bump" && decision.scope === "Major";
389
+ if (commit.breaking && !isBumpMajor) {
390
+ const marker = commit.breaking === "header" ? "`!` header" : "`BREAKING CHANGE:` footer";
391
+ if (policy.mode === "auto") {
392
+ release = "major";
393
+ decision.reasons.push(`The ${marker} releases major in auto mode.`);
394
+ } else {
395
+ decision.reasons.push(`The ${marker} is ignored in explicit mode; only \`Bump(Major)\` releases major.`);
396
+ decision.violations.push({
397
+ rule: "implicit-breaking",
398
+ message: `A ${marker} does not release a major version in explicit mode. Describe the change without the breaking marker, or ask the repository owner whether to add a \`Bump(Major)\` commit.`
399
+ });
400
+ }
401
+ }
402
+ const isBump = decision.type === "Bump";
403
+ if (release && !isBump && options.files && options.isReleasePath) {
404
+ if (!options.files.filter(options.isReleasePath).length) {
405
+ decision.reasons.push("No changed file ships in a published package; no release.");
406
+ decision.violations.push({
407
+ rule: "release-paths",
408
+ message: `\`${decision.type}${decision.scope ? `(${decision.scope})` : ""}\` releases ${formatRelease(release)}, but the commit changes no release paths. Use a non-release type: ${nonReleaseAlternatives.join(", ")}.`,
409
+ suggestion: suggestNonReleaseHeader({
410
+ ...commit,
411
+ type: decision.type
412
+ })
413
+ });
414
+ release = false;
415
+ }
416
+ }
417
+ decision.release = release;
418
+ return decision;
419
+ }
420
+ function resolveRelease(inputs, policy = defaultReleasePolicy, options = {}) {
421
+ const parsed = inputs.map((input) => parseCommit(input.message, input.hash));
422
+ const decisions = parsed.map((commit, index) => resolveCommitRelease(commit, policy, {
423
+ ...options,
424
+ ...inputs[index].files ? { files: inputs[index].files } : {}
425
+ }));
426
+ parsed.forEach((commit, index) => {
427
+ const revertedHash = commit.revert?.hash;
428
+ if (!revertedHash) return;
429
+ const revertedIndex = parsed.findIndex((candidate) => candidate.hash?.startsWith(revertedHash) || candidate.hash && revertedHash.startsWith(candidate.hash));
430
+ if (revertedIndex === -1 || revertedIndex === index) return;
431
+ for (const [target, other] of [[index, revertedIndex], [revertedIndex, index]]) {
432
+ decisions[target].release = false;
433
+ decisions[target].reasons.push(`Cancelled by \`${parsed[other].header}\` in the same release.`);
434
+ }
435
+ });
436
+ return {
437
+ release: maxRelease(...decisions.map(({ release }) => release)),
438
+ commits: decisions
439
+ };
440
+ }
441
+ // src/agent.ts
442
+ function getAgentCommitPolicy(policy = defaultReleasePolicy, options = {}) {
443
+ const explicit = policy.mode === "explicit";
444
+ const cli = options.cli ?? "npx aronrepo";
445
+ return {
446
+ mode: policy.mode,
447
+ defaultInternalCommit: "Chore(Agent): Clarify commit selection policy",
448
+ defaultInternalType: "Chore",
449
+ defaultInternalScope: "Agent",
450
+ releaseImpactRule: "Use a release type only when the commit changes files that ship in a published package (release paths) and affects published behavior, public API, release behavior, or published README content. Otherwise use a non-release type.",
451
+ ownerDecisions: explicit ? [
452
+ "Never write `Bump(Major)` or `Bump(Minor)` unless the user explicitly asked for that release level in the current conversation.",
453
+ "Never add a `BREAKING CHANGE:` footer or a `Type!:` header. If a change breaks published behavior, stop and ask the user how to release it.",
454
+ "Every type except `Bump(Minor)` and `Bump(Major)` releases at most a patch. Pick the type that describes the change, not the release level you want."
455
+ ] : [
456
+ "Never write `Bump(Major)` or `Bump(Minor)` unless the user explicitly asked for that release level in the current conversation.",
457
+ "A `BREAKING CHANGE:` footer releases a major version. Add one only for a real break in published behavior or public API, and tell the user.",
458
+ "`Add` releases a minor version. Use it only for new capabilities of published packages."
459
+ ],
460
+ releaseWorthyChanges: [
461
+ "Published package runtime behavior",
462
+ "Public API, exports, or types",
463
+ "Release, changelog, or versioning behavior",
464
+ "Published README content that requires a package patch release"
465
+ ],
466
+ nonReleaseDefaults: [
467
+ "Agent instructions, prompts, and repository context: `Chore(Agent)`",
468
+ "Sites, apps, and other unpublished workspaces: `Chore(<Scope>)`, `Refactor`, `Style`, or `Test`, even for fixes and performance work",
469
+ "Tests that only cover existing behavior: `Test`",
470
+ "Benchmark harnesses, fixtures, reports, and measurement-only changes: `Benchmark` (never `Perf`)",
471
+ "Build tooling and dev-only configuration: `Build`; CI workflows: `CI`; formatting: `Style`",
472
+ "Development dependencies, lockfiles, and routine metadata: `Chore(Deps)` or `Chore`",
473
+ "Examples: `Example`; internal or site documentation: `Docs`",
474
+ "Refactors that do not change published behavior: `Refactor`"
475
+ ],
476
+ verification: [`${cli} check --from origin/main --to HEAD`, `${cli} explain`],
477
+ examples: [
478
+ {
479
+ commit: "Add(Core): Support ESM config files",
480
+ use: "A new capability in a published package",
481
+ release: getReleaseType("Add", "Core", policy) ?? false,
482
+ ...explicit ? { avoid: "Feat(Core): Support ESM config files" } : {}
483
+ },
484
+ {
485
+ commit: "Chore(Site): Fix the homepage header transition",
486
+ use: "A fix in an unpublished site or app workspace",
487
+ release: false,
488
+ avoid: "Fix(Site): Fix the homepage header transition"
489
+ },
490
+ {
491
+ commit: "Chore(Agent): Update repository guidance for coding agents",
492
+ use: "Agent instructions, prompts, or repo context",
493
+ release: false,
494
+ avoid: "Feat(Agent): Add agent guide"
495
+ },
496
+ {
497
+ commit: "Test(Release): Cover scoped README bump rules",
498
+ use: "Tests that validate existing release behavior",
499
+ release: false
500
+ },
501
+ {
502
+ commit: "Benchmark(Benchmarks): Refresh benchmark report",
503
+ use: "Benchmark-only changes when the benchmark area is the useful scope",
504
+ release: false,
505
+ avoid: "Perf(Benchmarks): Refresh benchmark report"
506
+ },
507
+ {
508
+ commit: "Benchmark: Refresh benchmark report",
509
+ use: "Benchmark-only changes when no clear scope exists",
510
+ release: false
511
+ },
512
+ {
513
+ commit: "CI(GitHub): Update PR title check permissions",
514
+ use: "CI workflow, status check, and automation changes that do not change release behavior",
515
+ release: false,
516
+ avoid: "Fix(CI): Update PR title check permissions"
517
+ },
518
+ {
519
+ commit: "Build(Tooling): Update Vitest config",
520
+ use: "Build system, bundler, local tooling, and dev-only configuration changes",
521
+ release: false,
522
+ avoid: "Update(Build): Update Vitest config"
523
+ },
524
+ {
525
+ commit: "Style(Lint): Format TypeScript files",
526
+ use: "Formatting, lint-only, whitespace, or readability changes with no behavior change",
527
+ release: false,
528
+ avoid: "Improve(Core): Format TypeScript files"
529
+ },
530
+ {
531
+ commit: "Chore(Deps): Update Vitest dev dependency",
532
+ use: "Development dependency, lockfile, audit report, and routine metadata changes",
533
+ release: false,
534
+ avoid: "Upgrade(Deps): Update Vitest dev dependency"
535
+ },
536
+ {
537
+ commit: "Example(Release): Add a release workflow sample",
538
+ use: "Example-only additions or updates that do not change published package behavior",
539
+ release: false,
540
+ avoid: "Add(Example): Add a release workflow sample"
541
+ },
542
+ {
543
+ commit: "Docs: Add commit type examples",
544
+ use: "Documentation that is not published as package README content",
545
+ release: false
546
+ },
547
+ {
548
+ commit: "Docs(README): Clarify package installation",
549
+ use: "A published package README (not the repository root README)",
550
+ release: "patch"
551
+ },
552
+ {
553
+ commit: "Fix(Release): Preserve workspace dependency ranges",
554
+ use: "A release behavior bug that affects published package behavior",
555
+ release: "patch"
556
+ }
557
+ ]
558
+ };
559
+ }
560
+ /** Agent policy for the default `explicit` mode. */
561
+ const agentCommitPolicy = getAgentCommitPolicy();
562
+ function formatRule(type, scope) {
563
+ return `\`${type}${scope ? `(${scope})` : ""}\``;
564
+ }
565
+ function getReleaseTable(policy = defaultReleasePolicy) {
566
+ const rows = {
567
+ major: [],
568
+ minor: [],
569
+ patch: [],
570
+ none: []
571
+ };
572
+ for (const { type, scope } of commits) rows[getReleaseType(type, scope, policy) || "none"].push(formatRule(type, scope));
573
+ if (policy.mode === "auto") rows.major.push("`BREAKING CHANGE:` footer");
574
+ return rows;
575
+ }
576
+ const agentsBlockStart = "<!-- aronrepo:commit-policy:start -->";
577
+ const agentsBlockEnd = "<!-- aronrepo:commit-policy:end -->";
578
+ /** Renders the generated commit policy block for AGENTS.md, including its markers. */
579
+ function renderAgentsBlock(policy = defaultReleasePolicy, options = {}) {
580
+ const agent = getAgentCommitPolicy(policy, options);
581
+ const paths = options.releasePaths;
582
+ const typeRows = commits.map(({ type, scope, description }) => `| ${formatRule(type, scope)} | ${getReleaseType(type, scope, policy) || "none"} | ${description} |`);
583
+ return [
584
+ agentsBlockStart,
585
+ "<!-- Generated by `aronrepo agents --write`. Edit aronrepo.config.js and regenerate instead of editing this block. -->",
586
+ "",
587
+ "## Commit and Release Policy",
588
+ "",
589
+ `Release mode: **${policy.mode}**. ${policy.mode === "explicit" ? "Only the repository owner decides minor and major versions, with `Bump(Minor)` and `Bump(Major)` commits." : "`Add` releases minor versions, and `BREAKING CHANGE:` footers release major versions."}`,
590
+ "",
591
+ "Commit headers and PR titles use `Type(Scope): Summary` or `Type: Summary`: a PascalCase type, an optional scope for the package or area, and a sentence-case summary without a trailing period.",
592
+ "",
593
+ "| Type | Release | Use for |",
594
+ "| --- | --- | --- |",
595
+ ...typeRows,
596
+ "",
597
+ `Legacy types are rejected: ${legacyCommits.map(({ type, replacement }) => `\`${type}\` (use \`${replacement}\`)`).join(", ")}.`,
598
+ "",
599
+ "### Release decisions belong to the owner",
600
+ "",
601
+ ...agent.ownerDecisions.map((rule) => `- ${rule}`),
602
+ "",
603
+ "### Choosing a type",
604
+ "",
605
+ `- ${agent.releaseImpactRule}`,
606
+ "- Pick the type that describes the change: a new capability is `Add`, a better existing capability is `Improve`, a bug fix is `Fix`, and a runtime dependency change is `Upgrade`.",
607
+ `- A commit that changes no release path must use a non-release type (${nonReleaseAlternatives.map((type) => `\`${type}\``).join(", ")}), even when it fixes or speeds up something.`,
608
+ "- Split unrelated changes so each commit carries its own release level.",
609
+ "",
610
+ "Non-release defaults:",
611
+ "",
612
+ ...agent.nonReleaseDefaults.map((rule) => `- ${rule}`),
613
+ "",
614
+ "### Release paths",
615
+ "",
616
+ ...paths?.include ? [
617
+ "Only changes to these files can release a version:",
618
+ "",
619
+ ...paths.include.map((glob) => `- \`${glob}\``),
620
+ "",
621
+ `Excluding: ${(paths.exclude ?? defaultReleasePathExcludes).map((glob) => `\`${glob}\``).join(", ")}.`
622
+ ] : ["Every path can release a version in this repository."],
623
+ "",
624
+ "### Before handing off commits",
625
+ "",
626
+ ...agent.verification.map((command) => `- \`${command}\``),
627
+ "",
628
+ "`aronrepo explain` prints the next release level and the reason for each commit. Tell the user when it is not what they expect.",
629
+ agentsBlockEnd
630
+ ].join("\n");
631
+ }
632
+ // src/version.ts
633
+ /** Prerelease branches. Each publishes `X.Y.Z-<branch>.N` on the npm dist-tag of the same name. */
634
+ const prereleaseChannels = [
635
+ "alpha",
636
+ "beta",
637
+ "rc",
638
+ "canary"
639
+ ];
640
+ /** Branches that release: `main` publishes on `latest`, and each prerelease branch on its own dist-tag. */
641
+ const releaseBranches = ["main", ...prereleaseChannels];
642
+ /** The release channel of a branch, or `undefined` when the branch does not release. */
643
+ function getReleaseChannel(branch) {
644
+ if (branch === "main") return {
645
+ branch,
646
+ distTag: "latest"
647
+ };
648
+ if (branch && prereleaseChannels.includes(branch)) return {
649
+ branch,
650
+ distTag: branch,
651
+ prerelease: branch
652
+ };
653
+ }
654
+ /** The first version of a repository without version tags. */
655
+ const initialVersion = "1.0.0";
656
+ function toVersionTag(version) {
657
+ return `v${version}`;
658
+ }
659
+ /** The version of a `vX.Y.Z` or `vX.Y.Z-pre.N` tag, or `undefined` for other tags. */
660
+ function parseVersionTag(tag) {
661
+ if (!tag.startsWith("v")) return void 0;
662
+ const version = tag.slice(1);
663
+ return semver.valid(version) === version ? version : void 0;
664
+ }
665
+ function versionsOf(tags) {
666
+ return tags.flatMap((tag) => {
667
+ const version = parseVersionTag(tag);
668
+ return version ? [version] : [];
669
+ });
670
+ }
671
+ function highest(versions) {
672
+ return versions.length ? semver.rsort([...versions])[0] : void 0;
673
+ }
674
+ function stableVersions(versions) {
675
+ return versions.filter((version) => !semver.prerelease(version));
676
+ }
677
+ function channelVersions(versions, prerelease) {
678
+ return versions.filter((version) => semver.prerelease(version)?.[0] === prerelease);
679
+ }
680
+ function coreVersion(version) {
681
+ return `${semver.major(version)}.${semver.minor(version)}.${semver.patch(version)}`;
682
+ }
683
+ /** The latest stable version among tags, if any. */
684
+ function getLatestStableVersion(tags) {
685
+ return highest(stableVersions(versionsOf(tags)));
686
+ }
687
+ /**
688
+ * The tag a channel's release range starts from: the latest stable tag, or on a
689
+ * prerelease branch the latest tag of that channel when it is newer.
690
+ */
691
+ function getBaseTag(channel, reachableTags) {
692
+ const versions = versionsOf(reachableTags);
693
+ const stable = highest(stableVersions(versions));
694
+ if (channel.prerelease) {
695
+ const prerelease = highest(channelVersions(versions, channel.prerelease).filter((version) => !stable || semver.gt(version, stable)));
696
+ if (prerelease) return toVersionTag(prerelease);
697
+ }
698
+ return stable ? toVersionTag(stable) : void 0;
699
+ }
700
+ /**
701
+ * The next version of a channel.
702
+ *
703
+ * `main` raises the latest stable version by `release`. A prerelease branch
704
+ * targets the same version, or the version its earlier prereleases already
705
+ * target when that is higher, and numbers the prerelease after every existing
706
+ * tag of that version and channel.
707
+ */
708
+ function getNextVersion(channel, release, tags) {
709
+ const reachable = versionsOf(tags.reachable);
710
+ const stable = highest(stableVersions(reachable));
711
+ const target = stable ? semver.inc(stable, release) : initialVersion;
712
+ if (!channel.prerelease) return target;
713
+ const previous = highest(channelVersions(reachable, channel.prerelease).filter((version) => !stable || semver.gt(version, stable)));
714
+ const core = previous && semver.gte(coreVersion(previous), target) ? coreVersion(previous) : target;
715
+ const numbers = channelVersions(versionsOf(tags.all), channel.prerelease).filter((version) => coreVersion(version) === core).map((version) => Number(semver.prerelease(version)?.[1] ?? 0));
716
+ return `${core}-${channel.prerelease}.${Math.max(0, ...numbers) + 1}`;
717
+ }
718
+ // src/notes.ts
719
+ const breakingTitle = "⚠ Breaking Changes";
720
+ function groupOf(decision) {
721
+ if (decision.type === "Revert") return "Reverts";
722
+ return findCommitRule(decision.type, decision.scope)?.group;
723
+ }
724
+ function formatEntry(decision, repositoryUrl) {
725
+ const summary = decision.subject ?? decision.header;
726
+ const scope = decision.scope ? `**${decision.scope}:** ` : "";
727
+ const hash = decision.hash ? decision.hash.slice(0, 7) : void 0;
728
+ return `- ${scope}${summary}${hash ? repositoryUrl ? ` ([${hash}](${repositoryUrl}/commit/${decision.hash}))` : ` (${hash})` : ""}`;
729
+ }
730
+ /**
731
+ * Markdown release notes. Only commits that release appear, grouped by type in
732
+ * the order of the commit table; `BREAKING CHANGE:` footers of major releases
733
+ * come first.
734
+ */
735
+ function renderReleaseNotes(options) {
736
+ const { version, previousTag, repositoryUrl } = options;
737
+ const tag = `v${version}`;
738
+ const lines = [`## ${repositoryUrl && previousTag ? `[${version}](${repositoryUrl}/compare/${previousTag}...${tag})` : version}${options.date ? ` (${options.date})` : ""}`];
739
+ const released = options.commits.filter(({ release }) => release);
740
+ const breaking = released.filter(({ release }) => release === "major").flatMap(({ breakingNotes }) => breakingNotes ?? []);
741
+ if (breaking.length) lines.push("", `### ${breakingTitle}`, "", ...breaking.map((note) => `- ${note}`));
742
+ const groups = /* @__PURE__ */ new Map();
743
+ for (const decision of released) {
744
+ const group = groupOf(decision);
745
+ if (!group) continue;
746
+ groups.set(group, [...groups.get(group) ?? [], decision]);
747
+ }
748
+ for (const group of releaseGroups) {
749
+ const entries = groups.get(group);
750
+ if (!entries) continue;
751
+ const sorted = [...entries].sort((a, b) => (a.scope ?? "").localeCompare(b.scope ?? "", "en") || (a.subject ?? a.header).localeCompare(b.subject ?? b.header, "en"));
752
+ lines.push("", `### ${group}`, "", ...sorted.map((decision) => formatEntry(decision, repositoryUrl)));
753
+ }
754
+ if (!breaking.length && !groups.size) lines.push("", "No changes to published packages.");
755
+ return `${lines.join("\n")}\n`;
756
+ }
757
+ export { agentCommitPolicy, agentsBlockEnd, agentsBlockStart, commits, compareRelease, createReleasePathMatcher, defaultReleaseMode, defaultReleasePathExcludes, defaultReleasePolicy, defineConfig, findCommitRule, getAgentCommitPolicy, getBaseTag, getLatestStableVersion, getNextVersion, getReleaseChannel, getReleasePolicy, getReleaseTable, getReleaseType, headerPattern, initialVersion, legacyCommits, legacyTypes, maxRelease, nonReleaseAlternatives, nonReleaseCommits, nonReleaseTypes, normalizeType, parseCommit, parseVersionTag, prereleaseChannels, releaseBranches, releaseCommits, releaseGroups, releaseLevels, releaseModes, releaseTypes, renderAgentsBlock, renderReleaseNotes, resolveCommitRelease, resolveRelease, toVersionTag, types };
11
758
 
12
759
  //# sourceMappingURL=index.js.map