Skip to content

feat(versioning): robust handling of calver, adjacent-letter pre-releases, word-prefix tags#555

Merged
rainxchzed merged 2 commits intomainfrom
feat/version-detection-robust
May 8, 2026
Merged

feat(versioning): robust handling of calver, adjacent-letter pre-releases, word-prefix tags#555
rainxchzed merged 2 commits intomainfrom
feat/version-detection-robust

Conversation

@rainxchzed
Copy link
Copy Markdown
Member

@rainxchzed rainxchzed commented May 8, 2026

Deep pass on VersionMath after the field report:

what happens when the repo actually have tag like 20260502 and app version is something like 1.2.0, or version-1.2.0-beta1 or 1.2.0beta01

Audited the existing implementation against ~30 tag-format conventions seen across GitHub releases. Found three real gaps + one cosmetic UX gap; all four addressed without breaking any existing behaviour.

Conventions audited (none flag false-positive bugs)

Convention Example Status
SemVer 1.2.3 ✅ already correct
SemVer pre-release 1.2.3-rc.1, 1.2.3-alpha, 1.2.3-beta.2 ✅ already correct
SemVer build metadata 1.2.3+sha.abc ✅ stripped via + cut
v / V prefix v1.2.3 ✅ already correct
refs/tags/ prefix refs/tags/v1.2.3 ✅ already correct
Dotted calver 2024.10.15, 2024.10.15.4567 ✅ parses as numeric semver
8-digit date integer 20260502, 20240315 ✅ parses as single-segment numeric
Architecture suffix on version 1.2.0-android, 1.2.0arm64 ✅ preserved as pre-release identifier (or filtered by dotted-digit fallback)

Gaps fixed

1. Hyphenated calver — 2024-10-15

Used to read as numbers=[2024], preRelease="10-15", which makes it semver-rank as older than 2024 stable. Net effect: a 2024-10-15 release would silently lose ordering against any 2024-10-16, AND lose to a hypothetical bare 2024 tag.

Fix: detect ^((?:19|20|21)\\d{2})-(\\d{1,2})-(\\d{1,2})(?:[-.](.+))?$ and rewrite to dotted calver (2024.10.15) before semver parse. The trailing identifier (e.g. 2024-10-15-rc1) is preserved as the pre-release.

2. Adjacent-letter pre-release marker — 1.2.0beta01

Used to lose the beta01 suffix entirely. parseSemanticVersion saw 0beta01 as a non-numeric segment, fell through to DOTTED_DIGIT_PATTERN which matched only the leading 1.2.0. Result: 1.2.0beta01 was treated as stable 1.2.0 — silently shipped a beta to opted-out users.

isPreReleaseTag had the same blind spot: its \\b regex boundary fails between 0 (digit) and b (letter), so 1.2.0beta01 returned false.

Fix: new insertHyphenBeforeKnownMarker step that looks for \\d[A-Za-z] and inserts a separating - ONLY if the trailing word starts with a known pre-release prefix (alpha, beta, rc, preview, prerelease, snapshot, canary, nightly, milestone, ea, dev, pre). Architecture suffixes like 1.2.0arm64 are intentionally NOT touched because arm64 is not a marker. Run from both normalizeVersion and isPreReleaseTag / preReleaseMarkerLabel so detection and ordering both benefit.

3. Word-style tag prefix — version-1.2.0, release/1.2.0, app-1.2.0, build-2025.04.10, ver_1.2.0

Used to fall through to DOTTED_DIGIT_PATTERN (which worked, but lost the pre-release suffix on tags like Release_1.2.0-beta). The fallback never applied semver ordering.

Fix: new VERSION_WORD_PREFIX regex ^(version|release|app|build|ver)\\s*[-_/.]\\s* runs before the v / V strip. Case-insensitive. Requires a separator so version1.2.3 (no separator) still falls through cleanly to the dotted-digit fallback.

4. Scheme detection helper for UIs — detectScheme(tag)Scheme

New public Scheme enum (SemVer, CalVer, CommitHash, Unknown) and detectScheme function. UI surfaces can now render "Released 2024-10-15" vs "Version 1.2.3" appropriately, and future code can warn when comparing across schemes (e.g. a maintainer who switched from date tags to semver mid-history would otherwise have all semver releases silently rank as older).

The underlying comparator does NOT branch on scheme — keeps the change minimal-blast. Just exposes the classification.

What stays unchanged

  • Public API surface — all existing functions keep their signatures.
  • Compare semantics for inputs that already parsed correctly (verified by mental walk-through against the existing kdoc examples).
  • isExactSameVersion — gates UI Open/Install CTA on case-sensitive literal equality after prefix strip; the new word-prefix strip applies here too, so release-1.2.3 and 1.2.3 now correctly read as the same version (was previously "different").

