Skip to content

Commit 804619f

Browse files
committed
docs(vers): add version-range specifier doc + pre-standard caveats
Covers the what/why of VERS, the 9 semver-aliased schemes we implement today, the Vers class API (parse, contains, toString), and the OR-semantics hazard that catches hand-writers. Prominent "pre-standard" warning up front — the spec is pre-Ecma submission (late 2026) and may still shift. Callers signing up get a clear-eyed list of limits: MAX_CONSTRAINTS=1000 cap, OR-not-AND across constraints, frozen instances, comparison-only semver schemes. Junior-dev level: explains why VERS exists before showing syntax, cheat-sheet for hand-writing, explicit "what VERS doesn't handle" section.
1 parent 5d613e1 commit 804619f

2 files changed

Lines changed: 288 additions & 0 deletions

File tree

docs/vers.md

Lines changed: 282 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,282 @@
1+
# VERS
2+
3+
**VE**rsion **R**ange **S**pecifiers — the pre-standard grammar for
4+
saying "any version of package X that matches this range" in a way
5+
every ecosystem can parse the same way. This doc covers what VERS is,
6+
how to read and write it, how this library implements it, and what
7+
"pre-standard" means for you as a consumer.
8+
9+
## Who this is for
10+
11+
Contributors who want to understand the `Vers` class before changing
12+
it, and callers who want to know whether VERS is stable enough to
13+
depend on in their own code.
14+
15+
## What VERS is
16+
17+
Today, every package ecosystem has its own version-range syntax:
18+
19+
- **npm** uses `^1.2.3`, `~1.0`, `>=1.0 <2.0` (npm semver).
20+
- **pypi** uses `>=1.0,<2.0`, `~=1.0.0` (PEP 440).
21+
- **cargo** uses `^1.2.3` but with different prerelease rules.
22+
- **maven** uses `[1.0,2.0)` (interval notation).
23+
- **composer** uses `^1.2.3 || ~2.0`.
24+
- **nuget** uses `[1.0,2.0)` (similar to maven, different edges).
25+
26+
A tool that consumes all of them — a vulnerability scanner, an
27+
SBOM tool, Socket.dev itself — has to implement and maintain eight
28+
different parsers just to answer "does version 1.4.2 satisfy this
29+
range?"
30+
31+
VERS proposes a single grammar that any consumer can parse, with
32+
a `scheme` field telling you how to *compare* versions within the
33+
range (semver semantics, PEP 440 semantics, etc.):
34+
35+
```
36+
vers:<scheme>/<constraint>[|<constraint>…]
37+
```
38+
39+
Each `<constraint>` is a comparator (`=`, `!=`, `<`, `<=`, `>`,
40+
`>=`) followed by a version string, or the wildcard `*`.
41+
42+
Constraints inside a single VERS are **ORed**. To express AND, use
43+
multiple VERS in your own logic (the spec is deliberately simple
44+
here — it doesn't try to encode every operator every ecosystem has).
45+
46+
## Worked examples
47+
48+
```
49+
vers:npm/>=1.0.0|<2.0.0
50+
```
51+
52+
"Any npm version ≥ 1.0.0, OR any npm version < 2.0.0." (OR semantics
53+
across constraints — note this matches almost everything; the example
54+
is intentionally showing the grammar, not a useful range.)
55+
56+
```
57+
vers:pypi/>=1.0,<2.0
58+
```
59+
60+
Same shape, pypi semantics. The `scheme` (`pypi`) tells the parser
61+
how to compare "1.0.0a1" vs "1.0.0" (PEP 440: prereleases sort
62+
before release; semver: same, but "1.0.0-a" form).
63+
64+
```
65+
vers:cargo/^1.2.3
66+
```
67+
68+
Cargo's caret — any version ≥ 1.2.3 and < 2.0.0.
69+
70+
```
71+
vers:semver/*
72+
```
73+
74+
Wildcard — matches any semver version.
75+
76+
```
77+
vers:npm/=1.2.3
78+
```
79+
80+
Exact match — only version 1.2.3 satisfies.
81+
82+
```
83+
vers:npm/>=1.0.0|!=1.3.5|<2.0.0
84+
```
85+
86+
"≥ 1.0.0 OR not 1.3.5 OR < 2.0.0" — again, grammar demo; the OR
87+
semantics make this permissive. Real policies usually fit in two
88+
constraints.
89+
90+
## The pre-standard caveat
91+
92+
VERS is **not finalized**. The spec lives at
93+
[package-url/vers-spec](https://github.com/package-url/vers-spec)
94+
with Ecma submission planned for **late 2026**. That means:
95+
96+
- **Grammar may change.** The comparator set, wildcard semantics, or
97+
scheme names could shift before ratification. This library tracks
98+
the spec; we will land breaking changes in sync with the spec,
99+
not ahead of it.
100+
- **Some ecosystems aren't covered yet.** The library today
101+
implements the semver scheme and its common aliases (see below).
102+
Schemes like PEP 440, maven, and nuget are planned but not yet
103+
implemented — the grammar parses, but comparison under those
104+
schemes would throw.
105+
- **Use cautiously in hot paths.** If your product hinges on VERS
106+
behavior, review every release's changelog for spec-driven
107+
changes. We flag them prominently.
108+
109+
If you need a stable version-range system *today*, use the native
110+
one for your ecosystem. VERS is for tooling that spans ecosystems
111+
and is willing to absorb some pre-standard churn in exchange for
112+
uniformity.
113+
114+
## Supported schemes
115+
116+
This library's `Vers` class currently supports the **semver
117+
comparison scheme** and its common aliases:
118+
119+
| Scheme | Notes |
120+
|---|---|
121+
| `semver` | Reference semver 2.0.0 comparison |
122+
| `npm` | Same as semver (npm follows semver) |
123+
| `cargo` | Same as semver (cargo follows semver, with pre-release tail differences) |
124+
| `golang` | Same as semver |
125+
| `hex` | Same as semver (Elixir/Erlang) |
126+
| `pub` | Same as semver (Dart) |
127+
| `cran` | Same as semver |
128+
| `gem` | Same as semver |
129+
| `swift` | Same as semver |
130+
131+
Unsupported-but-declared schemes (`pypi`, `maven`, `nuget`, `deb`,
132+
`rpm`, …) parse as VERS grammar but throw when a comparison is
133+
attempted. If you need one, open an issue or PR — the scheme table
134+
is a single-line addition plus the comparison function.
135+
136+
## The `Vers` class
137+
138+
Located at `src/vers.ts`.
139+
140+
```typescript
141+
class Vers {
142+
readonly scheme: string
143+
readonly constraints: readonly VersConstraint[]
144+
145+
static parse(versStr: string): Vers
146+
static fromString(versStr: string): Vers
147+
148+
contains(version: string): boolean
149+
toString(): string
150+
}
151+
```
152+
153+
### Parsing
154+
155+
Two synonymous entry points — `Vers.parse('vers:npm/>=1.0.0|<2.0.0')`
156+
or `Vers.fromString(...)`. Both:
157+
158+
1. Verify the string starts with `vers:`.
159+
2. Split on `/` to extract the scheme.
160+
3. Split the constraint list on `|`.
161+
4. For each constraint, extract the comparator (longest-match greedy
162+
against the COMPARATORS table).
163+
5. Validate the comparator + version combination.
164+
6. Freeze the resulting `Vers` instance (immutable, per the
165+
hardening doctrine — see `docs/hardening.md`).
166+
167+
Failure modes, all throwing `PurlError`:
168+
169+
- Missing `vers:` prefix.
170+
- Empty scheme or empty constraints.
171+
- Unknown comparator.
172+
- Invalid version string for the scheme.
173+
- Too many constraints (capped at `MAX_CONSTRAINTS = 1000` to
174+
prevent resource-exhaustion inputs).
175+
176+
### Matching
177+
178+
`vers.contains(version)` returns `true` if at least one constraint
179+
in the VERS accepts the version. For the semver scheme that means:
180+
181+
- `=``compareSemver(range.version, v) === 0`
182+
- `!=``compareSemver(range.version, v) !== 0`
183+
- `<` / `<=` / `>` / `>=` — the obvious comparisons
184+
- `*` — always true
185+
186+
```typescript
187+
const range = Vers.parse('vers:npm/>=1.0.0|<2.0.0')
188+
range.contains('1.5.0') // true
189+
range.contains('2.5.0') // true (matches the >=1.0.0 constraint)
190+
range.contains('0.9.0') // true (matches <2.0.0)
191+
// (OR semantics — most versions satisfy this particular range)
192+
```
193+
194+
Prerelease ordering follows semver 2.0.0:
195+
196+
```typescript
197+
compareSemver('1.0.0-alpha', '1.0.0') // -1 (alpha precedes)
198+
compareSemver('1.0.0-alpha.1', '1.0.0-alpha.2') // -1 (numeric compare)
199+
compareSemver('1.0.0-alpha.1', '1.0.0-alpha') // 1 (longer > shorter)
200+
```
201+
202+
Build metadata (`+xyz`) is ignored in comparisons, per semver.
203+
204+
### Round-tripping
205+
206+
`vers.toString()` reproduces a canonical string form:
207+
208+
```typescript
209+
const v = Vers.parse('vers:npm/>=1.0.0|<2.0.0')
210+
v.toString() // 'vers:npm/>=1.0.0|<2.0.0'
211+
```
212+
213+
Round-tripping is lossless — `Vers.parse(v.toString())` always
214+
produces an equivalent VERS. (Not byte-identical if the input had
215+
redundant whitespace; the string form strips.)
216+
217+
## Writing a VERS string by hand
218+
219+
Cheat sheet for the most common patterns:
220+
221+
| Intent | VERS |
222+
|---|---|
223+
| Exactly version X | `vers:npm/=X` |
224+
| Any version | `vers:npm/*` |
225+
| ≥ X | `vers:npm/>=X` |
226+
| Strict greater than X | `vers:npm/>X` |
227+
| Everything except X | `vers:npm/!=X` |
228+
| "X inclusive through Y exclusive" (intent: `[X,Y)`) | **Not expressible directly** — VERS uses OR between constraints; use your ecosystem's native range or pair a lower bound with a validator. |
229+
230+
Note the last row — **VERS constraints OR together**, so writing
231+
`>=1.0|<2.0` does not mean "≥1.0 AND <2.0" (the intuition from npm
232+
semver), it means "≥1.0 OR <2.0" (everything). This is the biggest
233+
hazard in writing VERS by hand, and a source of "this range matches
234+
way more than I expected" bugs.
235+
236+
If your use case needs AND-of-constraints, express it as multiple
237+
separate VERS on your end and AND them in your consumer code.
238+
239+
## Why we implement VERS
240+
241+
Socket consumes SBOMs from every ecosystem. Every new ecosystem-
242+
specific range syntax is new parser surface to write, test, and
243+
keep in sync with upstream rules. VERS is a bet that consolidating
244+
into one parser is worth the pre-standard risk. If VERS ratifies as
245+
Ecma-NNN, every range-aware tool gets one import instead of eight.
246+
247+
We ship it under a `pre-standard` tag so callers know what they're
248+
signing up for.
249+
250+
## Limits and hazards
251+
252+
Read these before relying on VERS in production:
253+
254+
- **MAX_CONSTRAINTS = 1000.** A VERS with more than 1000 `|`-
255+
separated constraints fails to parse. This is a hard cap to
256+
prevent resource-exhaustion inputs; if you have a legitimate use
257+
case for more, open an issue with the scenario.
258+
- **MAX string length: not enforced here.** Callers receiving VERS
259+
strings from the wire should size-limit at the boundary.
260+
- **OR semantics surprise.** As noted above — hand-written ranges
261+
with multiple constraints often mean what the author intended
262+
(AND) but match what they wrote (OR). A linter rule for
263+
"VERS with ≥2 constraints should be reviewed" is not a bad idea
264+
in consumer code.
265+
- **Scheme table is small.** We implement 9 semver-aliased schemes.
266+
Others parse but fail to compare.
267+
- **Exact-equality with prereleases:** `=1.0.0` does **not** match
268+
`1.0.0-alpha` under semver rules (the latter precedes the
269+
former). If you want prerelease-inclusive exact match, use
270+
`>=1.0.0-alpha` with a matching upper bound.
271+
272+
## Further reading
273+
274+
- [`docs/architecture.md`](./architecture.md) — where `vers.ts`
275+
fits in the module map.
276+
- [`docs/hardening.md`](./hardening.md) — why `Vers` instances are
277+
frozen and constraint-count capped.
278+
- [`src/vers.ts`](../src/vers.ts) — the implementation.
279+
- [package-url/vers-spec](https://github.com/package-url/vers-spec)
280+
— the upstream spec this library tracks.
281+
- [semver.org](https://semver.org/) — the version comparison
282+
semantics the default scheme follows.

tour.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,12 @@
184184
"title": "Tour",
185185
"source": "docs/tour.md",
186186
"summary": "How the tour site is built, from meander submodule through post-process to GitHub Pages deploy."
187+
},
188+
{
189+
"filename": "vers",
190+
"title": "VERS",
191+
"source": "docs/vers.md",
192+
"summary": "Version-range specifiers — the pre-standard grammar slated for Ecma submission in late 2026."
187193
}
188194
]
189195
}

0 commit comments

Comments
 (0)