Skip to main content
A version identifies a specific, immutable snapshot of a package’s source code. Versions give engineers a precise language for specifying requirements: when a package declares a dependency on component-lib@0.3.2, any engineer building it will use exactly that version, producing identical results regardless of when or where the build runs.

Principles

The package system is built on three principles: compatibility, repeatability, and cooperation. These principles reinforce each other and guide every design decision.

Compatibility

The meaning of a package version should not change over time.
Semantic versioning encodes compatibility promises in version numbers:
  • Patch versions (0.3.1 → 0.3.2): Metadata changes only. No changes to connectivity, layout, or electrical behavior. Examples: updating documentation, fixing a typo in a property value, adding manufacturer aliases.
  • Minor versions (0.3.x → 0.4.0): New functionality that doesn’t break existing designs. Examples: adding optional pins, new configuration options, additional footprint variants.
  • Major versions (0.x → 1.0): Breaking changes that may require design updates. Examples: changing pin definitions, restructuring interfaces, removing deprecated features.
For pre-1.0 packages, minor versions (0.3 vs 0.4) are treated as breaking changes, following the convention that early development may have frequent API changes.
Pre-1.0 packages are considered unstable. Per the Semantic Versioning spec, major version zero (0.y.z) is for initial development—anything may change at any time and the public API should not be considered stable. This is why 0.3.x and 0.4.x are treated as separate, potentially incompatible families.
Semver families group compatible versions together. Within a family, any version can substitute for any other without breaking dependent code:
v0.3.x family: 0.3.0, 0.3.1, 0.3.2, ...  (compatible)
v0.4.x family: 0.4.0, 0.4.1, ...          (compatible)
v0.3.x and v0.4.x: different families     (potentially incompatible)
When a build requires multiple versions from the same family, the system selects the highest version. This is safe precisely because compatibility guarantees that newer versions in a family work everywhere older versions did.

Repeatability

The result of a build should not change over time.
Given the same source files, a build should produce the same output whether it runs today, next month, or next year. Time should not be an input to the build process. Repeatability has two components: Version selection must be deterministic. Given a set of dependencies, the system must always select the same versions. This rules out “latest compatible version” resolution that depends on what versions exist at build time. Selected versions must be immutable. Once selected, a version’s contents cannot change. This rules out mutable references like branches or tags that can be moved. The package system achieves repeatability with Minimal Version Selection (MVS) and hydrated pcb.toml manifests. MVS selects the minimum version that satisfies the dependency graph. pcb sync records that selected graph in manifests, including pseudo-versions for branch and commit dependencies, so locked and offline builds reuse immutable versions.

Cooperation

To maintain the package ecosystem, we must all work together.
Technical mechanisms can enforce compatibility and repeatability, but they cannot create them. A version number is a promise from the package author to their users. The system trusts that authors will:
  • Follow semantic versioning conventions
  • Test changes before releasing new versions
  • Avoid breaking changes within a semver family
  • Communicate clearly when breaking changes are necessary
In return, the system provides tools that make cooperation easier:
  • Gradual migration: Multiple major versions can coexist in a single build, allowing dependent packages to upgrade at their own pace
  • Publishing workflow: The pcb publish command validates packages before release, catching common mistakes early
No amount of clever algorithms can fix a package that violates its compatibility promises. The goal is to make keeping promises easy and breaking them hard.

Workspace Package Discovery

Workspace package discovery is implicit. Starting from the workspace root, pcb looks up to eight directory levels deep and treats each descendant directory with a pcb.toml as a workspace package. [workspace].exclude controls discovery:
[workspace]
pcb-version = "0.3"
exclude = ["scratch/**", "experiments/old-board"]
Excluded directories are pruned, so pcb neither discovers a package there nor searches below it. The tool also always skips generated/cache directories such as .git, .pcb, vendor, target, node_modules, and fork. Older manifests may still contain [workspace].members; the field is parsed for compatibility but ignored. pcb sync removes it when it rewrites the workspace manifest.

Coexisting Versions

Sometimes breaking changes are necessary. A component library might need to restructure its pin definitions, or a shared component library might change how interfaces are represented. When this happens, packages that depend on the old API cannot immediately upgrade—they need time to adapt. The package system supports multiple major versions in a single build. If your dependency graph contains:
# Library X
[dependencies]
"github.com/acme/component-lib" = "0.3"

