phony.json and phony.lock
The package manifest and lockfile, verified against phony-cli/src/pkg/* and crates/pgdl/src/package.rs. A package is a single directory with a phony.json at its root. For the model and workflow see Packages and the package CLI commands.
Manifest fields
| Field | Type | Required | Description |
|---|---|---|---|
name | string | yes | The package id @scope/pkg (non-empty). |
version | string | yes | The package semver (1.2.0); the resolved git commit is authoritative. |
generators | array | no | Tier 1 composition generators (pure data — installing a package never runs code). |
assets | object | no | Locale contributions: lists and models (below). |
dependencies | object | no | Dependency name → git source (object or shorthand). |
{
"name": "@acme/finance",
"version": "0.1.0",
"generators": [
{ "name": "@acme/finance:gross", "params": {}, "let": {}, "body": "…" }
],
"assets": {
"lists": { "@acme/finance:currencies": { "*": ["USD","EUR","TRY"], "tr_TR": ["TRY","USD"] } },
"models": { "@acme/finance:ticker": { "*": "models/ticker.ngram" } }
},
"dependencies": {
"@acme/colors": { "git": "https://github.com/acme/colors", "version": "^1.2" }
}
}generators
Each generator is a composition. Every generator name must be in this package's own namespace — it must start with <name>: (e.g. package @acme/finance → @acme/finance:gross). A generator outside the namespace, or without a name, is a validation error.
assets
Two groups, each mapping an asset name to a per-locale map:
assets.lists = { "<asset>": { "<locale>": <list-value>, … }, … }
assets.models = { "<asset>": { "<locale>": <model-value>, … }, … }The locale key * is the locale-independent contribution; other keys are locale tags (e.g. tr_TR). Loading is additive across a dependency closure, merged into a shared contribution index. Asset keys are intentionally looser than generator names: a package may contribute to another package's asset (the locale-contribution model), so asset keys are not namespace-restricted.
List value — one of:
- an inline JSON array;
- an inline JSON object (a map, for
key-grouped data); - a path string to a
.jsonfile (array or object), relative to the package directory; - a remote asset
{ url, sha256 }whose blob holds that JSON.
Model value — one of:
- a path string to a portable
.ngramfile, relative to the package directory (see the .ngram format); - a remote asset
{ url, sha256 }resolved to a store blob.
File paths must stay inside the package: an absolute path or any .. component is rejected (directory-traversal guard).
dependencies
Each entry maps a dependency name to a git source, in object or shorthand form.
Object form:
| Key | Type | Description |
|---|---|---|
git | string (required) | The clone URL. May not begin with - (argument-injection guard). |
version | string | A semver range (^1.2, >=1 <2, 1.2.3, *). Must not be v-prefixed. |
ref | string | A branch or commit, pinned exactly. May not begin with -. |
path | string | A relative subdirectory holding the package's phony.json. No .., no absolute. |
version and ref are mutually exclusive, and exactly one is required.
Shorthand form — a string <host>/<path>@<spec>:
- expands
gittohttps://<host>/<path>; <spec>is a version when it is semver-shaped (starts with a digit,v+ digit, or one of^ ~ > < = * x) and not commit-shaped; otherwise it is a ref. A hex, commit-shaped spec (all hex and either ≥ 7 chars or containing ana–fletter) is always a ref, so a digit-leading commit like1a2b3c4is not misread as a version.
{ "dependencies": {
"@acme/colors": "github.com/acme/colors@2.1.0",
"@acme/nightly": "github.com/acme/colors@main",
"@acme/pinned": { "git": "https://github.com/acme/x", "ref": "1a2b3c4", "path": "pkg" }
} }Version resolution (MVS)
Only vX.Y.Z (or bare X.Y.Z) git tags are version candidates. Resolution uses minimal version selection: for each package it picks the lowest tag that satisfies all the version ranges gathered across the closure. Prereleases are excluded unless a range explicitly names one. A ref selector pins its branch/commit directly, bypassing version matching.
phony.lock
The resolved closure, pinned for reproducible installs. Consumed verbatim by install (no re-resolution) unless the caller asks to update. It holds no absolute paths — store locations are derived from the commit.
| Field | Type | Description |
|---|---|---|
version | number | Lockfile format version (currently 1). |
packages | array | One LockEntry per resolved package. |
Each LockEntry:
| Field | Type | Description |
|---|---|---|
name | string | Package id. |
git | string | The clone URL. |
version | string | The satisfied version (tag) or ref. |
commit | string | The exact resolved commit. |
checksum | string | sha256:<hex> of the package directory. |
Entries preserve the closure's topological order (leaves before dependents), so install (a fresh resolve) and generate (which reads the lock) load packages in the same order — keeping the (asset, locale) "later wins" tiebreak reproducible.
{
"version": 1,
"packages": [
{ "name": "@acme/colors", "git": "https://github.com/acme/colors",
"version": "v2.1.0", "commit": "…40 hex…", "checksum": "sha256:…" }
]
}Package checksum
checksum is sha256:<hex> over the package directory's regular files: relative paths (always /-separated) sorted lexicographically as raw bytes, each file fed to one hash stream as path · 0x00 · the byte length as an ASCII-decimal string · 0x00 · the file bytes. Symlinks are a fatal error; no filesystem metadata (mode, mtime) is hashed; empty directories are not represented.
Content-addressed store
The global store is shared across all projects, at $PHONY_HOME/store (else ~/.phony/store). It holds two kinds of entry:
| Path | Contents |
|---|---|
git/<host>/<scope>/<pkg>/<commit> | A fetched git package, materialised at an exact commit. |
blobs/<sha256> | A remote asset blob, addressed by its lowercase-hex sha256. |
Entries are installed atomically: content is written to a temp path, then renamed into place (the rename is the commit point, so a reader never sees a partial entry). An existing entry is left untouched. The store root is created user-only (0700 on Unix).
Remote (release) assets
Large models and lists can be fetched over https and kept out of git as content-addressed blobs. A remote asset reference is an object under assets.lists / assets.models:
| Key | Type | Description |
|---|---|---|
url | string | An https:// URL. |
sha256 | string | Lowercase-hex sha256; an optional sha256: prefix is tolerated. |
Both keys must be present, so an inline data map is never mistaken for a remote asset. The blob address is the bare 64-char lowercase-hex digest; it is validated as exactly 64 hex chars before use (path-traversal guard). The bytes are fetched over https and verified against sha256 before entering the store — a mismatch is an error. --frozen forbids any fetch: a missing blob then errors instead of downloading.
{ "assets": {
"models": {
"@acme/finance:big": {
"*": { "url": "https://cdn.example.com/big.ngram", "sha256": "sha256:9f86d0…" }
}
}
} }