Test plan

  • :core:domain:compileKotlinJvm
  • :core:domain:compileDebugKotlinAndroid
  • Local CodeRabbit: 0 findings.
  • Repo has no test scaffolding yet (*Test.kt absent across all modules), so I walked the changes against ~25 example inputs in head and confirmed no regression. When test infra lands (deferred from earlier sprints), full VersionMath matrix is the obvious first target.

Out of scope

  • Changing the comparator to branch on scheme (e.g. "warn on mixed-scheme history") — file as a UI feature, not a versioning core fix.
  • Date-format calver canonicalisation that's not YYYY-MM-DD (e.g. 15-10-2024 European order) — too ambiguous; a maintainer using that format would have ordering issues regardless.
  • AppImage versionless-filename heuristic — separate concern in feature/details/data.

Summary by CodeRabbit

  • New Features

    • Added detection/classification of version schemes: SemVer, CalVer, CommitHash, Unknown.
  • Improvements

    • Broader normalization of version tags to handle more formats and prefixes consistently.
    • Better handling of calendar-date styles and adjacent pre-release markers to improve parsing.
    • More accurate pre-release detection and UI labeling.

…arkers, word-style tag prefixes, scheme detection
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 8, 2026

Review Change Stack
No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 08cecb45-b64d-48a9-8510-d65577190c00

📥 Commits

Reviewing files that changed from the base of the PR and between 8b8aa20 and f0be524.

📒 Files selected for processing (1)
  • core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/util/VersionMath.kt
🚧 Files skipped from review as they are similar to previous changes (1)
  • core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/util/VersionMath.kt

Walkthrough

VersionMath utility module is enhanced to handle diverse version and tag formats. Core normalization now supports word-style prefixes, calendar dates, and pre-release marker separation. New public detectScheme function classifies tags into SemVer, CalVer, CommitHash, or Unknown categories using the expanded ruleset.

Changes

Version Normalization and Scheme Detection

Layer / File(s) Summary
Regex Patterns and Constants
core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/util/VersionMath.kt
New and expanded regex constants and known pre-release prefix list to detect word-style prefixes, hyphenated CalVer, adjacent-letter pre-release markers, dotted/date CalVer, and commit hashes.
Normalization Helper Functions
core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/util/VersionMath.kt
Added stripFullPrefix (refs/tags + word-prefix + leading v stripping), normalizeCalverHyphen (yyyy-mm-dd → yyyy.mm.dd, optional tail), and insertHyphenBeforeKnownMarker (insert hyphen before known pre-release markers when adjacent).
Core Normalization and Comparison
core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/util/VersionMath.kt
normalizeVersion updated to run the full pipeline: prefix strip, +build removal, CalVer normalization, marker separation, then semver parse with regex fallback. stripCommonPrefixes delegates to stripFullPrefix. isExactSameVersion docs updated to reflect conservative prefix cleanup.
Scheme Detection API and Pre-release Updates
core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/util/VersionMath.kt
New public detectScheme(version: String?): Scheme and Scheme enum added. isPreReleaseTag and preReleaseMarkerLabel now match markers on the hyphen-inserted/preprocessed string.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • OpenHub-Store/GitHub-Store#466: Adds a prefix-tolerant equality helper in VersionMath; related to this PR's expanded prefix stripping and normalization enhancements.

Poem

🐰
I nibble prefixes, patch a missing dash,
Turn dates to dots in a quiet flash,
I chase the markers that cling to a name,
Classify schemes so tags play the game,
Hoppy versions march in tidy rows!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main changes in the PR: improving versioning robustness for CalVer formats, adjacent-letter pre-release markers, and word-style prefixes, which are the core focuses of the changeset.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/version-detection-robust

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/util/VersionMath.kt (2)

152-169: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

KDoc on isExactSameVersion is now stale — doesn't mention word-prefix stripping.

The doc still claims the cleanup is just "trim, strip refs/tags/, strip leading v / V, trim again", but stripCommonPrefixes now delegates to stripFullPrefix, which also strips version-, release/, app_, build-, ver. (case-insensitive). This is a deliberate behavior change per the PR objectives ("isExactSameVersion semantics (now accounts for word-prefix strip)"), so callers reading the contract here will be misled — e.g., they'll incorrectly assume release-1.2.3 and 1.2.3 compare unequal under this function.