# Library Y
[dependencies]
"github.com/acme/component-lib" = "1.0"
Both libraries can build together. The system maintains separate copies of component-lib@0.3.x and component-lib@1.0.x, each used by the packages that require them. Types and values from different major versions are distinct and cannot be mixed. This approach has two key benefits: No flag day upgrades. Teams can migrate package-by-package over weeks or months, validating each migration before moving to the next. The old version remains available as long as any package needs it. Diamond dependencies work. If Board A uses Library X (component-lib 0.3) and Library Y (component-lib 1.0), both libraries function correctly. Library X sees component-lib 0.3 types, Library Y sees component-lib 1.0 types, and the board sees whatever version it declares. The limitation is that types from different major versions may be incompatible. A Foo from component-lib 0.3 cannot be passed to a function expecting a component-lib 1.0 Foo. Major versions represent potential breaking changes, and the system cannot guarantee compatibility across that boundary.

Minimal Version Selection

Our approach to dependency resolution is heavily inspired by Go modules, which pioneered Minimal Version Selection. Most package managers (npm, Cargo, pip) use SAT solvers to find the newest versions that satisfy all constraints. This approach has a fundamental problem: the result depends on what versions exist at resolution time. If a new version is published between two builds, the resolver might select it, changing your dependencies without any change to your code. SAT solvers also introduce complexity. When constraints conflict, the solver must backtrack and try alternative versions. This can be slow, and when it fails, the error messages are often inscrutable—the solver tried thousands of combinations and none worked. Minimal Version Selection (MVS) takes a different approach: instead of finding the newest versions that work, find the minimum versions that each package explicitly requires. This simple change has profound consequences.

How MVS Works

Consider this dependency graph:
Board
├── component-lib >= 0.3
└── regulator >= 1.0
    └── component-lib >= 0.3.2
The Board requires component-lib >= 0.3.0. The regulator requires component-lib >= 0.3.2. MVS selects component-lib@0.3.2—the minimum version that satisfies both constraints. Even if component-lib@0.3.9 exists and is compatible, MVS chooses 0.3.2 because that’s what the dependencies explicitly require. The existence of newer versions is irrelevant.

Why Minimum, Not Maximum?

This seems counterintuitive. Wouldn’t you want the newest compatible version with all its bug fixes? The key insight is that the author tested with specific versions. When the regulator author published version 1.0.0, they tested it with component-lib@0.3.2. They know it works with that version. They hope it works with 0.3.9, but they haven’t tested it. By selecting the minimum, MVS chooses the versions that were actually tested together. If you want newer versions, you explicitly upgrade:
[dependencies]
"github.com/acme/component-lib" = "0.3.9"
Now MVS selects 0.3.9 because you require it. You’re taking responsibility for testing with that version.

Properties of MVS

Deterministic. The selected versions depend only on the dependency graph, not on what versions exist remotely. Two engineers building the same code get the same versions, even if one builds months later when new versions exist. No backtracking. MVS never needs to “try” a version and backtrack if it fails. It computes the answer directly: for each package, find the maximum of the minimum versions required by all dependents. Understandable. You can compute the result by hand. For each package, look at every place it’s required and take the highest version. That’s it. Fast. Linear in the size of the dependency graph. No exponential search space.

Resolution Algorithm

  1. Seed: Collect direct dependencies from all workspace packages. Group by (package path, semver family). Initialize each family to the highest version explicitly required.
  2. Discover: Fetch manifests for selected versions. For each transitive dependency, if it requires a higher version within an existing family, upgrade. Repeat until no changes (fixed point).
  3. Build closure: Trace the dependency graph from workspace roots using final versions. This filters out any versions that were superseded during discovery.
Example with multiple semver families:
WV0001: component-lib = 0.2.13
WV0002: component-lib = 0.3.2, regulator = 1.0
WV0003: component-lib = 0.3.1
regulator@1.0.0: component-lib = 0.3.0

Result:
  v0.2.x family → component-lib@0.2.13
  v0.3.x family → component-lib@0.3.2 (max of 0.3.2, 0.3.1, 0.3.0)
Both versions coexist in the final build because they’re in different semver families.

Import Paths as Identity

Import paths serve as globally unique package identifiers:
load("@stdlib/units.zen", "Voltage")
load("github.com/myorg/components/capacitor.zen", "Capacitor")
This design has several benefits: No central registry. Anyone can publish packages to their own repository without coordinating names. There’s no competition for short names or namespacing conflicts. Unambiguous origin. The import path tells you exactly where code comes from. When reading unfamiliar code, you can immediately identify package ownership. Zero-configuration. A file’s imports fully specify its dependencies. No external configuration is needed to understand what a module requires. Versions are intentionally not embedded in import paths. Instead, the pcb.toml manifest declares which version of each package to use:
[dependencies]
"github.com/diodeinc/registry/components/ti/tps54331" = "1.0"
This separation means:
  • Import statements remain stable across version upgrades
  • Version changes are localized to manifest files
  • The same source file can work with different versions in different contexts

Hydrated Manifests

Workspaces store resolved dependency state in pcb.toml. pcb sync updates:
  • [dependencies]: direct dependencies the package imports or explicitly owns.
  • [dependencies.indirect]: the tool-managed MVS closure needed to build it.
Example:
[dependencies]
"github.com/diodeinc/registry/modules/Regulator" = "1.0"

[dependencies.indirect]
"github.com/diodeinc/registry/components/TPS54331@1" = "1.0.2"
"gitlab.com/kicad/libraries/kicad-footprints@9" = "9.0.3"
"gitlab.com/kicad/libraries/kicad-symbols@9" = "9.0.3"
The @1 and @9 suffixes are compatibility lanes. They allow multiple incompatible versions of the same package path to coexist while keeping the selected version exact. Do not edit [dependencies.indirect] by hand. Commit hydrated pcb.toml files.

KiCad-Style Libraries

KiCad symbol, footprint, and 3D model relationships are configured at workspace root with [[workspace.kicad_library]] entries. Workspaces default to KiCad 9.0.3 and 10.0.3; stdlib generics use KiCad 10.0.3.
[[workspace.kicad_library]]
version = "9.0.3" # concrete version; major is used for symbol↔footprint matching
symbols = "gitlab.com/kicad/libraries/kicad-symbols"
footprints = "gitlab.com/kicad/libraries/kicad-footprints"
models = { KICAD9_3DMODEL_DIR = "gitlab.com/kicad/libraries/kicad-packages3D" }
parts = "https://kicad-mirror.api.diode.computer/kicad-parts-{version}.toml"
http_mirror = "https://kicad-mirror.api.diode.computer/{repo_name}-{version}.tar.zst"
The default KiCad 10 entry uses the same shape, but with:
[[workspace.kicad_library]]
version = "10.0.3"
symbols = "gitlab.com/kicad/libraries/kicad-symbols"
footprints = "gitlab.com/kicad/libraries/kicad-footprints"
models = { KICAD10_3DMODEL_DIR = "gitlab.com/kicad/libraries/kicad-packages3D" }
parts = "https://kicad-mirror.api.diode.computer/kicad-parts-{version}.toml"
http_mirror = "https://kicad-mirror.api.diode.computer/{repo_name}-{version}.tar.zst"
parts is optional. When set, the file is fetched alongside the selected symbols repo and parsed as a virtual pcb.toml containing parts = [...] entries for manifest-backed symbol→part inheritance. It supports the same {repo}, {repo_name}, {version}, and {major} placeholders as http_mirror. http_mirror is optional and supports {repo}, {repo_name}, {version}, and {major} placeholders. KiCad aliases in .zen files are reconciled by pcb sync. Transitive KiCad dependencies are written to [dependencies.indirect] using lane-qualified keys:
[dependencies.indirect]
"gitlab.com/kicad/libraries/kicad-footprints@10" = "10.0.3"
"gitlab.com/kicad/libraries/kicad-packages3D@10" = "10.0.3"
"gitlab.com/kicad/libraries/kicad-symbols@10" = "10.0.3"
Then regular dependency aliases apply (for example @kicad-symbols, @kicad-footprints).

Vendoring ([workspace].vendor)

Vendoring policy is controlled by the root workspace manifest:
[workspace]
vendor = ["github.com/myorg/**"]
  • pcb build and pcb publish use the same [workspace].vendor patterns.
  • pcb vendor without --all uses [workspace].vendor.
  • pcb vendor --all vendors everything.

Workspace Name ([workspace].name)

Workspace manifests can override the Diode workspace name used by board release uploads:
[workspace]
name = "my-workspace"
If name is omitted, pcb publish derives the workspace name from the first path segment of [workspace].repository. For example, anything.com/XYZ/boards/MyBoard uses XYZ.

Endpoint ([workspace].endpoint)

Workspace manifests can override the Diode host suffix used by CLI commands that talk to Diode services:
[workspace]
endpoint = "diode.computer"
  • endpoint = "diode.computer" resolves app/API URLs under app.diode.computer and api.diode.computer.
  • This applies to workspace-aware commands such as pcb auth, pcb bom, pcb publish, pcb preview, and routing flows.
  • Authentication is scoped per resolved endpoint, so logging into one workspace endpoint does not overwrite tokens for another.

BOM Matching ([workspace.bom])

Workspace manifests can opt into strict BOM matching for pcb bom availability lookups:
[workspace.bom]
strict = true
When enabled, pcb bom asks the BOM service to require exact MPN matches. The default is false, which preserves the legacy fuzzy matching behavior.

