Skip to content

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:

json
{
  "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" } }
  }
}
keymeaning
name / versionthe package's identity
generatorsan array of composition definitions — Tier 1, pure data
assets.listslist contributions: asset → { locale → array | path }
assets.modelsmodel 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-trnl can contribute Turkish data to the coin.flip asset that @phony/coin defined. 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.

rust
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
APIuse
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>.

CommandDoes
phony installresolve 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 hash

Every 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.

Phony Cloud — Documentation & Specification