dsh-semver
Semantic versioning toolbox for DeepSeek Harness (dsh) — parse, compare, range-check, increment, diff, sort, find a range's lowest version and check whether two ranges overlap. Zero runtime dependencies, pure logic, fully deterministic.
Agents are bad at version arithmetic. "Is 2.1.0-rc.1 newer than 2.1.0-beta.11?", "does 1.2.4-beta.1 satisfy ^1.2.3?" and "what does 1.2.4-beta.0 + prerelease become?" are exactly the questions LLMs get wrong — prerelease ordering, tuple-lock, caret/tilde upper bounds and prerelease bump rules are easy to misremember. This plugin turns those questions into deterministic tool calls.
Tools
| Tool | Description |
|---|
semver_parse | Parse a semver 2.0.0 string into major / minor / patch / prerelease / build, with a precise rejection reason for malformed input (1.2, 1.2.3.4, leading zeros, 1.2.3-01, …) |
semver_compare | Compare two versions → lt / eq / gt plus a human verdict (2.1.0 > 2.0.3). Build metadata is ignored for precedence (1.0.0+build.1 == 1.0.0) |
semver_satisfies | Check a version against an npm-style range, with the matched range normalized in the explanation |
semver_inc | Increment a version following npm semver.inc rules: major / minor / patch / premajor / preminor / prepatch / prerelease, with an optional prerelease identifier |
semver_diff | Release-type difference between two versions (npm semver.diff): major / minor / patch / premajor / preminor / prepatch / prerelease |
semver_sort | Sort a list of versions by precedence, ascending or descending, with build metadata as tie-break (npm semver.sort / rsort) |
semver_min_version | Lowest version a range accepts (npm semver.minVersion): ~1.2.3 → 1.2.3, >1.2 → 1.3.0, <=1.2.3 → 0.0.0; unsatisfiable combinations report satisfiable: false instead of a plausible-looking bound |
semver_intersects | Whether two ranges can be satisfied by one version (npm semver.intersects) — plus a witness version that satisfies both when they overlap |
semver_min_version / semver_intersects — range algebra (v0.3.0)
Two questions that come up whenever an agent has to negotiate versions:
semver_min_version("^1.2.3") → 1.2.3
semver_min_version(">1.2.3") → 1.2.4 (> bound with no prerelease bumps patch)
semver_min_version(">1.2") → 1.3.0 (partial versions follow npm's x-range rule)
semver_min_version("~1.2.3") → 1.2.3
semver_min_version("<=1.2.3") → 0.0.0 (no lower bound → the floor)
semver_min_version(">2.0.0 <2.0.1") → satisfiable: false (never a guessed bound)
semver_intersects("^1.2.3", "^1.3.0") → true, witness 1.3.0
semver_intersects("^1.0.0", "^2.0.0") → false
semver_intersects("^1.0.0 || ^2.0.0", "^2.5.0") → true
satisfiable: false is a result, not an error. Unsatisfiable combinations (^0.1.4 ~3.0.2, >=1.0.0 <1.0.0, >2.0.0 <2.0.1) report that no version fits, rather than returning a boundary that looks reasonable but matches nothing.
- The witness is verified, never guessed. When two ranges overlap,
semver_intersects probes the ranges' boundary versions and checks each candidate against both ranges before reporting it — so a reported witness always satisfies both, though it is not claimed to be the lowest one.
- npm parity is enforced against npm itself.
npm semver renders ~1.2.3 as >=1.2.3 <1.3.0; a hand-written expectation would have missed that 1.2.0 used to be accepted. See Testing below.
semver_inc — npm increment semantics (v0.2.0)
- Plain bumps:
1.2.3 + major → 2.0.0, + minor → 1.3.0, + patch → 1.2.4.
- Pre bumps with identifier:
1.2.3 + preminor (rc) → 1.3.0-rc.0; without identifier → 1.3.0-0.
- Prerelease counter:
1.2.4-beta.0 + prerelease → 1.2.4-beta.1; 1.2.4-beta + prerelease → 1.2.4-beta.0; switching identifier restarts the counter (prerelease with rc → 1.2.4-rc.0).
- npm quirks preserved: a pre-major/pre-minor/pre-patch version releases without incrementing (
1.0.0-5 + major → 1.0.0); 1.2.0-5 + prerelease → 1.2.0-6 (no patch bump); build metadata is dropped; dotted identifiers are allowed (premajor with beta.1 → 2.0.0-beta.1.0); identifiers with leading zeros are rejected.
semver_diff — npm difference semantics (v0.2.0)
1.2.3 → 2.0.0 = major; 1.2.3 → 2.0.0-beta.1 = premajor; 1.2.4-beta.1 → 1.2.4-beta.2 = prerelease. npm special cases preserved: 1.2.3 vs 1.2.3-beta.1 → patch, 1.0.0-1 vs 1.0.0 → major. Equal precedence (build metadata ignored) → equal: true with no difference key.
Supported range syntax (npm-compatible subset)
- Exact:
1.2.3, =1.2.3
- Comparisons:
>1.2.0, >=1.2.0, <2.0.0, <=2.0.0 — partial versions follow npm's x-range rewrite (>1.2 → >=1.3.0, <=1.2 → <1.3.0, <1.2 → <1.2.0, >=1.2 → >=1.2.0; npm compiles >* / <* to a range nothing satisfies)
- Caret:
^1.2.3 → >=1.2.3 <2.0.0, with the 0.x rules (^0.2.3 → >=0.2.3 <0.3.0, ^0.0.3 → >=0.0.3 <0.0.4)
- Tilde:
~1.2.3 → >=1.2.3 <1.3.0, ~1.2 → >=1.2.0 <1.3.0, ~1 → >=1.0.0 <2.0.0 (fixed in v0.3.0 — the patch component used to be dropped from the lower bound, so 1.2.0 wrongly matched ~1.2.3)
- Wildcards:
1.2.x, 1.x, * (bare partials 1.2 / 1 behave the same)
- Hyphen ranges:
1.2.3 - 2.3.4 (inclusive endpoints; partial upper endpoints like 1.2.3 - 2.3 → <2.4.0)
- AND / OR:
>=1.2.0 <2.0.0, ^1.2.3 || >=3.0.0
Prerelease semantics (npm tuple-lock)
- A plain range never matches prerelease versions:
1.2.4-beta.1 does not satisfy ^1.2.3.
- A range with a prerelease lower bound only matches prereleases of the same
[major, minor, patch] tuple: 1.2.3-beta.5 satisfies ^1.2.3-beta.2, but 1.2.4-beta.1 does not (releases like 1.2.3 still match).
- Set
includePrerelease: true (tool parameter or plugin config) to relax both rules.
Install
# into the web profile (dsh plugin has no default profile — always pass --profile)
dsh plugin --profile web add github:TYEclipse/dsh-semver
Verify the layer mounted:
dsh --profile web --dump-config | grep '== dsh-semver'
Usage examples
semver_parse("2.1.0-rc.1+build.42")
→ major 2, minor 1, patch 0, prerelease [rc, 1], build [build, 42]
semver_compare("2.1.0", "2.0.3")
→ "2.1.0 > 2.0.3" (gt)
semver_satisfies("1.2.4-beta.1", "^1.2.3")
→ false — prerelease does not match a plain range
semver_inc("1.2.4-beta.0", "prerelease", "beta")
→ 1.2.4-beta.1
semver_inc("1.2.3", "preminor", "rc")
→ 1.3.0-rc.0
semver_diff("1.2.3", "2.0.0-beta.1")
→ premajor
semver_sort(["1.2.3", "1.2.10", "1.0.0"], "desc")
→ ["1.2.10", "1.2.3", "1.0.0"]
semver_min_version(">=1.2.0 <2.0.0")
→ 1.2.0
semver_intersects("^1.2.3", ">=1.3.0 <1.5.0")
→ true — e.g. 1.3.0 satisfies both
Safety
All eight tools are pure and offline: no network, no filesystem access, no shell, no dynamic evaluation. Inputs are validated with strict regexes and numeric checks; malformed versions, ranges and identifiers return explicit reasons instead of throwing.
Testing
pnpm test # vitest — 132 tests
Tool semantics are anchored to npm's own semver package as an independent oracle, never to hand-written expectations:
test/oracle/range-algebra-oracle.mjs # regenerates the anchors from npm semver
test/anchors/range-algebra.json # 54 ranges · 400 range pairs · 864 matcher cases
With npm i semver@7 available, node test/oracle/range-algebra-oracle.mjs rewrites the anchors; the test suite sweeps the plugin's matcher, semver_min_version and semver_intersects against every one of them. The ~1.2.3 lower-bound bug fixed in v0.3.0 was found exactly this way — its v0.2.x expectation had been chosen by hand and sat outside the wrong bound.
Development
pnpm install # dev dependencies only (esbuild allowBuilds is pre-configured)
pnpm build # tsc → dist/ (committed; git installs do not run build scripts)
pnpm test # vitest — 132 tests; anchors generated by npm semver 7.8.5 (independent oracle)
pnpm lint # oxlint (src + test only)
License
MIT
中文简介
dsh-semver 是 DeepSeek Harness 的语义化版本号工具箱:解析(semver_parse)、比较(semver_compare,遵循 semver 2.0.0 优先级规则,build 元数据不影响比较)、范围匹配(semver_satisfies,支持 ^/~/>=/<=/通配符/连字符区间/||,含 npm 的 prerelease tuple-lock 语义),以及 v0.2.0 新增的版本号递增(semver_inc,npm semver.inc 全规则:major/minor/patch/premajor/preminor/prepatch/prerelease + 可选预发布标识符)、版本差异判定(semver_diff,npm semver.diff 语义,含 prerelease 特殊分支)与版本列表排序(semver_sort,升/降序,build 元数据作次序裁决)。零运行时依赖、纯逻辑、确定性输出——LLM 经常算错的版本号问题(如 1.0.0-beta.11 > 1.0.0-beta.2、1.2.4-beta.0 递增后是 beta.1 而非 beta.01、1.2.4-beta.1 不满足 ^1.2.3)交给工具一次性算对。