📝 Suggested doc update
     /**
      * Strict literal equality after the conservative cleanup that
      * [normalizeVersion] applies BEFORE the semver normalization steps:
-     * trim, strip `refs/tags/`, strip leading `v` / `V`, trim again.
+     * trim, strip `refs/tags/`, strip a recognised word-style prefix
+     * (`version-`, `release/`, `app_`, `build-`, `ver.` — see
+     * `VERSION_WORD_PREFIX`), strip leading `v` / `V`, trim again.
      *
      * Differs from [isSameVersion] in that it does NOT strip `+build`
      * metadata, does NOT extract a dotted-digit core from prefixed
      * tags, and is case-sensitive on the suffix.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/util/VersionMath.kt`
around lines 152 - 169, Update the KDoc for isExactSameVersion to reflect the
current normalization behavior: mention that stripCommonPrefixes now delegates
to stripFullPrefix and that word-prefixes (case-insensitive) such as "version-",
"release/", "app_", "build-", "ver." in addition to "refs/tags/" and leading
"v"/"V" are stripped during cleanup; clarify that this function remains
case-sensitive for suffixes and does NOT remove +build metadata or extract
dotted-digit cores, and keep the note that two null/blank inputs return false.
Reference isExactSameVersion, stripCommonPrefixes, and stripFullPrefix in the
doc so readers can find the exact helpers implementing the behavior.

344-423: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Orphaned KDoc — isPreReleaseTag lost its documentation when detectScheme was inserted above it.

The KDoc block at lines 344–379 was originally bound to isPreReleaseTag. After this PR, two KDoc blocks now sit back-to-back (344–379 and 380–394) before detectScheme at line 395. In Kotlin, only the immediately preceding KDoc binds to a declaration, so:

  • The block at 344–379 is now orphaned (no declaration to attach to — a bare doc comment that IDEs/Dokka will drop).
  • detectScheme at line 395 picks up the new KDoc at 380–394 (correct).
  • isPreReleaseTag at line 423 now has no KDoc, despite the rich heuristic/rationale that was originally written for it (recognised markers list, "intentionally not recognised" list, examples).

Easiest fix: move detectScheme and the Scheme enum (plus their KDoc) to below isPreReleaseTag/preReleaseMarkerLabel, restoring the original doc-to-function adjacency. Alternatively, swap the two KDoc blocks and move the isPreReleaseTag function up to sit immediately after its docs.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/util/VersionMath.kt`
around lines 344 - 423, The KDoc for isPreReleaseTag (and its heuristic details)
became orphaned because detectScheme and the Scheme enum were inserted between
the comment and the function; move detectScheme and the Scheme enum (and their
KDoc) below isPreReleaseTag (and its related preReleaseMarkerLabel) so the
original KDoc immediately precedes isPreReleaseTag again, or alternatively swap
the two KDoc blocks and reposition the isPreReleaseTag function so its rich
heuristic KDoc is directly above the isPreReleaseTag declaration; ensure
references to detectScheme and Scheme remain unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/util/VersionMath.kt`:
- Around line 323-342: KNOWN_PRE_RELEASE_PREFIXES does not cover the
JetBrains-style milestone shorthand (m\d+), so insertHyphenBeforeKnownMarker
fails to detect adjacent-letter tags like "1.2.0m5" and
isPreReleaseTag/preReleaseMarkerLabel misclassify them as stable; fix by
extending insertHyphenBeforeKnownMarker (or KNOWN_PRE_RELEASE_PREFIXES logic) to
treat a tail starting with 'm' followed by a digit as a known marker (e.g.,
detect /^m\d+/ on the lowercased tail and insert the hyphen) or update the KDoc
to explicitly document the divergence from PRE_RELEASE_MARKER_PATTERN so
behaviour is intentional.

---

Outside diff comments:
In
`@core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/util/VersionMath.kt`:
- Around line 152-169: Update the KDoc for isExactSameVersion to reflect the
current normalization behavior: mention that stripCommonPrefixes now delegates
to stripFullPrefix and that word-prefixes (case-insensitive) such as "version-",
"release/", "app_", "build-", "ver." in addition to "refs/tags/" and leading
"v"/"V" are stripped during cleanup; clarify that this function remains
case-sensitive for suffixes and does NOT remove +build metadata or extract
dotted-digit cores, and keep the note that two null/blank inputs return false.
Reference isExactSameVersion, stripCommonPrefixes, and stripFullPrefix in the
doc so readers can find the exact helpers implementing the behavior.
- Around line 344-423: The KDoc for isPreReleaseTag (and its heuristic details)
became orphaned because detectScheme and the Scheme enum were inserted between
the comment and the function; move detectScheme and the Scheme enum (and their
KDoc) below isPreReleaseTag (and its related preReleaseMarkerLabel) so the
original KDoc immediately precedes isPreReleaseTag again, or alternatively swap
the two KDoc blocks and reposition the isPreReleaseTag function so its rich
heuristic KDoc is directly above the isPreReleaseTag declaration; ensure
references to detectScheme and Scheme remain unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: b8c78528-844b-46e8-9b2d-3c8bdc3aa2ed

📥 Commits

Reviewing files that changed from the base of the PR and between 9dbce7a and 8b8aa20.

📒 Files selected for processing (1)
  • core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/util/VersionMath.kt

@rainxchzed rainxchzed merged commit f2c9804 into main May 8, 2026
1 check passed
@rainxchzed rainxchzed deleted the feat/version-detection-robust branch May 8, 2026 15:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant