Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

31. Package distribution: git, no registry

  • Status: Accepted
  • Date: 2026-09-05

Context

ADR 0018 built the module system and deferred this exact question on the spot: "A package manifest / registry. Way beyond M2. Path-based resolution needs no configuration and is enough to build with; a package story can come later without changing import's surface." Milestone 5 is that later point — a Korrin codebase should be able to depend on someone else's code, not just its own files.

The decisions in play:

  • Where does a package's code live, and how does a consumer get it onto disk?
  • What identifies a package and a version of it?
  • What does a manifest look like, and what does it pin?
  • Where does a fetched package land locally, and is that shared across projects or private to one?

Constraints that shape the answer: the project has no backend anywhere today — every deliverable is either a Rust binary or a static site on Cloudflare Pages (ADR 0030's editors/vscode/ and korrin-web/ are the two precedents, and neither talks to a server the project runs). ADR 0013 asks that a new dependency be justified and that large transitive trees be avoided. Milestone 6 — a native-module system — is the next milestone specifically because there is no way today to write a Korrin module in Rust and ship it, so a package in Milestone 5 can only be Korrin source.

Decision

Distribution is git, with no central hosting

A package is a git repository. There is no upload step, no package registry service, and nothing this project hosts or runs. korrin pkg clones or fetches a package directly from wherever its manifest says it lives.

The manifest: korrin.toml

[package]
name = "requests"
version = "0.3.0"

[dependencies]
json    = { git = "https://github.com/alice/korrin-json", tag = "v1.2.0" }
testkit = { git = "https://github.com/bob/korrin-testkit", branch = "main" }
scratch = { git = "https://github.com/carol/korrin-scratch", rev = "9f2a1c3b" }
  • [package].name and .version are a package's self-declared identity — shown in diagnostics, in korrin pkg list, and on the community index (ADR 0034). They play no role in resolving a dependent's import statements; see the next point.
  • A [dependencies] table key is the name a dependent imports it by, independent of what the upstream package calls itself. import "json" means whatever this manifest calls json, regardless of what alice/korrin-json's own [package].name says. This is the same principle ADR 0018 already gave files (import "./a/config" as cfg renames at the use site) applied to packages, and it avoids a separate rename field.
  • Each dependency needs a git URL and exactly one of tag, branch, or rev. Zero or more than one is an error from korrin pkg add/install, not a silently-picked default.
  • A package's entry file is the fixed path src/lib.kor — mirroring Cargo's src/lib.rs — not derived from [package].name, since that name can legitimately differ from what a dependent calls the package.

Packages are leaf-only in Milestone 5

If a package's own korrin.toml declares a non-empty [dependencies] table, korrin pkg add/install refuses it: "json declares dependencies of its own — packages with dependencies are not supported in Milestone 5." A package may only depend on nothing.

The lockfile: korrin.lock

# @generated by `korrin pkg` — do not edit by hand.
version = 1

[[package]]
name = "json"
git = "https://github.com/alice/korrin-json"
rev = "a1b2c3d4e5f6..."
path = ".korrin/packages/json"

Every dependency resolves to a pinned commit, even one specified by tag or branch — a build stays reproducible even if the upstream ref later moves. korrin pkg update is the only thing that ever moves a pin.

An application (any korrin.toml with a non-empty [dependencies] table run via korrin run) commits korrin.lock, for the same reproducibility reason Cargo asks binaries to commit Cargo.lock. A package meant to be depended on does not — mirroring Cargo's own binary/library split, and costing nothing to state now even though leaf-only packages give this rule little practical bite yet.

No versioning solver

Because a dependency is an exact git ref, not a version range, there is nothing to solve. korrin pkg add pins a commit at add time. Combined with leaf-only packages, there is no transitive dependency graph to flatten either — resolution is: read [dependencies], fetch each entry once, write one lockfile line per entry. No semver crate is added; it would have no load-bearing job under this model.

Local storage: project-local, no shared cache

A fetched package lands at .korrin/packages/<name>/, a real checkout (not a symlink), next to the korrin.toml that declared it — gitignored. Manifest and lockfile discovery walks upward from the entry file's own directory, not the process's working directory, matching the principle ADR 0018 already set for ./ resolution.

Consequences

  • Nobody has to trust, fund, or operate a registry for Korrin packages to exist. Anyone with a git host (or a bare local repo) can publish one.
  • A package author who wants to depend on another package cannot, yet — that is real functionality given up, not an oversight. It is the direct price of landing packaging with zero new hosted infrastructure and no dependency solver; a transitive story is real future work, not blocked by anything chosen here.
  • No global package cache means repeated korrin pkg install across different projects re-clones the same repository each time. Acceptable at Milestone 5's scale; a shared cache is legitimate future work (see "Alternatives considered").
  • korrin.lock's commit-or-not convention needs restating, and possibly revisiting, once transitive dependencies exist and a package can plausibly want its own lock for its own dependencies.
  • A package pinned by branch silently becomes stale until korrin pkg update is run — this is the intended tradeoff (an explicit update step, not automatic tracking), matching how rev/tag pins already work.

Alternatives considered

  • A hosted registry (crates.io-style: upload, storage, an API). Real functionality — search, download counts, a single canonical source — but needs a backend this project has never had, funded and operated indefinitely. Far more than packaging needs to exist at all.
  • Vendoring only (copy a dependency's source into your own repo, no live reference). Simplest possible, and needs nothing this ADR designs — but it doesn't solve "depend on someone else's code" in any lasting way; every update is a manual re-copy with no record of where it came from.
  • A shared global cache (~/.korrin/cache/, à la ~/.cargo/registry, reusable across projects via git worktree add or a maintained bare mirror). More bandwidth- and disk-efficient, and the design Cargo/uv actually use — but it is real, ongoing machinery: locking against concurrent korrin pkg runs, a staleness/GC policy, and a first cross-platform cache-directory decision. On Windows specifically, the natural cheap sharing mechanism — a symlink — needs Developer Mode or elevated privileges, a concrete wrinkle worth avoiding for a first cut. Legitimate future work once the project-local approach's cost is actually felt.
  • Transitive dependencies from the start, with a real solver. The standard shape for a package manager, but a dependency-graph solver is substantial, separately-scoped work, and nothing about Milestone 5's actual goal — letting one person depend on another's code at all — needs it yet.