AUR-498 - Deterministic changelog and semver engine
Status: implemented for the reconstruction. Package: internal/changelog.
Profile owner: AUR-006. Upstream: AUR-490.
Purpose and contract
internal/changelog turns the reviewed commit messages of a range into (a) the
next semantic version and (b) a Markdown changelog entry. It is:
- pure - only the standard library, no clocks, no random, no I/O;
- offline - no network, no LLM, no
gitinvocation, no shell-out; - deterministic - the same
[]Commitalways renders byte-identical output; - read-only - it never writes the repository, never edits
CHANGELOG.md, never creates a tag or release; - untrusted-input safe - commit text is data, not instructions. It is escaped before rendering, bounded in count and length, and can never change the computed bump or authorize publication.
The rendered entry is advisory. Publication (PR/CI/tag) is AUR-499.
Public API
| Symbol | Meaning |
|---|---|
type Commit struct { Subject, Body, Hash string } |
one reviewed commit; all fields untrusted |
type Kind string |
recognized Conventional Commit type, or KindUnknown |
type Classification struct { Kind, Scope, Description, Breaking, Conventional, Subject, Hash } |
parse result |
func Classify(Commit) Classification |
parse one commit |
func ClassifyCommits([]Commit) []Classification |
parse at most MaxCommits |
type Bump int |
BumpNone, BumpPatch, BumpMinor, BumpMajor |
func (Bump) String() string |
"none" | "patch" | "minor" | "major" |
type Version struct { Major, Minor, Patch int } |
release-line version |
func ParseVersion(string) (Version, error) |
strict [v]major.minor.patch |
func (Version) String() string |
major.minor.patch |
func NextVersion(Version, []Commit) (Version, Bump) |
bump table |
func Render(Version, []Commit) string |
deterministic Markdown entry |
Classification table (AC-001)
Subject grammar: type(scope)!: description.
typeis one of the recognized kinds below; anything else (or a subject that does not match the grammar) is residual:KindUnknown,Conventional == false, and the subject on its own is never breaking.scopeis optional and may not contain parentheses.!immediately after the type or closing parenthesis marks breaking.- A body line beginning
BREAKING CHANGE:orBREAKING-CHANGE:marks breaking, including on a residual subject, because it is a deliberate machine-readable declaration.
| Type | Kind | Breaking-capable |
|---|---|---|
feat |
KindFeat |
yes |
fix |
KindFix |
yes |
perf |
KindPerf |
yes |
refactor |
KindRefactor |
yes |
docs |
KindDocs |
yes |
chore |
KindChore |
yes |
test |
KindTest |
yes |
build |
KindBuild |
yes |
ci |
KindCI |
yes |
style |
KindStyle |
yes |
| anything else | KindUnknown |
footer only |
Semver bump table (AC-002)
Let current = major.minor.patch. The highest applicable rule wins.
| Condition | New version | Bump |
|---|---|---|
any breaking, major > 0 |
(major+1).0.0 |
major |
any breaking, major == 0 |
0.(minor+1).0 |
minor |
else any feat |
major.(minor+1).0 |
minor |
else any fix or perf |
major.minor.(patch+1) |
patch |
| else | unchanged | none |
The 0.x exception keeps an unstable line from jumping to 1.0.0 on a breaking
change; it rises the minor instead. docs, chore, test, build, ci,
style and residual subjects do not bump. Prose such as
chore: release v9.0.0 does not bump: only the parsed commit structure counts.
Section mapping and ordering (AC-003)
The section set and order are fixed:
| Section | Source |
|---|---|
### Added |
feat |
### Changed |
perf, refactor, and every residual subject |
### Fixed |
fix |
### Removed |
reserved; no current type maps to it |
Removed has no mapped Conventional type in this card's catalog; its heading is
still emitted so the structure is stable. Types that neither bump nor map
(docs, chore, test, build, ci, style) are omitted from the body.
Inside a section, bullets keep input order; the same input always yields the same bytes. Example:
## 1.3.0
### Added
- feat(api): add endpoint (abc1234)
### Changed
- perf: speed up parser
- totally non conventional subject
### Fixed
- fix: correct off-by-one
### Removed
Escaping and bounds (AC-004)
Commit text is untrusted. Before rendering:
<,>,&become<,>,&;- backticks,
*,_,[,]are backslash-escaped.
A subject like feat: <script>alert(1)</script> therefore renders as
feat: <script>alert(1)</script> and cannot inject markup. The
engine never executes or shells out to commit text.
Bounds:
| Constant | Value | Effect |
|---|---|---|
MaxCommits |
256 | commits beyond the cap are ignored |
MaxSubjectLen |
500 runes | subject/hash clipped with ... |
MaxOutputLen |
65536 bytes | entry hard cap, with a truncation marker |
Empty or nil input never panics: ClassifyCommits(nil) is empty,
NextVersion returns BumpNone, and Render emits the version heading plus
the four fixed section headings.
Non-goals
Publishing to a PR, writing CHANGELOG.md, creating tags or releases, calling
an LLM, reading .git at runtime inside the sandbox. Those belong to AUR-499.
Validation
tests/acceptance/AUR-498.sh runs the four behavioral selectors plus three
mutations (all exits 0 and prints AUR-498/<selector>/pass), rejects an
unknown selector with exit 64, and reports missing Go as infrastructure
(exit 79). The mutation scenarios edit a writable staging copy of the package,
never the repository.
MUT-001neutralizes thefix/perf-> patch rule; AC-002 (semver) fails.MUT-002stops marking breaking; AC-002 fails.MUT-003removes renderer escaping; AC-004 (untrusted text) fails.