RFD0026 - Riot Package Management
- Feature Name:
riot_package_management - Start Date:
2026-03-30 - Status:
implemented
Summary
Section titled “Summary”This RFD defines Riot’s package-management model on top of Riot’s existing registry, sparse index, and search infrastructure.
It introduces:
- a package-name-first dependency model
- manifest syntax for registry, source, path, and builtin dependencies
- a mandatory
riot.lockfile that stores the resolved graph - a PubGrub-based solver contract for runtime, build, and dev universes
- the user-facing flows for
riot add,riot rm,riot update, andriot publish
The registry and sparse index are treated as existing infrastructure.
This RFD defines how riot should consume them.
Motivation
Section titled “Motivation”Riot now has the backend pieces for packages:
- explicit publication through
api.pkgs.ml - a sparse named-package index under
cdn.pkgs.ml/index/v1/... - search for package discovery
- immutable package artifacts with optional release provenance metadata
What was still missing when this RFD was written was the client-side model that makes those pieces usable from Riot.
That model must solve several concrete problems at once:
- users should be able to add named packages quickly:
riot add stdriot add std@0.0.1
- users should be able to add packages directly from source:
riot add github.com/owner/reporiot add github.com/owner/repo/path/to/pkgriot add https://github.com/owner/repo#main
- local workspace development should remain ergonomic with path dependencies
- published packages must remain installable by others, even if local path dependencies were used during development
- every dependency operation should produce a reproducible lockfile
- publish must batch workspace packages in dependency order and validate that what is being published is already public on GitHub
The main design pressure is to avoid splitting package identity in two.
The package manager should not sometimes think a package is called
github.com/owner/repo/path and other times think it is called pkg_name.
The chosen model is:
- package name is the dependency identity
- source, path, and registry metadata are resolution inputs and provenance
riot.tomlstores user intentriot.lockstores the exact resolved graph
That gives Riot a model that is simple enough to teach and strong enough to support deterministic solving and reproducible publishing.
Guide-level explanation
Section titled “Guide-level explanation”Dependency identity
Section titled “Dependency identity”The identity of a dependency is always its package name.
That means all of these ultimately resolve to a package named, for example,
minttea:
[dependencies]minttea = "^0.4.0"minttea = { github = "leostera/minttea" }minttea = { source = "https://github.com/leostera/minttea" }minttea = { path = "../minttea" }The different forms mean different things operationally, but they all point at
the same conceptual package identity: minttea.
The authoritative package name comes from the target package’s riot.toml.
Dependency kinds
Section titled “Dependency kinds”Riot recognizes four dependency kinds:
- registry dependencies
- source dependencies
- path dependencies
- builtin/system dependencies
Registry dependency
Section titled “Registry dependency”This is a normal named package from the Riot registry:
[dependencies]std = "^0.1.0"Riot resolves this through the sparse index.
Source dependency
Section titled “Source dependency”This uses a remote source locator directly:
[dependencies]minttea = { github = "leostera/minttea" }minttea = { source = "https://github.com/leostera/minttea", ref = "main" }widgets = { source = "https://github.com/owner/repo/packages/widgets" }github = "owner/repo" is shorthand.
The canonical form is source = "https://github.com/..."
ref is optional.
If omitted, the default is main.
A source dependency may also include a version requirement:
[dependencies]minttea = { version = "^0.4.0", github = "leostera/minttea" }This means:
- materialize this source package
- read its declared package name and version
- require the declared version to satisfy the given constraint
Path dependency
Section titled “Path dependency”This points at a local path:
[dependencies]b = { path = "../b" }This is useful for local development.
Path dependencies may also declare a fallback non-local identity:
[dependencies]b = { path = "../b", version = "*" }b = { path = "../b", source = "https://github.com/owner/repo/packages/b" }These are important for publishing.
The rule is:
pathalone is local-only and not publishablepath + versionis publishable because consumers can fall back to registry resolutionpath + sourceis publishable because consumers can fall back to cached source provenance
Builtin/system dependency
Section titled “Builtin/system dependency”Some dependencies are shipped with the OCaml toolchain and are not published in the Riot registry.
Examples currently include:
stdlibunixdynlink
These are treated as builtin/system dependencies.
Manifest scopes
Section titled “Manifest scopes”Riot keeps dependency scopes separate:
[dependencies]for runtime[build-dependencies]for build-time[dev-dependencies]for development and test-only use
riot add chooses the target section based on flags:
- default:
[dependencies] --build:[build-dependencies]--dev:[dev-dependencies]
It also chooses the target manifest based on flags:
- default: current package manifest
--workspace: workspace manifest-p <pkg>/--package <pkg>: that package manifest only
What riot add does
Section titled “What riot add does”Named add
Section titled “Named add”riot add stdriot add std@0.0.1This is exact package-name resolution, not a search query.
The rough flow is:
- resolve the workspace and target manifest
- fetch sparse index metadata for the named package
- solve the graph with PubGrub
- update
riot.toml - write the exact resolved graph to
riot.lock
If the package does not exist, Riot should fail clearly and may optionally show close matches from package search.
Source add
Section titled “Source add”riot add github.com/leostera/mintteariot add github.com/owner/repo/path/to/pkgriot add https://github.com/owner/repo#mainThe flow is:
- normalize the source locator
- materialize it through the registry
- inspect the fetched package manifest
- discover the real package name and declared version
- write the dependency entry under the real package name
- solve the graph
- write
riot.toml - write
riot.lock
If the package discovered from source is actually named awesome-utils, then:
riot add github.com/leostera/fartassmay write:
[dependencies]awesome-utils = { github = "leostera/fartass" }That normalization is intentional. Package name remains the identity.
riot add should print useful progress while doing this:
- that it is discovering the source package
- which ref it resolved
- which commit SHA it selected
- which package name it discovered
- what it wrote to
riot.toml
What riot rm does
Section titled “What riot rm does”riot rm stdThis removes a dependency from the targeted manifest section only, then
re-solves and rewrites riot.lock.
It does not remove from every section automatically.
There is no implicit --all.
What riot update does
Section titled “What riot update does”riot updateThis updates the whole workspace graph while preserving manifest requirements.
It should:
- keep
riot.tomlunchanged - fetch newer package metadata as needed
- solve again against the current constraints
- rewrite
riot.lock
riot update updates the graph, not the user requirements.
What riot publish does
Section titled “What riot publish does”riot publish publishes the workspace batch by default.
It should:
- enumerate workspace packages
- skip packages that are
private = trueorpublic = false - sort publishable packages in dependency order
- verify that the current code is already on GitHub
- run mandatory local verification
- publish each package through the registry
This means Riot, not the registry, owns workspace publish serialization.
The registry remains the authority on:
- package-name claims
- immutable versions
- publish auth
- sparse index updates
Riot owns:
- workspace traversal
- publish ordering
- local verification
- operator UX
Reference-level explanation
Section titled “Reference-level explanation”1. Manifest model
Section titled “1. Manifest model”1.1 Canonical package identity
Section titled “1.1 Canonical package identity”Every dependency key in riot.toml is a package name.
The dependency payload may constrain how that package is resolved, but it does not replace the package identity with a locator string.
1.2 Dependency grammar
Section titled “1.2 Dependency grammar”The dependency grammar is:
[dependencies]pkg = "<semver requirement>"pkg = { version = "<semver requirement>" }pkg = { source = "<absolute source url>", ref = "<selector>" }pkg = { github = "<owner/repo[/path]>", ref = "<selector>" }pkg = { path = "<relative path>" }pkg = { path = "<relative path>", version = "<semver requirement>" }pkg = { path = "<relative path>", source = "<absolute source url>" }pkg = { path = "<relative path>", github = "<owner/repo[/path]>" }pkg = { version = "<semver requirement>", source = "<absolute source url>" }pkg = { version = "<semver requirement>", github = "<owner/repo[/path]>" }Rules:
githubis shorthand for a GitHubsourcerefapplies to source dependenciesrefdefaults tomainpathmust be relativesourcemust be absoluteversionmust be a valid semver requirement- handwritten dependency keys that disagree with the actual package manifest should be rejected during validation
1.3 Publishability rules for dependency declarations
Section titled “1.3 Publishability rules for dependency declarations”For a package to be publishable:
pathalone is not enoughpath + versionis allowedpath + sourceis allowedsourcealone is allowedgithubalone is allowed
This ensures every published dependency still has a non-local resolution path.
2. Lockfile model
Section titled “2. Lockfile model”2.1 riot.lock is mandatory
Section titled “2.1 riot.lock is mandatory”Every workspace using package management should maintain riot.lock.
This is true for:
- registry dependencies
- source dependencies
- path dependencies
- exact commit refs
2.2 Lockfile stores the resolved graph
Section titled “2.2 Lockfile stores the resolved graph”riot.lock should be a resolved graph, not just a list of selected top-level
versions.
Each resolved node should include at least:
- package name
- exact version
- source kind
- canonical provenance
- resolved SHA where applicable
- checksum for immutable artifacts where applicable
- direct resolved dependencies
- scope participation as needed by the build graph
Cargo and Hex are useful prior art here:
- Cargo stores one resolved node per package with provenance and checksums
- Hex stores enough locked dependency data to avoid reloading the registry for normal operations
Riot should follow that general shape.
2.3 Provenance strings
Section titled “2.3 Provenance strings”The lockfile should preserve provenance explicitly.
Illustrative examples:
registry+https://cdn.pkgs.ml/index/v1source+https://github.com/leostera/minttea#mainpath+../mintteabuiltin+stdlibThe exact encoding can be implementation-defined, but provenance must be preserved and round-trippable.
3. Solver model
Section titled “3. Solver model”3.1 PubGrub
Section titled “3.1 PubGrub”Riot should use packages/pubgrub as the version solver.
PubGrub should operate on package names and version ranges, not on raw source locators.
3.2 Three universes
Section titled “3.2 Three universes”Riot conceptually solves three dependency universes:
- runtime
- build
- dev
These are solved together as part of one overall resolution operation, but they remain distinct in meaning.
Runtime is the important universe for published consumer installability. Build and dev are for package authors and local workflows.
3.3 Published-package constraints
Section titled “3.3 Published-package constraints”For published packages:
- runtime dependencies are part of the published install surface
- build dependencies matter for author-side builds, but not for downstream runtime dependency surfaces
- dev dependencies are ignored for published consumer solving
During publish validation:
- runtime must be publish-valid
- build and dev may remain local-author concerns unless a stricter policy is chosen later
3.4 Source dependencies as solver inputs
Section titled “3.4 Source dependencies as solver inputs”Source dependencies are not package-name ranges in the registry universe.
Instead:
- materialize the source package
- discover its package name and declared version
- convert it into a concrete package input for solving
If a source dependency also declared version = "^1.2.0", then the discovered
package version must satisfy that requirement.
Otherwise solving fails.
3.5 Sparse index contract
Section titled “3.5 Sparse index contract”For registry packages, Riot should solve from the sparse index.
The sparse index contract from RFD0023 is sufficient if each release entry provides:
- exact version
- dependency metadata
- immutable provenance
- artifact location
The solver loop should be lazy:
- fetch
config.json - fetch package shard(s) only for packages entering the frontier
- feed release versions and dependency requirements into PubGrub
- materialize the selected graph into
riot.lock
Search is not on the solver hot path.
4. riot add
Section titled “4. riot add”4.1 Exact package lookup
Section titled “4.1 Exact package lookup”riot add <pkg> expects an exact package name.
If missing:
- return a direct error
- optionally perform a lightweight search query to suggest near matches
4.2 Source discovery
Section titled “4.2 Source discovery”riot add <source> must resolve through the registry immediately.
It should not merely write a textual manifest entry without validation.
That means riot add is both:
- a manifest-editing command
- a resolution command
4.3 Automatic manifest edits
Section titled “4.3 Automatic manifest edits”riot add edits riot.toml automatically.
If the actual package name differs from the repo name or path, Riot should use the actual discovered package name when writing the dependency entry.
5. riot rm
Section titled “5. riot rm”riot rm <pkg> removes the dependency from the targeted manifest scope only and
then rewrites riot.lock from a fresh solve.
If the dependency does not exist in the targeted section, Riot should fail clearly.
6. riot update
Section titled “6. riot update”riot update performs a workspace-wide re-solve.
It should:
- keep dependency requirements unchanged
- update as much of the graph as the current requirements allow
- rewrite
riot.lock
It should not silently widen or rewrite manifest requirements.
7. riot publish
Section titled “7. riot publish”7.1 Workspace-first publish
Section titled “7.1 Workspace-first publish”riot publish publishes the workspace batch by default.
That means it should:
- discover workspace packages
- filter to publishable packages
- order them by dependency
- publish them serially
7.2 Publish eligibility
Section titled “7.2 Publish eligibility”A package is publishable only if:
- it is public
- its version is valid
- it passes local verification (builds correctly)
- the source commit already exists on GitHub
- its runtime dependencies are publish-valid
The registry remains the final authority, but Riot should fail locally first where possible.
7.3 Credentials
Section titled “7.3 Credentials”Riot should store credentials in:
~/.riot/config.tomlThat file should hold the API token used for authenticated publish operations.
Implementation
Section titled “Implementation”This section records the current implementation plan for the first rollout.
Package boundaries
Section titled “Package boundaries”The package-management implementation should live in:
packages/pkgs-mlpackages/riot-depspkgs-ml owns:
- sparse-index path computation and cache layout helpers
- registry document parsing
- registry cache reads and writes
- reusable registry access APIs that are not Riot-specific
- an in-memory registry implementation for tests that should not require network I/O
riot-deps owns:
- manifest-to-resolution orchestration
- registry/source metadata fetches
- package materialization
- lock refresh and unlock behavior
- projection of resolved packages into build-ready data
riot-model owns:
- the manifest-intent package model (
Package.t) - the resolved package model used by the builder (
Package.resolved) - the lockfile schema and TOML serialization (
riot.lock) - package-management and download event types
This split keeps package-management policy and network/materialization logic in
riot-deps, reusable registry mechanics in pkgs-ml, and the durable shared
data model in riot-model.
Current implementation status
Section titled “Current implementation status”As of April 3, 2026, most of this RFD is implemented.
Implemented:
- package-name-first dependency identity in
riot.toml - manifest parsing for registry, source, path, and builtin dependencies
riot add,riot rm, andriot update- mandatory
riot.lockreads, writes, refresh, and unlock behavior - lockfile provenance for workspace, path, source, and registry packages
- PubGrub-backed solving in
riot-deps - source dependency materialization through the local Git cache
- projection of resolved packages into
Riot_model.Package.resolved - workspace publish ordering and publishability validation
- shared lockfile/resolution/materialization events in
riot-model
Intentionally different from earlier versions of this document:
- Riot keeps external package materialization outside the normal
planner/executor action graph. Resolved external packages are repaired before
package build planning continues, instead of introducing planner-emitted
DownloadPackageorEnsurePackageMaterializedactions. That tradeoff is documented in RFD0031. - Some text below still describes the original phase-1 plan and is preserved here as history; the live implementation is further along than that plan.
Phase-1 ownership and responsibilities
Section titled “Phase-1 ownership and responsibilities”For the first rollout, riot-deps should own four concrete responsibilities:
- build dependency universes by traversing and connecting manifest dependencies
- fetch all manifests needed for packages in those universes
- run the solver to establish exact package versions
- present the final resolved package graph in
riot-modelterms for the builder
In other words, the operational flow should look like:
- read
riot.tomlfiles intoRiot_model.Package.t - feed those package roots into
Riot_deps.Dep_solver - have
riot-deps, usingpkgs-mlfor registry access, compute ariot.lockplus resolved package data - pass the resolved package data into the builder/planner
The original phase-1 plan expected a naive solver first and PubGrub later. The
current implementation has already crossed that boundary: riot-deps now uses
packages/pubgrub directly for version choice and incompatibility reporting.
Global registry cache
Section titled “Global registry cache”Registry state should be split by configured registry name from day one. For a
registry named pkgs.ml, the global cache layout should be:
~/.riot/registry/pkgs.ml/index/...~/.riot/registry/pkgs.ml/archive/<package-name>/<exact-version>.tar~/.riot/registry/pkgs.ml/src/<package-name>/<exact-version>/...This keeps:
- sparse index metadata separate from package artifacts
- downloaded archives separate from extracted sources
- registry caches partitioned cleanly once multiple registries are supported
For build purposes, the canonical on-disk home of a resolved external package is its extracted source directory under:
~/.riot/registry/<registry-name>/src/<package-name>/<exact-version>/...Phase 1 should use the configured registry name directly in the path, not a generic placeholder and not a derived URL host.
pkgs-ml testability
Section titled “pkgs-ml testability”pkgs-ml should be configurable and usable in tests without network I/O.
That means the library should provide:
- a filesystem-backed registry rooted at the configured cache directories
- an in-memory registry that can serve sparse-index config and per-package documents directly from test data
riot-deps tests should prefer the in-memory registry whenever they are testing
solver and lockfile behavior rather than transport or on-disk cache behavior.
Manifests stay name-based
Section titled “Manifests stay name-based”Downloaded package manifests must not be rewritten into path-based manifests.
For example, a downloaded package may still contain:
[dependencies]kernel = "^0.3.0"The lockfile and resolved graph, not rewritten manifests, determine which exact
kernel node that name refers to.
This means there are three distinct layers:
- manifest intent in
riot.toml - the exact resolved graph in
riot.lock - materialized package roots in
~/.riot/registry/<registry-name>/src/...
The builder should consume the resolved graph, not re-resolve transitive dependencies from downloaded manifests.
End-to-end data flow
Section titled “End-to-end data flow”The intended data flow is:
riot-modelparses local manifests intoPackage.triot-depstraverses dependency names and builds the runtime/build/dev universesriot-depsfetches package metadata and manifests for the packages entering those universesriot-depscomputes exact package selectionsriot-depsdownloads package archives into the registry archive cacheriot-depsmaterializes those archives into the registry source cacheriot-depswritesriot.lockriot-depsprojects the resolved graph intoRiot_model.Package.resolved- the builder consumes
Package.resolved, not unresolved downloaded manifests
This means that for build purposes every external dependency is eventually a materialized package root on disk, but it is still resolved through the lock graph rather than by mutating manifests into path dependencies.
Concrete example
Section titled “Concrete example”Suppose a workspace package declares:
[dependencies]std = "^0.1.0"jsonrpc = "^0.2.0"and the downloaded std manifest still says:
[dependencies]kernel = "^0.3.0"Phase 1 should work like this:
- the workspace manifest is parsed into
Riot_model.Package.t riot-depsdiscovers the transitive universe:stdjsonrpckernel
riot-depsfetches their manifests- the solver picks exact versions
- their downloaded archives are cached at paths like:
~/.riot/registry/pkgs.ml/archive/std/<version>.tar~/.riot/registry/pkgs.ml/archive/jsonrpc/<version>.tar~/.riot/registry/pkgs.ml/archive/kernel/<version>.tar
- their extracted sources are materialized at paths like:
~/.riot/registry/pkgs.ml/src/std/<version>/...~/.riot/registry/pkgs.ml/src/jsonrpc/<version>/...~/.riot/registry/pkgs.ml/src/kernel/<version>/...
riot.lockrecords that:- the workspace depends on exact
std - exact
stddepends on exactkernel - exact
jsonrpcdepends on exactstd
- the workspace depends on exact
- the builder consumes the resolved graph and the extracted source paths directly
At no point should std’s downloaded manifest be rewritten to say:
[dependencies]kernel = { path = "..." }The lockfile is what explains what kernel means in that context.
Build-time flow
Section titled “Build-time flow”The build path should ensure that a build always uses the latest lock.
The flow is:
- read workspace and package manifests into
Riot_model.Package.t - check whether
riot.lockexists - if
riot.lockis missing, solve and write it - if any participating workspace
riot.tomlis newer thanriot.lock, refresh the lock and rewrite it - otherwise read the existing lock
- project the lock into
Riot_model.Package.resolved - feed resolved packages into the builder/planner
The staleness check is against:
- the workspace
riot.toml - each workspace member
riot.toml
It should not be driven by downloaded manifests in the global cache.
This guarantees that if a build runs, it is using the latest lock.
Refresh vs unlock
Section titled “Refresh vs unlock”There are two solve modes:
- lock refresh
- unlock
Lock refresh is used by:
riot buildriot addriot rm
Unlock is used by:
riot update
Lock refresh must not behave like a full cold solve when an existing lock is present. It should preserve the current locked selections whenever possible and only reopen the affected frontier when required by changed manifests or missing lock state.
Unlock is the mode that is allowed to reopen the whole graph intentionally.
The intended behavioral split is:
riot build- solve only when the lock is missing or stale
- otherwise trust the existing lock
riot add- edit the manifest
- refresh the lock conservatively around that new requirement
riot rm- edit the manifest
- refresh the lock conservatively after removal
riot update- unlock and intentionally reopen the graph
This policy distinction matters regardless of solver implementation.
Phase-1 solver
Section titled “Phase-1 solver”The original phase-1 proposal was to use a deliberately naive solver first and replace it later.
That is no longer current. Riot now uses PubGrub for solving, version choice, and conflict reporting.
What remains true from the original plan is that the rest of the package management pipeline is already real:
- manifest parsing
- graph construction
- metadata fetches
- lockfile writes
- materialization
- build integration
The solver pass is operationally real:
- it traverses the universes
- it fetches the manifests
- it picks exact versions
- it produces a lockfile
Builder integration
Section titled “Builder integration”For the builder, resolved external packages are effectively packages with materialized roots on disk.
The builder already consumes Riot_model.Package.resolved.
The remaining design question is how far down the build pipeline lazy repair of missing external package materialization should live.
Today, riot-deps repairs missing registry package roots lazily during
projection, right before it loads the external package manifest and converts the
lockfile node into Riot_model.Package.resolved.
The design described here is still attractive: the planner could emit a download/materialization action for any non-workspace package whose cache root is missing.
That means the planner can produce actions such as:
DownloadPackage- regular build/compile/link actions
The download action is responsible for ensuring the resolved package exists at its expected cache path before normal build actions consume it.
That would push the repair one layer later than the current implementation and would let normal build actions depend on explicit download/materialization actions instead of the projection layer repairing cache entries opportunistically.
Events
Section titled “Events”Package management should emit explicit events so solving, locking, and materialization are visible in both CLI and server flows.
These events should be part of the shared riot-model event surface so both
CLI and server/session flows can report them consistently.
This is implemented for lockfile, resolution, metadata fetch, source dependency materialization, manifest-edit reporting, package-version update reporting, and materialization lifecycle events.
Phase 1 should add explicit package-management event kinds such as:
Lockfile events
Section titled “Lockfile events”LockfileReadStartedof{ path: string }LockfileReadFinishedof{ path: string; duration_ms: int }LockfileReadFailedof{ path: string; error: string }LockfileWriteStartedof{ path: string }LockfileWriteFinishedof{ path: string; duration_ms: int }LockfileWriteFailedof{ path: string; error: string }
Resolution lifecycle events
Section titled “Resolution lifecycle events”DependencyResolutionStartedof{ packages: string list; mode: [Refresh |Unlock ] }DependencyResolutionUsingExistingLockof{ path: string }DependencyResolutionRefreshingLockof{ path: string }DependencyResolutionUnlockingof{ path: string option }DependencyResolutionFinishedof{ duration_ms: int; resolved_packages: int; resolved_edges: int }DependencyResolutionFailedof{ error: string }
Universe-building events
Section titled “Universe-building events”DependencyUniverseBuildingof{ packages: string list }DependencyUniverseBuiltof{ runtime_packages: int; build_packages: int; dev_packages: int; duration_ms: int }
Registry / manifest metadata events
Section titled “Registry / manifest metadata events”PackageMetadataFetchStartedof{ package: string }PackageMetadataFetchFinishedof{ package: string; version: string option; duration_ms: int }PackageMetadataFetchFailedof{ package: string; error: string }PackageManifestFetchStartedof{ package: string; version: string }PackageManifestFetchFinishedof{ package: string; version: string; duration_ms: int }PackageManifestFetchFailedof{ package: string; version: string option; error: string }
Materialization / download events
Section titled “Materialization / download events”PackageDownloadStartedof{ package: string; version: string; path: string }PackageDownloadFinishedof{ package: string; version: string; path: string; duration_ms: int }PackageDownloadFailedof{ package: string; version: string; path: string; error: string }PackageDownloadSkippedof{ package: string; version: string; path: string; reason: string }PackageCacheHitof{ package: string; version: string; path: string }PackageMaterializationStartedof{ package: string; version: string; path: string }PackageMaterializationFinishedof{ package: string; version: string; path: string; duration_ms: int }PackageMaterializationFailedof{ package: string; version: string; path: string; error: string }
Build-integration events
Section titled “Build-integration events”PackageResolvedForBuildof{ package: string; version: string option; path: string; workspace: bool }PackageDownloadQueuedof{ package: string; version: string; path: string }
The CLI should surface these as progress while operations such as riot add,
riot update, riot publish, and stale-lock riot build are running.
Drawbacks
Section titled “Drawbacks”- this model is more explicit than a pure “dependency string only” package manager and therefore has more syntax to teach
- package-name identity plus source/path payloads means Riot must validate name mismatches carefully
- mandatory lockfiles create more file churn
- solving runtime, build, and dev together is more complex than solving only one dependency class
Rationale and alternatives
Section titled “Rationale and alternatives”Why package-name identity instead of locator identity?
Section titled “Why package-name identity instead of locator identity?”Because locator identity makes normal package usage awkward:
riot add github.com/leostera/mintteashould not permanently force every later tool to think the package is named by that locator.
Using package names as identity keeps the graph coherent and lets both registry and source installs converge on the same package model.
Why allow version + source together?
Section titled “Why allow version + source together?”Because it is useful and precise.
It expresses:
- where to get the package from
- what version the discovered package must satisfy
That is better than forcing users to choose only one axis.
Why allow path + version and path + source?
Section titled “Why allow path + version and path + source?”Because local development and publishability are both important.
These forms let package authors develop locally while still expressing a consumer-visible fallback identity for publication.
Why solve from the sparse index instead of the search API?
Section titled “Why solve from the sparse index instead of the search API?”Because search is for discovery. The sparse index is the canonical fast path for exact named installs and has the right shape for a solver.
Why make riot publish publish the workspace batch by default?
Section titled “Why make riot publish publish the workspace batch by default?”Because Riot workspaces commonly contain multiple related packages that must be published in dependency order. Making batch publish the default removes a lot of tedious operator work and matches how the repository is actually structured.
Prior art
Section titled “Prior art”- Cargo
- Cargo reinforces that package-name identity should be stable even when the actual source of a package comes from a registry or a git repository.
- Cargo’s sparse index model shows that exact named-package resolution should go through a small per-package metadata document rather than a search API or giant central catalog.
- Cargo’s lockfile shape shows the value of recording one resolved node per package, with provenance and checksums attached to the selected artifact.
- Hex
- Hex shows that a lockfile should preserve enough dependency metadata that normal operations do not need to go back to the registry just to understand the graph.
- Hex also reinforces the distinction between dependency requirements in the manifest and exact locked selections in the lockfile.
- Hex’s treatment of the lockfile as a self-contained resolution artifact is
especially relevant for
riot updateand reproducible workspace builds.
- Go modules
- Go demonstrates the ergonomics of source-first package acquisition. A user can point at a public source location and expect the tool to figure out the package boundary and usable version information.
- The Riot source-dependency flow borrows that feeling, but keeps it separate from package-name claiming and publication.
- Bun and npm
- Bun and npm reinforce that package installation by name should feel direct and fast, with local lockfile state doing most of the reproducibility work.
- They also show the UX value of commands like
add,remove, andupdatedirectly editing manifests and lockfiles as one coherent action.
Riot intentionally combines:
- Go’s source ergonomics
- Cargo’s package-name identity, sparse index, and provenance-oriented lockfile
- Hex’s lockfile completeness and offline-friendly graph metadata
- Bun/npm’s direct command UX around manifest and lockfile mutation
Unresolved questions
Section titled “Unresolved questions”- What exact on-disk format should
riot.lockuse? - How much build- and dev-universe detail should be preserved in the lockfile?
- Should
riot update <pkg>and other more granular update commands be part of the first rollout or follow later? - How should Riot represent builtin/system package compatibility against OCaml compiler versions in the solver?
Future possibilities
Section titled “Future possibilities”- add
riot search <query>as a discovery companion to exact-matchriot add <pkg> - add yanked, deprecated, or compatibility flags to the sparse index and teach the solver how to treat them
- add more source providers beyond GitHub without changing the package-identity model
- add partial graph updates and more explicit workspace publish targeting
- add richer publish-time verification such as registry-side builds, documentation generation, or compatibility checks