16  panproto-cli

The panproto-cli crate provides the schema binary. It wraps panproto-core and panproto-vcs for terminals, scripts, and CI pipelines. The binary has two groups of commands: schema tools (validate, check, diff, lift) and version control (init through gc).

16.1 Schema tool commands

These operate on schema files directly, without a .panproto/ repository.

16.1.1 schema validate

schema validate --protocol atproto schema.json

Loads the schema from JSON, resolves the protocol by name, and runs protocol-aware validation. In addition to structural validation, schema validate type-checks the protocol’s schema theory equations via gat_validate::validate_theory_equations. Exits with code 1 on failure. Calls panproto_core::schema::validate.

16.1.2 schema check

schema check --src old.json --tgt new.json --mapping migration.json
schema check --typecheck --src old.json --tgt new.json --mapping migration.json

Loads both schemas and the migration mapping, builds the theory registry from the protocol, and runs mig::check_existence. With --verbose, also dumps the full JSON report.

The --typecheck flag additionally runs GAT-level type-checking on the migration: it validates that the migration’s vertex/edge maps form a well-typed theory morphism and that the protocol theory’s equations typecheck. When --typecheck is set, the command calls gat_validate::validate_migration and gat_validate::validate_theory_equations and includes their diagnostics in the output.

16.1.3 schema diff

schema diff old-schema.json new-schema.json
schema diff --theory old-schema.json new-schema.json

Computes structural differences between two schema files. Reports added/removed vertices and edges, kind changes, constraint changes, variant changes, ordering changes, and usage mode changes. Unlike git diff, this understands the schema graph structure: it distinguishes a tightened constraint from a relaxed one, and a removed coproduct variant from a removed optional field.

--stat uses format::format_diff_stat to show a summary of additions, removals, and modifications per field. --name-only uses format::format_diff_name_only to list only the names of changed elements. --staged diffs the index against HEAD rather than comparing two files.

The --theory flag computes a theory-level diff instead of a schema-level diff. This shows changes to sorts, operations, and equations in the protocol’s GAT theory rather than changes to vertices and edges in the schema graph. Theory-level diffs are useful for understanding how a schema change affects the mathematical structure that governs migration correctness.

16.1.4 schema scaffold

schema scaffold --protocol atproto schema.json
schema scaffold --depth 4 --max-terms 500 --json schema.json

Generates a skeleton instance from a schema using the free model construction. The command calls free_model from panproto-gat with the protocol’s schema theory and outputs a minimal valid instance.

Flags:

  • --depth <n> sets the maximum term generation depth (default: 3). Maps to FreeModelConfig::max_depth.
  • --max-terms <n> sets the maximum number of terms per sort (default: 1000). Maps to FreeModelConfig::max_terms_per_sort.
  • --json outputs the result as JSON instead of the default human-readable format.

16.1.5 schema normalize

schema normalize schema.json
schema normalize --identify A=B --identify f=g schema.json
schema normalize --json schema.json

Normalizes a schema by quotienting its theory. Without --identify flags, this runs structural normalization (deduplication, canonical ordering). With --identify flags, it calls quotient from panproto-gat to merge the specified sorts or operations.

Flags:

  • --identify <a>=<b> specifies a pair of names to identify. Can be repeated. Pairs are parsed as sort or operation identifications based on the theory.
  • --json outputs the normalized schema as JSON.

16.1.6 schema typecheck

schema typecheck schema.json
schema typecheck --src old.json --tgt new.json --migration mig.json

Runs GAT type-checking on a schema’s theory equations. Without --src/--tgt/--migration, it loads the schema, resolves its protocol theory, and runs typecheck_theory. With the migration flags, it additionally type-checks the migration morphism and validates equation preservation.

This command is the standalone equivalent of the type-checking that schema add and schema commit perform automatically. It’s useful for CI pipelines that want to validate schemas without a .panproto/ repository.

16.1.7 schema verify

schema verify --protocol atproto schema.json
schema verify --max-assignments 50000 schema.json

Builds a model from the schema and verifies all theory equations via check_model_with_options. Reports any equation violations with the specific variable assignment that caused the failure.

Flags:

  • --max-assignments <n> sets the maximum number of variable assignments to enumerate per equation (default: 10,000). Maps to CheckModelOptions::max_assignments.

