Example
facets.lock
name is still viper-planning and its files still live under skills/viper-planning/. Only the file on disk is named team-planning.
Fields
number
required
The schema version. A normal install writes
0.3. Version 0.2 remains readable.Versions are dispatched by exact match, never by ordering. A version number names a schema, not a position in a sequence, so a loader recognizes exactly what it supports and fails on anything else. There is no cross-version fallback.A normal install migrates a verified 0.2 lockfile to 0.3. A frozen install never rewrites it.map<name, entry>
required
One entry per resolved facet, keyed by name. The name lives only in the key. Keys are written sorted, so add, remove, and add again produces byte-identical output.
Entry fields
tagged union
required
Provenance, discriminated by
kind, so an illegal cross-kind combination cannot be expressed:registry:{ kind, registry }, the origin the artifact was resolved from. It carries no version specifier, becauseversionis the resolved identity.git:{ kind, url, commit }. The commit is required, since it is the immutable identity that makes a git install reproducible. The symbolic ref is deliberately not recorded: a ref is what the user requested, and it is mutable.local:{ kind, path }.
string
required
An exact
M.N.P version. No ranges, wildcards, or prerelease. Always written from resolution, never copied from a specifier.string
required
The canonical fingerprint of the verified content, derived from the freshly verified or built archive rather than trusted from input.
array
required
The text assets this facet contributed, each
{ scope, type, name, materialization, files }. scope is system, user, or project; type is skill, agent, or command. Names MUST pass asset-name validation, so a crafted lockfile cannot smuggle path traversal into adapter I/O.This field holds text assets only: there is no server arm. MCP servers are a separate identity space, and a server-only facet is represented with an empty assets array.name and files are authored, unchanged by aliasing, because they anchor integrity.materialization is the resolved disposition, and all three arms are legal here. This array is the resolved set, not the on-disk set, so an omitted text asset stays listed with its complete records.files records every file the text asset owns as { path, integrity }, sorted strictly ascending by path, with at least one record. Each hash is the recomputed per-entry hash of the verified file. Archive-only supplementary files never appear here; they stay covered by facet-level integrity.Ownership is derived, not merely asserted. An agent or command entry MUST contain exactly one record at its own canonical primary path, and every record in a skill entry MUST live under that skill’s authored root with SKILL.md present. A path no derivation from the text asset’s authored identity could produce is rejected, so integrity and deletion can never be pointed at an unrelated file.A
0.2 text-asset entry has no materialization field. It reads as { "kind": "authored" }, the only disposition that schema could express.Unrecognized fields
Fields the schema does not define are accepted and survive a rewrite. When an install rebuilds the lockfile it carries forward unrecognized fields on the document, on each facet entry, on a retained entry’ssource when the kind is unchanged, on each text asset matched by authored identity, and on each file record matched by path. Migrating 0.2 to 0.3 preserves them exactly as a same-version write does.
A schema-defined field always wins over an unrecognized field of the same name, so a future version claiming a name cannot be shadowed by stale data. Unrecognized fields attached to something that no longer exists are dropped with it.
Why MCP did not move the lockfile
The lockfile records no MCP declaration, effective server name, or server disposition. A facet’sintegrity already commits to the exact bytes of the facet.json its declarations live in. Recording them again would create a second copy that can disagree with the first.
Two consequences follow:
- Declaration drift is already detected. Changing a declaration changes
facet.json, which changes the facet’s integrity, which no longer matches the locked value. - Server intent lives in
facets.jsonalone. Aliases and omissions for servers are project intent rather than resolution. Approval is machine-local and is recorded in neither file.