Packages
A package bundles two things a schema can depend on — generators and data — behind one namespaced name, so a community can ship reusable building blocks. Phony is built so that installing a package can never run code on your machine: a package carries only data (Tier 1 compositions and assets), never a native artifact.
The package document
A package is a single JSON document — conventionally phony.json at the root of a package directory:
{
"name": "@acme/finance",
"version": "0.1.0",
"generators": [
{ "name": "@acme/finance:gross",
"params": { "net": { "type": "number", "default": 100 },
"rate": { "type": "number", "default": 0.2 } },
"let": { "tax": { "computed": "round(net * rate, 2)" } },
"body": "{{ net + tax }}" }
],
"assets": {
"lists": { "@acme/finance:currencies": { "*": ["USD","EUR","TRY"], "tr_TR": ["TRY","USD","EUR"] } },
"models": { "@acme/finance:ticker": { "*": "models/ticker.ngram" } }
}
}| key | meaning |
|---|---|
name / version | the package's identity |
generators | an array of composition definitions — Tier 1, pure data |
assets.lists | list contributions: asset → { locale → array | path } |
assets.models | model contributions: asset → { locale → .ngram path } |
Generators
Each entry is a composition — the same { name, params, let, body } shape you register by hand. On load, each is compiled and registered under its name, so a schema reaches it with use exactly like a built-in.
Assets
Assets are the package's locale contributions. Each entry maps an asset name to per-locale data, merged into the shared contribution index — so a package can add a locale to an asset another package defined, without either touching the other. The * locale is the locale-independent fallback.
A list value is an inline JSON array, or (when loading from a directory) a relative path to a .json array. A model value is a relative path to a portable .ngram — so model assets require a directory to resolve against. (Either value can instead be a remote { url, sha256 } entry — see remote assets.)
There's a deliberate asymmetry in the naming rules, and it's the whole point of the contribution model:
- Generator names are scoped. Every generator a package defines must live in that package's own namespace (
@acme/finance:…) — the loader rejects a package that tries to register@other:x. Generators are owned. - Asset keys are open. An asset key is not required to match the package name, precisely so
@alice/coin-trnlcan contribute Turkish data to thecoin.flipasset that@phony/coindefined. Data is contributed, not owned.
That asymmetry is what lets locale coverage grow from separate, data-only packages while every generator stays pure and unmodified.
Loading
Loading is additive: point the loader at the same registry and asset index for every package in a dependency closure, then attach the assets once.
use std::sync::Arc;
use phony_pgdl::{load_package_dir, LocaleAssets, Registry};
let mut registry = Registry::with_builtins();
let mut assets = LocaleAssets::new(vec!["tr_TR".into(), "base".into()]);
// Each call registers the package's generators and contributes its assets.
let report = load_package_dir(&mut registry, &mut assets, "packages/finance")?;
registry.set_assets(Arc::new(assets)); // wire the merged data in once| API | use |
|---|---|
load_package(&mut reg, &mut assets, &doc) | load an in-memory document (inline list assets only) |
load_package_dir(&mut reg, &mut assets, dir) | load <dir>/phony.json, resolving .ngram / .json paths relative to dir |
Both return a LoadReport (the package name/version and the generator and asset names it contributed) — enough to report what was installed or to build a lock.
The package manager (git-based)
The loader above is the bottom layer. On top of it, the phony CLI ships a git-based package manager — no central registry, no accounts, no publish step: a package is a git repo (or a subdirectory in one), addressed as <host>/<path>@<version-or-ref>.
| Command | Does |
|---|---|
phony install | resolve the dependency closure into the store and write phony.lock (--frozen forbids any fetch; --update re-resolves) |
phony add <spec> | add a dependency to phony.json and lock it (--name asserts the repo's package name) |
phony remove <name> | remove a dependency and re-resolve the lock |
phony update [name] | re-resolve (all, or just name) ignoring the lock |
phony list [--json] | print the resolved closure |
Declaring a dependency. A dep is either the object form { "git": "https://…", "version": "^1.2", "path": "sub/dir" } or the shorthand string github.com/acme/finance@^1.2. version and ref are mutually exclusive: a version is a semver range resolved against the repo's vX.Y.Z tags; a ref pins an exact branch or commit. In the shorthand, a spec that looks like a semver range is treated as a version, otherwise as a ref (so @main or @a1b2c3d is a ref). An optional path points at the subdirectory holding that package's phony.json.
Resolution is MVS. Minimal version selection gathers every range requested for a package across the whole closure and picks the lowest version that satisfies all of them (prereleases excluded unless a range names one). "Lowest that works" makes builds stable: a new upstream release never silently changes your resolution — you only move when you ask to. Two different git URLs claiming the same package name, or a version and a ref for the same package, are hard conflicts.
The lockfile. phony.lock records each resolved package pinned to an exact git commit (the commit is authoritative, not the tag) plus a package checksum, in topological (leaves-first) order. A locked install is byte-for-byte reproducible; --frozen requires the lock to be current and the cache warm and refuses to touch the network.
The content-addressed store lives at $PHONY_HOME/store (default ~/.phony/store, created 0700):
~/.phony/store/
git/<host>/<scope>/<pkg>/<commit>/ ← a materialised package, keyed by commit
blobs/<sha256> ← remote-asset blobs, keyed by content hashEvery entry is written to a temp path and atomically renamed into place, so a reader never sees a half-populated package and two concurrent installs can't corrupt each other; an entry that already exists is a no-op, so the same commit is fetched once and shared across projects.
Remote (release) assets. A big binary — a multi-megabyte .ngram model — doesn't belong in git history. Declare it as a { "url": "https://…", "sha256": "…" } entry under assets.lists/assets.models instead of a path. On install it's fetched over https only, its bytes are verified against the 64-hex sha256, and it's stored at blobs/<sha256>; a mismatch is a hard error, and --frozen refuses to fetch an uncached blob. phony generate loads the project's whole locked closure through the same additive loader before generating.
Installing still never runs code: a package carries only data — Tier 1 compositions and assets. The full design is in Spec → Package Manager.