16.1.8 schema lift

schema lift --migration mig.json --src-schema old.json --tgt-schema new.json record.json

Compiles the migration, parses the input JSON as a W-type instance (auto-detecting the root vertex), applies lift_wtype, and writes the result as pretty-printed JSON to stdout. This command has no git equivalent; it transforms actual data through a schema change.

16.2 Expression commands

The schema expr subcommand group provides tools for the Haskell-style expression language.

# Parse and print AST
schema expr parse "x + 1"

# Evaluate and print result
schema expr eval "2 + 3"

# Pretty-print in canonical form
schema expr fmt "\x->x+ 1"

# Check syntax without evaluating
schema expr check "let x = 1 in x + 2"

The existing GAT commands are now under schema expr gat-eval and schema expr gat-check.

16.3 Version control commands

These require a .panproto/ repository (created by schema init). See Chapter 17 for the architectural details of the object store, DAG algorithms, and merge strategy.

16.3.1 schema init

schema init

Creates .panproto/ with the object store, refs, logs, and HEAD pointing to main. Unlike git init, this doesn’t create a working tree. The repository tracks a single schema’s evolution, not a directory of files. The -b <name> flag sets the initial branch name (default: main).

16.3.2 schema add

schema add schema.json

Stages a schema for the next commit. Internally: loads the schema, hashes it via hash::hash_schema, computes check::diff against HEAD’s schema, calls auto_mig::derive_migration to produce the migration morphism, runs mig::check_existence to validate it, and writes the result to .panproto/index.json. Unlike git add, which stages file blobs, this stages a schema graph and simultaneously derives and validates the migration that connects it to the previous version.

Flags: --dry-run runs the full pipeline but doesn’t write to the index (calls index::stage in preview mode). --force skips the check_existence validation step, allowing staging of schemas that fail migration validation.

16.3.3 schema commit

schema commit -m "add verification status field"
schema commit --skip-verify -m "force commit despite GAT warnings"

Reads the index, stores the schema and migration as content-addressed objects, creates a CommitObject with parent pointer(s), advances the branch ref, appends a reflog entry, and clears the index. Unlike git commits which snapshot a tree of files, each commit here stores exactly one schema and one migration morphism.

Before creating the commit, the command checks the staged GAT diagnostics. If the gat_diagnostics field contains type errors or equation violations, the commit is blocked with VcsError::ValidationFailed. This ensures that only schemas with well-typed theories and satisfied equations are committed.

The --skip-verify flag bypasses the GAT diagnostic check, allowing commits even when type errors or equation violations are present. This is an escape hatch for advanced users who need to commit work-in-progress schemas. The diagnostics are still stored in the commit metadata for later inspection.

The --amend flag calls repo::amend, which replaces the most recent commit by creating a new commit with the same parent(s) but updated schema, migration, and message.

16.3.4 schema status

schema status

Prints the current HEAD state (branch name or detached commit ID) and whether the index has a staged schema. The -s flag produces a compact one-line summary. --porcelain produces machine-readable output suitable for scripts.

16.3.5 schema log

schema log
schema log -n 5

Walks the DAG from HEAD using a timestamp-ordered max-heap (see dag::log_walk). Handles merge commits by visiting each commit exactly once. Prints commit ID, author, date, schema hash, and message.

Additional flags: --oneline calls format::format_commit_oneline for compact output. --graph renders branch topology as ASCII art. --all includes all branches, not just the current one. --format <fmt> uses format::format_commit with a custom format string. --author <name> and --grep <pattern> filter commits by author or message substring.

16.3.6 schema show

schema show main
schema show v1.0
schema show abc1234...

Resolves the argument as a branch, tag, or raw object ID (via refs::resolve_ref), then loads and displays the object. For commits: schema ID, parents, migration ID, protocol, author, message. For schemas: protocol, vertex count, edge count. For migrations: source/target IDs, mapping counts.

--stat appends a change summary via format::format_diff_stat. --format <fmt> uses format::format_commit for custom output.

16.3.7 schema branch

schema branch             # list branches
schema branch feature     # create at HEAD
schema branch -d feature  # delete

Calls refs::create_branch, refs::list_branches, or refs::delete_branch. Additional flags: -D calls refs::force_delete_branch to delete even if not fully merged. -m old new calls refs::rename_branch. -v lists branches with their commit IDs.

