Pular para conteúdo

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 git invocation, no shell-out;
  • deterministic - the same []Commit always 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.

  • type is 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.
  • scope is optional and may not contain parentheses.
  • ! immediately after the type or closing parenthesis marks breaking.
  • A body line beginning BREAKING CHANGE: or BREAKING-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 &lt;, &gt;, &amp;;
  • backticks, *, _, [, ] are backslash-escaped.

A subject like feat: <script>alert(1)</script> therefore renders as feat: &lt;script&gt;alert(1)&lt;/script&gt; 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-001 neutralizes the fix/perf -> patch rule; AC-002 (semver) fails.
  • MUT-002 stops marking breaking; AC-002 fails.
  • MUT-003 removes renderer escaping; AC-004 (untrusted text) fails.