Registry Search Scope

Registry-backed pcb search defaults to the public Diode registry and registries in the workspace configured by [workspace].repository.
  • pcb search --registry github.com/diodeinc/registry ... overrides the default scope for that invocation.
  • Repeat --registry to search more than one registry.

Pseudo-Versions

Sometimes you need to depend on unreleased code—a bug fix that hasn’t been tagged, or a feature branch under development. Pseudo-versions provide a way to reference specific commits while maintaining version ordering. Format: v<base>-0.<timestamp>-<commit>
[dependencies]
# Branch reference - resolved to pseudo-version
"github.com/acme/component-lib" = { branch = "main" }

# Specific commit
"github.com/acme/component-lib" = { rev = "a1b2c3d4" }
When resolved, these become pseudo-versions like:
v0.3.15-0.20251120004415-137e2dcabc28…   # commit hash shortened here for readability
pcb sync writes the resolved pseudo-version back to the package manifest with the full 40-character commit hash. The base version (0.3.15) is the next patch version after the most recent tag reachable from that commit. This ensures pseudo-versions sort correctly: they’re newer than the tag they follow but older than the next official release. If the package has never been tagged, pseudo-versions start in the 0.1.1 family (for example 0.1.1-0.<timestamp>-<commit>), one patch above the initial unpublished release version 0.1.0. Pseudo-versions participate fully in MVS. If one package requires component-lib@0.3.14 and another requires the pseudo-version above, MVS selects the pseudo-version (it’s higher). This enables testing unreleased fixes without breaking version resolution. Use pseudo-versions sparingly. They represent unreleased, potentially unstable code. For production builds, prefer tagged releases.

Commands

pcb migrate

Runs project migrations using the latest stable pcbc toolchain, regardless of the workspace’s current pcb-version lane. After all migrations succeed, the command updates [workspace].pcb-version in pcb.toml to the target toolchain lane.
pcb migrate
pcb migrate ./path/to/workspace

pcb sync

Reconciles imports and hydrates package manifests. Run this after adding or removing imports or changing dependency versions.
pcb sync                    # Sync packages under the current workspace/package
pcb sync --offline          # Use only already cached package data
pcb sync -v                 # Print changed manifests
The command also downloads selected packages into the cache and vendors packages matched by [workspace].vendor.

pcb add

Adds or upgrades a direct dependency for the package in the current directory.
pcb add github.com/acme/regulators/Buck@1.2.3
pcb add github.com/acme/regulators/Buck@latest
pcb add -u                              # Upgrade all direct remote dependencies
pcb add -u github.com/acme/regulators/Buck
pcb add rewrites the direct dependency entry and rehydrates the package’s dependency closure.

pcb build

Builds a board or workspace package.
pcb build                    # Build default board
pcb build WV0002.zen         # Build a specific board file
pcb build --offline          # Build using only cached/vendored packages
pcb build --locked           # Do not rewrite manifests; recommended for CI
--locked checks that the hydrated state is sufficient and does not rewrite pcb.toml.

pcb list

Lists read-only package dependency information.
pcb list -m -u                              # Show compatible updates for direct dependencies
pcb list -m -versions github.com/acme/foo   # Show published versions for a dependency
pcb list -m -u must be run from a package directory. It reports direct remote dependencies only, showing the latest stable version in the same compatibility lane and the latest newer breaking lane when available. It does not update manifests.

pcb update

pcb update is deprecated. Use pcb add -u instead.
pcb add -u                   # Upgrade all direct remote dependencies
pcb add -u github.com/acme/regulators/Buck

pcb publish

Publishes packages by creating annotated git tags. Discovers which packages have changed since their last published version and tags them.
pcb publish                  # Publish all changed packages
pcb publish --bump=infer     # Infer bumps from commit history and dependency waves
pcb publish --bump=infer -y  # Skip the final publish confirmation
pcb publish --force          # Skip preflight checks
A package needs publishing if:
  • No version tag exists (unpublished)
  • Content hash differs from the published tag
  • Manifest (pcb.toml) hash differs from the published tag
Versions are computed automatically:
  • Unpublished: starts at 0.1.0
  • Published packages: apply the requested semver bump (patch, minor, or major)
  • --bump=infer: infers each package bump from conventional commits since its last tag, then raises dependents to at least the highest bump among published internal dependencies
  • -y / --yes: skips the final package publish confirmation prompt
Packages with interdependencies are published in waves—packages with no dirty dependencies first, then their dependents after pcb.toml files are updated.

pcb info

Displays workspace and package information.
pcb info                     # Show workspace summary
pcb info --format json       # Machine-readable output