16.3.8 schema tag

schema tag v1.0
schema tag -d v1.0

Calls refs::create_tag, refs::list_tags, or refs::delete_tag. Tags are immutable pointers, typically marking schema releases. -a -m "message" calls refs::create_annotated_tag to store a TagObject with tagger, timestamp, and message. -f calls refs::create_tag_force to overwrite an existing tag.

16.3.9 schema checkout

schema checkout feature

If the argument matches a branch name, calls refs::checkout_branch to set HEAD as a symbolic ref. Otherwise resolves it as a commit ID and calls refs::checkout_detached. -b <name> calls refs::create_and_checkout_branch to create a new branch at HEAD and switch to it. --detach <ref> forces detached HEAD at the resolved ref.

16.3.10 schema merge

schema merge feature
schema merge --verbose feature

Calls repo::merge. First checks for fast-forward (if HEAD is an ancestor of the target, just moves the ref). Otherwise finds the merge base via dag::merge_base, loads the three schemas (base, ours, theirs), and runs merge::three_way_merge. Clean merges are auto-committed as merge commits with two parents. Conflicts are reported with their structural types.

The merge algorithm differs from git in a fundamental way: instead of three-way text merge with conflict markers, it computes the union of non-conflicting structural changes (vertex additions, edge additions, constraint changes, variant additions) and produces typed conflict descriptors for overlapping modifications. There are no conflict markers to resolve manually; conflicts are reported as structured data.

The --verbose flag enables pullback overlap detection. When set, the merge computes the pullback of the two branch theories over the merge base theory using panproto_gat::pullback. The pullback_overlap field in the merge result reports which sorts and operations both branches modified. This helps developers understand the structural intersection of concurrent changes.

Additional flags control merge behavior via MergeOptions: --no-commit stages the result without committing. --ff-only fails if fast-forward isn’t possible. --no-ff forces a merge commit even for fast-forwards. --squash collapses all changes into a single non-merge commit. --abort cancels a conflicted merge and restores the pre-merge state. -m "msg" provides a custom merge commit message.

16.3.11 schema rebase

schema rebase main

Calls rebase::rebase. Finds the merge base, collects commits from merge_base to HEAD, moves the branch ref to the target, then replays each commit via merge::three_way_merge using the commit’s parent schema as the base. Each replayed commit gets a new commit ID (different parent). Stops on conflict.

16.3.12 schema cherry-pick

schema cherry-pick abc1234...

Calls cherry_pick::cherry_pick. Loads the commit and its parent, extracts the schema diff, and three-way merges it onto HEAD using the parent as the base. Creates a new commit with the message prefixed by “cherry-pick:”.

Flags are passed via CherryPickOptions: -n (no-commit) stages the result without creating a commit. -x (record origin) appends the source commit ID to the message.

16.3.13 schema reset

schema reset --soft v1.0
schema reset --mixed v1.0   # default
schema reset --hard v1.0

Calls reset::reset. Soft moves the ref only. Mixed also clears the index. Hard also writes the target commit’s schema to the working schema file. All modes append a reflog entry.

16.3.14 schema stash

schema stash push -m "wip"
schema stash pop
schema stash list
schema stash drop

Stashes are stored as special commits with the HEAD commit as parent. The refs/stash ref points to the latest; the reflog preserves the stack history. Additional subcommands: apply calls stash::stash_apply to re-apply the top entry without removing it. show calls stash::stash_show to display the stash contents. clear calls stash::stash_clear to discard all entries.

16.3.15 schema reflog

schema reflog
schema reflog main

Reads the NDJSON reflog file for the specified ref (default: HEAD). Every mutation (commit, merge, checkout, reset, rebase, cherry-pick) appends an entry with old ID, new ID, author, timestamp, and action description. --all shows reflogs for every ref, not just the specified one.

16.3.16 schema bisect

schema bisect v1.0 HEAD

Calls bisect::bisect_start. Finds the path from good to bad, presents the midpoint. The user inspects the midpoint (e.g. via schema show) and re-runs with a narrowed range. Converges in \(O(\log n)\) steps.

16.3.17 schema blame

schema blame --element-type vertex user_status
schema blame --element-type edge "post->text:prop:text"
schema blame --element-type constraint "text:maxLength"

