sparkles:versions — Delivery Plan
Audience: contributors implementing the library. This document is execution-only — milestones, the dynamic-workflow orchestration that builds them, verification, and deferrals. For the desired-state specification, read SPEC.md; for design history and prior-art justification, read the explanation guides; for the per-scheme catalogue, read the scheme catalogue.
The library is a set of hand-written, per-ecosystem version structs conforming to compile-time concepts — a required isVersion!T plus an orthogonal optional capability vocabulary — with generic algorithms (Ranges!V, VERS interop, purl dispatch) layered as fallback/fast-path shells over those capabilities. The milestones below build that surface bottom-up; each one is realised as a single dynamic-workflow invocation.
1. Milestone overview
| # | Deliverable | Depends on |
|---|---|---|
| M1 | Foundation (traits, parse-error types, salvaged internals) + ten version schemes including the Generic void-hook baseline | — |
| M2 | Ranges!V set-algebra + per-scheme native range parsers + prerelease-in-range policy | M1 |
| M3 | VERS URI interop (parseVersUri/formatVersUri) + per-scheme constraint mapping + compile-time scheme registry | M1, M2 |
| M4 | purl parser + schemeForPurlType / parsePurlVersion dispatch + non-identity purl→scheme table | M1 |
| M5 | AnyVersion/AnyRange sum types + compareAny + README rewrite + DDoc polish | M1–M4 |
| M6 | Documentation rewrite (SPEC, RATIONALE, PRESETS, PLAN) | M1–M5 |
M1 is the bulk of the work. M2/M4 are independent of each other (both depend only on M1); M3 layers on top of M2; M5 integrates everything; M6 documents the shipped state.
2. Per-milestone detail
Each milestone's outcome is the relevant SPEC sections compiling, passing their unit tests, and documented per AGENTS.md. The detail below is execution-focused — which files ship and in what dependency order — and does not re-specify the API; follow the linked SPEC sections for behaviour.
M1 — Foundation + ten schemes
Replaces the deleted DbI engine (engine.d, parser.d, layouts.d, presets.d, semver_rules.d — Version!Layout, layoutBody, LayoutDescriptor, @Component/@InternalFlag/@StringSlot, and the descriptor-walking parser) with the concept-based surface from SPEC §3 (The Version concept), SPEC §4 (The Range concept), and SPEC §6 (The Scheme concept).
base text prelude — foundation primitives are generic, so they live in the sparkles.base.text package, not in a versions/_internal. Done as four atomic commits, each building green (dub test :base and dub test :core-cli) before the next:
- Refactor
text_writers→text.writers. Movetext_writers.d→text/writers.d(module sparkles.base.text.writers;), add atext/package.d(module sparkles.base.text;) re-exporting the package's modules, and update the importers (logger,prettyprint,styled_template). Pure move — no behaviour change. - Add
writeIntegerPadded(w, val, minDigits)totext.writers, and refactorlogger.writePadded2onto it (it already hand-rolls the same logic). - Add
sparkles.base.text.errors— the genericParseError {code, offset},ParseErrorCode,ParseExpected!T = Expected!(T, ParseError, NoGcHook). Add theexpecteddependency tobase'sdub.sdl. - Add
sparkles.base.text.readers—readInteger,skipWhile,tryConsume/tryConsumeAny,readUntil— slice-advance (ref scope const(char)[]),@safe pure nothrow @nogc.readIntegerreturnsParseExpected!Tand is constrainedif (isUnsigned!T).
Salvage into versions (move, do not rewrite):
ParseMode→sparkles.versions.parsing(re-exports thebaseparse types). The oldParseError/ParseErrorCode/ParseExpectedare superseded by the genericbaseversions above — not salvaged.compareSemVerPrerelease,validateIdentifierList,IdentifierKind→package-scoped inschemes/semver.d, reused byschemes/dmd.d(and any other SemVer-shaped scheme).putPaddedNumber→ superseded bybase.text.writers.writeIntegerPadded.checkParse/checkRoundTrip/checkRejects/checkAscending→sparkles.versions.testing(version(unittest)).
Write:
traits.d—isVersion!T,isVersionRange!R,isVersionScheme!S, and the optional capability vocabulary (hasOrderKey,supportsPrerelease,hasComponents,hasBuildMetadata,supportsNativeRange,supportsLooseParse), with unit tests asserting conformance against every shipped scheme.The ten scheme structs under
schemes/:Module Struct purl type Capabilities beyond isVersionschemes.genericGenericgenericnone (void-hook baseline) schemes.semverSemVersemverorderKey, prerelease, components, build schemes.dmdDmd(D-internal) orderKey, prerelease, components schemes.dmd_compactDmdCompact(D-internal) orderKey, prerelease, components schemes.tinyTiny(internal) orderKey, components schemes.calver_yymmCalVerYYMM(internal) orderKey, components schemes.calver_yyyymmddCalVerYYYYMMDD(internal) orderKey, components schemes.vimVimVer(internal) orderKey, components schemes.pypiPypiVersionpypiprerelease, components schemes.mavenMavenVersionmavenprerelease schemes.debDebianVersiondebprerelease SemVer is the reference scheme, written first. Dmd / DmdCompact / Tiny / CalVer* / Vim port their compare-and-format logic directly from the old layout structs (no generation). PyPI / Maven / Debian are new: each implements
opCmp,toString,parse, a stubbedparseNativeRange(filled in M2), and a real-world fixture test.schemes/package.d+ rootpackage.dre-exports;dub.sdlsource paths updated.
Each scheme module ends with static assert(isVersion!ThisStruct && isVersionScheme!ThisStruct);, so any conformance regression is a compile error. The Generic scheme is an opaque lexicographic string compare declaring zero optional capabilities — it exists to exercise every generic algorithm's fallback path.
Key files: base text/{writers,readers,errors}.d + text/package.d; versions traits.d, parsing.d, all eleven schemes/*.d, schemes/package.d, package.d, testing.d.
M2 — Ranges!V + native range parsers
Implements the Range concept and the generic operations over it, plus the per-scheme native range grammars.
ranges.d—Ranges!V, the concrete sorted, disjoint interval set per SPEC §4.2. Implements the full method surface and bound representation specified there; no API is re-listed here.parseNativeRangeadditions per non-trivial scheme, implementing each ecosystem's grammar as catalogued in the scheme catalogue §3. The six SemVer-shaped internal schemes inherit the SemVer grammar.- Prerelease-in-range policy encoded and tested per scheme, gated on the
supportsPrereleasecapability (the node-semver convention specified in SPEC §5.2).
Key files: ranges.d, the parseNativeRange additions to each scheme, a Ranges!V property-test module.
M3 — VERS URI interop
Implements SPEC §9 (VERS interop).
vers.d—VersUri,parseVersUri,formatVersUrihandling the URI surface only (ASCII-only, lowercase scheme, sort + dedupe constraints), plus the static-dispatchparseVersAs!Schemetemplate and runtimeparseVersAnyoverAnyRange.- Per scheme:
fromVersConstraint(segment →Range) andtoVersConstraint(Range→ segment), with the native-operator → VERS-operator map as per-scheme static data, gated onsupportsNativeRange. schemes/package.dgains the CTFE registry keyed bypurlType.
Key files: vers.d, the constraint methods on each scheme, the registry in schemes/package.d.
M4 — pURL parser + AnyVersion/AnyRange + runtime dispatch
Implements SPEC §10 (pURL interop) and SPEC §11 (AnyVersion / AnyRange). The sum types were pulled forward from M5 because the runtime dispatch entry points (parsePurlVersion, and the M3-deferred parseVersAny) return them — so M4 ships them rather than stubbing.
any.d—AnyVersion/AnyRangeSumTypes over all eleven schemes (derived from the registry scheme list) andcompareAnyreturningNullable!int(null when schemes differ).purl.d—PackageUrl,parsePurl(parse only; we consume purls, we do not mint them);purlTypeToSchemeName(the non-identity purl-type→scheme map, e.g. npm/cargo/.../packagist→semver); andparsePurlVersionreturningAnyVersion.parseVersAny(invers.d) — runtime VERS dispatch →AnyRange, closing the M3 deferral.
Key files: any.d, purl.d, parseVersAny in vers.d, schemes/registry.d.
M5 — README + DDoc
Brings the public surface to release quality (the AnyVersion/AnyRange sum types landed in M4).
- README rewrite from "DbI engine" to "ecosystem-aware version library", with three runnable examples: per-ecosystem parse-and-compare; parse a
vers:URI and test satisfaction; parse a purl, dispatch on type, parse the version, check against a range. - DDoc on every public symbol per
docs/guidelines/ddoc.md.
Key files: README.md, DDoc across all public modules.
M6 — Documentation rewrite
Rewrites all four docs to the shipped design (this is one of them). SPEC is rebuilt around the three concepts, the per-scheme module convention, Ranges!V, VERS, and purl; RATIONALE replaces the DbI-engine narrative with the pubgrub/univers/Aether prior-art findings; PRESETS retargets to per-scheme notes (capabilities, edge cases, provenance, how to add a scheme); PLAN becomes this M1–M6 outline. The source-material/ catalogues are inputs and stay untouched.
Key files: docs/specs/versions/{SPEC,RATIONALE,PRESETS,PLAN}.md.
3. Execution via dynamic workflows
Each milestone is implemented as one Workflow invocation, run in sequence so the result of one informs the next. Within a milestone, agents fan out over independent units of work — one module each — and converge through a serial build-and-fix loop.
Cross-cutting conventions (all milestones)
- No worktree isolation. Fan-out agents write disjoint files (one module each), so parallel writes never conflict. The only serialised work is the build/test loop, which runs the compiler and must be a single agent at a time.
- Schema-validated agent output. Every agent that yields structured data — file manifests, build-error reports, conformance verdicts, review findings — returns a JSON-schema-validated object, so the orchestrator branches on data, not prose.
- Serial build-fix convergence loop. After each fan-out, a
whileloop spawns one build agent per turn that runsnix develop -c dub test :versions, captures the first error cluster into a schema, fixes it, and repeats until green or the iteration cap is hit (then it reports residual errors back to the orchestrator). Schemes share one dub build and cannot compile in isolation, so the write-phase agents do not build — this loop is the single compile authority after the fan-out barrier. - Adversarial verification on the hard schemes. PyPI (PEP 440), Maven (qualifier order), and Debian (epoch/upstream/revision) each get an independent skeptic that tries to refute the comparison logic against authoritative test vectors (PEP 440 spec examples,
dpkg --compare-versionsrules, MavenComparableVersionordering) before the scheme is accepted. The seven SemVer-shaped schemes get a single-vote review. - Completeness critic. M1 and M6 end with an agent asking "what's missing — a scheme without a conformance assert, a public symbol without DDoc, a doc with a stale
layoutBodyreference?" and feeds the answer into a final fix round.
WF-M1 — foundation-and-schemes
phase('Foundation') // parallel barrier — disjoint files
├─ delete the DbI engine surface; salvage parse_error.d
├─ salvage _internal/{identifier_rules,compare_semver,format,test_helpers}.d
├─ write traits.d (isVersion / isVersionRange / isVersionScheme + capabilities)
└─ write ranges.d skeleton (type + isVersion-satisfying surface)
phase('Schemes') // pipeline over the 10 scheme modules
stage 1 write: one agent per scheme → schemes/<purlType>.d + conformance
static assert + real-world round-trip tests
stage 2 review: adversarial correctness review (3 hard schemes get the
refute-skeptic; 7 SemVer-shaped + Generic get 1 vote)
phase('Integrate') // single agent
└─ schemes/package.d + root package.d re-exports + dub.sdl source paths
phase('Build & fix') // serial while-loop, single build agent per turn
phase('Completeness') // single critic agentWF-M2 — ranges-and-native-parsers
phase('Ranges') complete Ranges!V set-algebra + property tests
(De Morgan, idempotence, absorption, subset/disjoint)
phase('Native parsers') pipeline over {semver/npm, maven, pypi, deb}:
write parseNativeRange → review. The 6 internal
SemVer-shaped schemes inherit the SemVer grammar.
phase('Prerelease policy') encode + test the node-semver
prerelease-in-range rule per scheme
phase('Build & fix') serial loop
phase('Verify laws') adversarial: confirm property tests are not vacuousWF-M3 — vers-interop
phase('VERS URI') vers.d parser + emitter (ASCII/lowercase/sort/dedupe)
phase('Constraints') pipeline per scheme: from/toVersConstraint + operator map
phase('Registry') CTFE purlType→module registry in schemes/package.d
phase('Fixtures') extract round-trip corpus from
/home/petar/code/repos/univers/tests/data
phase('Build & fix') serial loop
phase('Round-trip') adversarial: native → Range → VERS → Range equalityWF-M4 — purl-dispatch
phase('purl parser') purl.d (type/namespace/name/version/qualifiers/subpath)
phase('Dispatch') schemeForPurlType template + parsePurlVersion → AnyVersion
+ non-identity purl→scheme mapping table
phase('Conformance') official purl-spec test suite if vendored, else curated
phase('Build & fix') serial loopWF-M5 — any-and-polish
phase('Sum types') any.d (AnyVersion/AnyRange + compareAny → Nullable!int)
phase('README') rewrite README with 3 runnable examples
phase('DDoc') parallel: one agent per public module adds DDoc
phase('Verify') serial loop: nix run .#ci -- --verify --files README.mdWF-M6 — docs-rewrite
phase('Rewrite') parallel barrier — 4 disjoint files:
SPEC.md, RATIONALE.md, PRESETS.md, PLAN.md
phase('Consistency') critic: zero remaining references to layoutBody /
LayoutDescriptor / Version!Layout / StringSlot in any
.md or .d under docs/specs/versions or libs/versions4. Verification
Per milestone:
M1.
nix develop -c dub test :versionsexits 0. Each scheme module carriesstatic assert(isVersion!ThisStruct && isVersionScheme!ThisStruct);, so any conformance regression is a compile-time failure. Each scheme has at least one parse-and-round-trip test (checkRoundTrip!Scheme("real-world-string")). The real-world catalogue from the scheme catalogue — Python3.13.0a1, Maven1.0-SNAPSHOT, Debian2:4.13.1-0ubuntu0.16.04.1.1~, and the rest — is exercised end-to-end.M2. A property-test module asserts the set-algebra laws for
Ranges!Vover hand-constructed intervals: De Morgan, idempotence, absorption, and thesubsetOf/isDisjointconsistency laws. Each scheme'sparseNativeRangeround-trips a small real-world corpus (npm^1.2.0, Maven[1.0,2.0), PEP 440>=1.2.4,<2, Debian>= 2.0, << 3.0). The prerelease-in-range rule is tested for eachsupportsPrereleasescheme.M3. Round-trip property test per scheme:
parseNativeRange(s) ⟶ toVersConstraint ⟶ fromVersConstraintproduces an equalRange. The VERS URI parser round-trips a corpus pulled from univers'stests/data/(verbatim where semantics match).M4. A test parses
pkg:pypi/[email protected],pkg:npm/[email protected],pkg:deb/debian/[email protected], andpkg:maven/org.apache.commons/[email protected], and checks that each version is parsed by the dispatched scheme. The official purl-spec test suite (vendored or fetched at CI time) runs end-to-end if available; otherwise a curated subset.M5.
nix run .#ci -- --verify --files README.mdpasses for the three runnable examples.dub build :versionssucceeds in release configuration and the documented public API matches the produced symbol surface (verified via a-Xf=docs.jsonintrospection check).M6. All four docs rewritten; no remaining references to
layoutBody,LayoutDescriptor,Version!Layout, orStringSlotin any.mdor.dfile underdocs/specs/versions/orlibs/versions/.
Capability-matrix coverage
Beyond the per-milestone checks, the test suite verifies each optional primitive individually:
hasOrderKey— present on SemVer-shaped schemes, absent onGeneric, PyPI, Maven, Debian; covered by staticstatic assertchecks both ways. Where present, an orderKey-vs-opCmp equivalence test assertssign(a.orderKey <=> b.orderKey) == sign(a <=> b)whenever the keys differ, across the scheme's example corpus.supportsPrerelease— exercised by the prerelease-in-range tests and a directisPrereleaseassertion per scheme that provides it.hasComponents— exercised by the caret/tilde operator tests in M2.hasBuildMetadata— exercised by a build-aware compare test on SemVer.supportsNativeRange/supportsLooseParse— the VERS and loose-parse paths static-if on these, and tests cover both the present and absent branches (theGenericbaseline provides neither and must still compile through every generic algorithm).
5. Out-of-scope deferrals
- Dependency solving. The library ships
Ranges!Vand the set-algebra; it does not ship a pubgrub-style solver. A resolver would importsparkles.versionsand define its ownDependencyProvider— that is a separate library. - Mutation helpers.
bump(releaseType),inc, "next prerelease",withbuilders. The library survey found these rare (3 of 13 libraries) and not part of the abstractVersioncontract. Deferred until a real consumer needs them. - Cross-scheme total order.
compareAnyreturnsNullable!int; there is no universal total order across schemes (Repology'slibversionis the only precedent and is explicitly best-effort). Same policy as univers and the VERS spec. - Runtime scheme registration. All built-in schemes are compile-time-known. A user can add their own scheme by writing a struct conforming to
isVersionScheme!T, but the registry does not accept runtime-installed plugins. - SSO strings. The
string-backed prerelease/build slots stay plain GCstrings. A small-string-optimisedSsoStringdrop-in is deferred until a measured allocation cost justifies it. - Bucket-F pseudo-SemVer. Hyphenless-prerelease schemes (Go
go1.22rc1, Unity2023.2.1f1, OpenSSL1.1.1w, OpenSSH9.7p1) need bespoke numeric→alphanumeric tokenisers; they are not in the M1 ten. PyPI's3.13.0a1is the one such form that ships, handled by the dedicatedPypiVersionparser rather than a generic tokeniser. - Part-2 heavyweight schemes. The part-2 catalogue (entries 26–50: Eclipse
2024-03, Maya2025, iOS21F79, AndroidUP1A.231005.007, …) needs non-power-of-two encodings and pure-alphanumeric fallbacks. Each is its own structural decision and is out of scope for this rewrite.
→ SPEC.md — desired-state specification → Explanation guides — design history, prior art