Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
de6d421
feat(cli): [WIP] Support semantic version ranges.
heatonmatthew Apr 7, 2026
ce2b42f
feat(cli): [WIP] Additional unit tests for semantic version ranges.
heatonmatthew Apr 7, 2026
99ed6e7
feat(cli): Update documentation
heatonmatthew Apr 7, 2026
95326c5
fix(cli): Incomplete handling of parts in "name version" Deserialize
heatonmatthew Apr 11, 2026
a9ae96c
fix(cli): Incorrect handling of CWD when installing a workspace.
heatonmatthew Apr 18, 2026
65ce199
chore: fix lint fmt
heatonmatthew Apr 18, 2026
46604d8
chore(cli): re-add debug traces for Artifactory calls
heatonmatthew Apr 18, 2026
d33ebc7
doc: Re-add incorrectly removed content
heatonmatthew Apr 28, 2026
19596ee
Remove unused function.
heatonmatthew Apr 28, 2026
09e53cf
Tidy-up imports
heatonmatthew Apr 28, 2026
5f14c66
chore: ignore .worktrees/ directory
heatonmatthew Apr 28, 2026
8f3db35
manifest: sort dependencies_as_vec output deterministically
heatonmatthew Apr 28, 2026
7964fae
style: remove redundant type annotation in dependencies_as_vec
heatonmatthew Apr 28, 2026
d3289fc
resolver: replace VersionConflict with NoCompatibleVersion and Lockfi…
heatonmatthew Apr 28, 2026
7f7238e
fix: scope unused_assignments allow to file level for thiserror v2 co…
heatonmatthew Apr 28, 2026
8134655
resolver: drop LockfileStale variant
heatonmatthew Aug 18, 2026
9c0d86e
resolver: replace recursion with worklist + per-package requirement m…
heatonmatthew Aug 18, 2026
a416a81
test: cascade re-resolution when a package's chosen version changes
heatonmatthew Aug 18, 2026
a6deab1
test: lockfile pins that fail merged requirements are re-resolved
heatonmatthew Aug 18, 2026
7c4fcd1
docs: describe merge-and-resolve version selection
heatonmatthew Aug 18, 2026
308a0cf
test: cover retraction cascade and survivor rescheduling directly
heatonmatthew Aug 27, 2026
c1c8e41
Merge remote-tracking branch 'upstream/main' into versioning
heatonmatthew Aug 28, 2026
4d4a09f
test: lockfile pins are not re-resolved for range criteria
heatonmatthew Aug 28, 2026
ff08773
doc: keep examples in original single line format
heatonmatthew Aug 28, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ target
.DS_Store
.vscode
.envrc
.worktrees/
26 changes: 14 additions & 12 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@ validation = [
git = []

[dependencies]
async-recursion = "1.0.5"
async-trait = "0.1.89"
anyhow = { version = "1.0", optional = true }
base64 = "0.22.1"
Expand Down Expand Up @@ -89,6 +88,7 @@ assert_fs = "1.0"
axum = { version = "0.8", default-features = false, features = [
"tokio",
"http1",
"query",
] }
hex = "0.4.3"
pretty_assertions = "1.4"
Expand Down
13 changes: 6 additions & 7 deletions docs/src/commands/buffrs-add.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,12 @@ it will default to the latest version of this artifact in the registry.

The repository name should adhere to lower-kebab case (e.g. `my-buffrs-repo`).
The package name has its own set of constraints as detailed in [Package Name
Specification](../reference/pkgid-spec.md). When specified, the version must
adhere to the [Semantic Version convention](https://semver.org/) (e.g. `1.2.3`)
-- see [SemVer compatibility](../reference/semver.md) for more information.

Currently there is no support for resolving version operators but the specific
version has to be provided. This means `^1.0.0`, `<2.3.0`, `~2.0.0`, etc. can't
be installed, but `=1.2.3` has to be provided.
Specification](../reference/pkgid-spec.md). When specified, the version must be a valid
[SemVer requirement](../reference/semver.md). Both exact pins (`=1.2.3`) and
range operators (`^1.0.0`, `~2.1.0`, `>=1.5.0`, etc.) are supported. During
`buffrs install`, the resolver queries the registry and selects the highest
available version that satisfies the requirement, then records the concrete
version in the lockfile for reproducibility.

#### Lockfile interaction

Expand Down
4 changes: 2 additions & 2 deletions docs/src/guide/consuming-packages.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ type = "lib"
version = "1.0.0"

[dependencies]
google = { version = "=1.0.0", registry = "<your-registry>", repository = "<your-repository> }
google = { version = "^1.0.0", registry = "<your-registry>", repository = "<your-repository>" }
```

Running `buffrs install` yields you with the following filesystem:
Expand Down Expand Up @@ -65,7 +65,7 @@ major difference is the lack of the `[package]` section in your manifest.

```
[dependencies]
logging = { version = "=1.0.0", registry = "<your-registry>", repository = "<your-repository> }
logging = { version = "^1.0.0", registry = "<your-registry>", repository = "<your-repository>" }
```

Running a `buffrs install` yields you the very same as above, except for the
Expand Down
80 changes: 80 additions & 0 deletions docs/src/reference/resolver.md
Original file line number Diff line number Diff line change
@@ -1 +1,81 @@
# Dependency Resolution

When you run `buffrs install`, the resolver builds a complete dependency graph
for your project — including all transitive dependencies — and determines the
concrete version to install for each package.

## Resolution algorithm

For each dependency (direct or transitive), the resolver follows this priority
order:

1. **Lockfile hit** — if `Proto.lock` records a version of the package that
satisfies *every* requirement gathered for it so far, that version is used
immediately without contacting the registry. This makes repeated installs
fast and reproducible. A pin that does not satisfy them all is simply not a
usable answer, and resolution falls through to the registry.

2. **Registry resolution** — if no locked version qualifies, the resolver
queries the registry for all available versions of the package, then selects
the **highest** version that satisfies **all** of the requirements at once.

3. **Download and cache** — the resolved version is downloaded, stored in the
local cache, and its digest is recorded in the lockfile for future installs.

Transitive dependencies are discovered by reading the `Proto.toml` bundled
inside each downloaded package archive, then resolved using the same steps
above. Because a package's full set of requirements is only known once every
path to it has been walked, choosing a lower version can retract dependencies
that a previously-chosen higher version had pulled in; those become unreachable
and are dropped from the graph.

## Version conflict detection

If the same package is required by more than one path in the dependency tree,
the resolver merges every requirement and picks the highest version satisfying
all of them together. Requirements are never evaluated pairwise or in
encounter order, so a later, tighter requirement can lower an earlier pick
rather than conflicting with it.

The install fails only when the intersection is empty — no published version
satisfies every requirement:

```
no version of leaf-lib satisfies all requirements: [^1.0.0, ^2.0.0];
available versions: [2.0.0, 1.0.0]
```

To fix a conflict, update the requiring packages so their version requirements
overlap, or introduce a package that bridges the incompatible requirements.

Note that this conflict detection operates **within a single package's
dependency graph**. In a workspace, different members may independently resolve
different versions of the same package — the workspace lockfile records them
separately using a `(name, version)` composite key.

## Workspace resolution

In a workspace, each member package's dependency graph is resolved
independently. The workspace lockfile (`Proto.lock` at the workspace root)
accumulates all resolved packages across all members. Because the workspace
lockfile allows multiple versions of the same package, two members that require
incompatible versions of a shared library can co-exist.

If a subsequent install finds a workspace lockfile, it reuses those locked
versions (subject to satisfying each member's requirements) to avoid redundant
registry queries.

## Topological ordering

After the full graph is built, packages are sorted topologically so that each
dependency is installed before its dependants. This guarantees that vendored
proto sources are available in the correct order during compilation.

## Determinism and the lockfile

The resolver always picks the **highest** version satisfying every requirement
when multiple candidates exist. This is deterministic given the same set of available
registry versions. Once a version is recorded in `Proto.lock`, it is used
as-is on all subsequent installs, regardless of newer versions that may have
been published since. Run `buffrs install` after deleting or modifying
`Proto.lock` to re-resolve against the current registry state.
87 changes: 87 additions & 0 deletions docs/src/reference/semver.md
Original file line number Diff line number Diff line change
@@ -1 +1,88 @@
# SemVer Compatibility

buffrs uses [Semantic Versioning](https://semver.org/) for all packages.
Version requirements in `Proto.toml` follow the same syntax as
[Cargo](https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html).

## Version requirement syntax

A version requirement is placed in the `version` field of a dependency:

```toml
[dependencies.my-lib]
version = "^1.2.0"
registry = "https://my-registry.example.com"
repository = "my-repo"
```

The following operators are supported:

### Caret (`^`) — default for ranges

Allows minor and patch updates within the same major version.
This is the recommended operator for most dependencies.

| Requirement | Resolves versions |
|-------------|-------------------|
| `^1.2.3` | `>=1.2.3, <2.0.0` |
| `^1.2` | `>=1.2.0, <2.0.0` |
| `^1` | `>=1.0.0, <2.0.0` |
| `^0.2.3` | `>=0.2.3, <0.3.0` |
| `^0.0.3` | `>=0.0.3, <0.0.4` |

Note that `0.x` versions are treated as unstable: `^0.2` only allows `0.2.x`,
not `0.3.x`, since breaking changes are expected in pre-1.0 packages.

### Tilde (`~`) — patch-level updates only

Allows patch updates within the same minor version.

| Requirement | Resolves versions |
|-------------|-------------------|
| `~1.2.3` | `>=1.2.3, <1.3.0` |
| `~1.2` | `>=1.2.0, <1.3.0` |
| `~1` | `>=1.0.0, <2.0.0` |

### Exact (`=`) — pin to a specific version

Resolves to exactly the stated version, with no flexibility.

```toml
version = "=1.2.3"
```

Use exact pins when you need bit-for-bit reproducibility in the manifest
itself, or when you are distributing a library whose consumers should be
in full control of the version.

### Comparison operators

For more control, the standard comparison operators are available:

| Requirement | Meaning |
|-----------------|----------------------------------|
| `>=1.2.0` | Any version at or above 1.2.0 |
| `>1.2.0` | Any version strictly above 1.2.0 |
| `<2.0.0` | Any version strictly below 2.0.0 |
| `<=2.0.0` | Any version at or below 2.0.0 |
| `>=1.0.0, <2.0.0` | Intersection (multiple constraints) |

## How the resolver picks a version

When a requirement matches more than one available version, buffrs always
selects the **highest** satisfying version. The resolved concrete version is
written to `Proto.lock` to ensure reproducible installs — re-running
`buffrs install` will use the locked version rather than querying the registry
again.

See [Dependency Resolution](./resolver.md) for a full description of the
resolution algorithm.

## Choosing between pinning and ranges

| Situation | Recommended style |
|-----------|-------------------|
| Public library — let consumers decide | `^1.0.0` |
| Internal service — stable dependency set | `^1.0.0` or `~1.2.0` |
| Security patch must be applied exactly | `=1.2.5` |
| Compatibility ceiling known | `>=1.0.0, <3.0.0` |
80 changes: 56 additions & 24 deletions docs/src/reference/specifying-dependencies.md
Original file line number Diff line number Diff line change
@@ -1,34 +1,61 @@
# Specifying Dependencies

Dependencies are declared in the `[dependencies]` section of the `Proto.toml`
manifest. Each entry maps a dependency package name to a dependency
specification object.
Dependencies are declared in the `[dependencies]` section of `Proto.toml`.
Each entry names the package and provides a version requirement, registry URL,
and repository name.

## Remote Dependencies

Remote dependencies are downloaded from an Artifactory registry during
[`buffrs install`](../commands/buffrs-install.md).
## Inline table syntax

```toml
[dependencies]
my-package = { registry = "https://your.registry/artifactory", repository = "my-repo", version = "1.2.3" }
my-package = { registry = "https://your.registry/artifactory", repository = "my-repo", version = "^1.0.0" }
```

The `version` field must be an exact semantic version (e.g. `"1.2.3"`).
Version ranges or operators (`^`, `~`, `<`, `>`) are not currently supported.
The three required fields for a remote dependency are:

| Field | Description |
|-------|-------------|
| `version` | A SemVer requirement — see [SemVer Compatibility](./semver.md) |
| `registry` | Base URL of the Artifactory registry |
| `repository` | Repository name within that registry |

## Adding dependencies via the CLI

Use [`buffrs add`](../commands/buffrs-add.md) to add a remote dependency from
the command line:
The `buffrs add` command writes the manifest entry for you:

```bash
# Caret range (recommended): resolves to the highest 1.x.y
buffrs add --registry https://my-registry.example.com my-repo/my-lib@^1.0.0

# Exact pin: resolves to exactly 1.2.3
buffrs add --registry https://my-registry.example.com my-repo/my-lib@=1.2.3

# Latest: omitting the version resolves to the latest available
buffrs add --registry https://my-registry.example.com my-repo/my-lib
```
buffrs add --registry https://your.registry/artifactory my-repo/my-package@1.2.3

After adding a dependency, run `buffrs install` to resolve and download it.

## Version requirements

buffrs supports the full range of SemVer requirement operators:

```toml
version = "^1.0.0" # >=1.0.0, <2.0.0 (recommended for most deps)
version = "~1.2.0" # >=1.2.0, <1.3.0 (patch updates only)
version = ">=1.5.0" # any version at or above 1.5.0
version = "=1.2.3" # exactly 1.2.3
version = ">=1.0, <2.0" # explicit intersection
```

## Local Dependencies
The resolver queries the registry and selects the **highest** available version
satisfying the requirement. See [SemVer Compatibility](./semver.md) for the
full operator reference.

## Local dependencies

Local dependencies are resolved from the local filesystem relative to the
manifest. They are useful for multi-package repositories where packages depend
on each other without going through a remote registry.
You can depend on a package in a local directory (useful in monorepos or during
development):

```toml
[dependencies]
Expand All @@ -38,15 +65,20 @@ my-lib = { path = "../my-lib" }
The `path` field is a relative path from the manifest file to the dependency's
root directory (the directory containing the dependency's `Proto.toml`).

Local dependencies do not have a version requirement — the package at that
path is used as-is. They cannot be mixed with a remote entry for the same
package name.
Comment on lines +68 to +70

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Can we keep the removed lines:

Suggested change
Local dependencies do not have a version requirement — the package at that
path is used as-is. They cannot be mixed with a remote entry for the same
package name.
The `path` field is a relative path from the manifest file to the dependency's
root directory (the directory containing the dependency's `Proto.toml`).
Local dependencies do not have a version requirement — the package at that
path is used as-is. They cannot be mixed with a remote entry for the same
package name.
See [Local Dependencies](../guide/local-dependencies.md) for more information.

@heatonmatthew heatonmatthew Apr 28, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Good catch. Fixed.

Please check you're happy.


See [Local Dependencies](../guide/local-dependencies.md) for more information.

## Lockfile
## The lockfile

After adding or modifying dependencies in the manifest, run
[`buffrs install`](../commands/buffrs-install.md) to resolve and lock them.
The lockfile (`Proto.lock`) records the exact resolved versions and checksums
and should be committed to version control.
Once resolved, the concrete version is recorded in `Proto.lock`. Subsequent
installs use the locked version without re-querying the registry, ensuring
reproducible builds across machines and CI environments.

See [Manifest vs Lockfile](../guide/manifest-vs-lockfile.md) for more
information.
Commit `Proto.lock` to version control for applications and services. For
libraries intended to be consumed by others, committing the lockfile is
optional — consumers will resolve their own versions.

See [Manifest vs Lockfile](../guide/manifest-vs-lockfile.md) for more detail.
Loading