Calls blame::blame_vertex, blame::blame_edge, or blame::blame_constraint. Walks the first-parent chain from HEAD, checking at each commit whether the element is present in that commit’s schema but absent from its parent’s. Returns the introducing commit. Unlike git blame which operates on lines of text, this operates on structural elements of the schema graph.

16.3.18 schema gc

schema gc

Calls gc::gc. Collects all ref targets as roots, marks reachable objects via BFS through commit/schema/migration links, enumerates all objects in the store, and deletes unreachable ones. Reports the number of reachable and deleted objects. --dry-run passes GcOptions { dry_run: true }, which reports what would be deleted without removing anything.

16.4 Output formatting

The format.rs module provides reusable formatting functions for commit and diff output.

  • format_commit(commit, fmt_str): interpolates a format string with placeholders: %H (full hash), %h (short hash), %s (subject/message), %an (author name), %ad (author date)
  • format_commit_oneline(commit): produces <short_hash> <message>, used by schema log --oneline
  • format_diff_stat(diff): summary table of additions, removals, and modifications per schema field
  • format_diff_name_only(diff): plain list of changed element names
  • format_diff_name_status(diff): element names prefixed with A (added), D (deleted), or M (modified)

16.5 Error handling

The CLI uses miette for user-facing error diagnostics. All handlers return miette::Result<()> and convert errors using .into_diagnostic().wrap_err(...). Library crates use thiserror; miette is CLI-only.

16.6 Adding a new subcommand

  1. Add a variant to the Command enum in main.rs with clap attributes
  2. Implement the handler as fn cmd_<name>(...) -> Result<()>
  3. Wire it up in the match cli.command block
  4. Add tests via the Rust API in tests/integration/

For commands that are planned but not yet implemented, follow the remote stub pattern: add the Command variant and handler, but return an error immediately using miette::bail!("remote operations are not yet supported"). See the remote commands below for an example.

16.7 Lens subcommands

The schema lens command is organized into subcommands:

16.7.1 schema lens generate

schema lens generate old.json new.json --protocol atproto

Auto-generates a protolens chain between two schemas. The --protocol flag selects the protocol theory. Output is a JSON-encoded protolens chain. -o writes to a file instead of stdout. --fuse merges the chain into a single protolens.

16.7.2 schema lens compose

schema lens compose chain1.json chain2.json --protocol atproto

Composes two protolens chains (or lenses) into a single chain. -o writes the result to a file.

16.7.3 schema lens apply

schema lens apply chain.json data.json --protocol atproto

Applies a saved lens or protolens chain to a data file.

16.7.4 schema lens verify

schema lens verify data.json --protocol atproto

Verifies the GetPut and PutGet lens laws on a test instance.

16.7.5 schema lens inspect

schema lens inspect chain.json

Prints a human-readable summary of a protolens chain: number of steps, optic classification, complement cost, and whether the chain is lossless.

16.8 Data subcommands

The schema data command group handles data migration and staleness tracking:

16.8.1 schema data migrate

schema data migrate records/
schema data migrate records/ --dry-run
schema data migrate records/ --range HEAD~3..HEAD
schema data migrate records/ --backward
schema data migrate records/ -o migrated/

Migrates data files to match the current schema version. --dry-run previews the migration without writing. --range restricts to a commit range. --backward restores from stored complements. -o writes to a different directory.

16.8.2 schema data convert

schema data convert --src-schema old.json --tgt-schema new.json record.json

One-step data conversion between schemas via auto-generated protolens.

16.8.3 schema data sync

schema data sync records/
schema data sync records/ --edits
schema data sync records/ --target v2

Syncs data files to match a target schema version. With --edits, stores an EditLogObject in the VCS recording the translated edits. --target specifies the target ref (default: HEAD).

16.8.4 schema data status

schema data status records/
schema data status records/ --verbose

Reports data staleness: how many files exist, which schema HEAD tracks, and whether any data sets are tracked. --verbose lists individual files.

16.9 Remote command stubs

The commands remote, push, pull, fetch, and clone are registered as Command variants with clap attributes but their handlers immediately return an error: "schema repositories are currently local-only; remote operations are planned for a future release". This reserves the command names and provides a clear message to users, while keeping the implementation surface minimal.