Output contract#

--format json and its compatibility alias --json emit a versioned envelope for every command. Consumers should read schema_version before interpreting the rest of the document.

{
  "schema_version": "2",
  "peta_version": "<installed version>",
  "generated_at": "2026-09-04T12:00:00Z",
  "query": {
    "command": "info",
    "arguments": {"package": "requests"},
    "target_environment": {
      "implementation": "CPython",
      "python_version": "3.14.0",
      "platform": "darwin"
    }
  },
  "status": "success",
  "sources": [],
  "warnings": [],
  "errors": [],
  "result": {}
}

Status and source states#

The envelope status is one of:

  • success: the query completed with a result;

  • partial: the primary result is usable, but optional enrichment failed or a dependency tree is incomplete because of a conflict, depth limit, or transitive resolution failure;

  • empty: the query succeeded and returned no items; or

  • failed: the command failed and result is null.

Each source record has its own state: success, empty, skipped, unavailable, unsupported, or failed. This distinguishes a source that returned no data from one that was disabled, could not be configured, does not support the query, or failed during retrieval. Successful retrievals include the time captured when the source returned in retrieved_at. The fields array links a source to JSON paths in result; records may also include a query target or failure reason.

A record that completed a retrieval also carries freshness, saying where the data came from: live means the source answered, cached means peta served a stored response without contacting the source, and revalidated means the source confirmed a stored response was still current without resending it. The field is absent when the question does not apply — a package read from the installed environment, or a source that never completed a lookup. Freshness describes provenance, not quality: a cached record is a real answer, and --refresh forces a live one.

Warnings and errors contain stable code and message fields, plus source when a specific provider is responsible. When two providers answer the same result field with different values, the first one consulted wins and the disagreement is reported as a provider_conflict warning naming both; every consulted provider still appears in sources, so neither side is lost. Under --offline peta contacts no source. A fatal lookup that the cache cannot answer produces a failed envelope with an offline_unavailable error and exit code 2, naming the URL it could not answer; this is deliberately distinct from network_error, because nothing went wrong and the fix is to warm the cache or drop --offline rather than to check connectivity. An optional source that the cache cannot answer stays non-fatal, and an entry past its TTL is still served offline rather than withheld — reported as cached, so the staleness is visible. Optional enrichment failures produce a partial envelope and exit code 0. Fatal errors produce a failed envelope and retain the documented nonzero CLI exit code. When command-line validation fails before a command handler runs, query.arguments.argv preserves the unparsed argument vector in the same failed envelope.

Dependency-tree lookups preserve empty or failed transitive resolutions on the affected node. The envelope is partial, its source record identifies the affected result path, and a dependency_resolution_failed warning explains the failure; the successfully resolved portion of the tree remains available. For deps --why, a failure on a branch that no returned path covers is still reported, with an empty fields array because no result path identifies it.

artifacts represents a release’s files as structured data under result.files, each with its digest, size, upload time, wheel tags, and a provenance object. compatible is nullable: null means peta could not read the evidence, which is a different answer from false. provenance.available reports whether the index exposes a PEP 740 document, and provenance.publishers carries the Trusted Publisher identities PyPI supplied — both are reports of published evidence, never verification results, and their absence is not a failure. It is an array because PEP 740 permits one attestation bundle per publisher, and each entry names its kind plus a claims object carrying that kind’s own fields verbatim, since each publisher kind describes itself differently. summary.total_size sums the sizes the index reported, so summary.unsized_files says how many files contributed none — any value above zero makes the total a lower bound. With --provenance, pypi-provenance source records name the exact result.files[i].provenance.publishers paths the lookup reached; completed lookups and failed ones are separate records, because one state cannot describe both and a consumer must be able to tell a path PyPI supplied nothing for from a path peta could not reach. A reached path is listed whether or not PyPI supplied a publisher for it, and survives a sibling file’s failure. Where the per-file retrievals mix live and cached answers, the record reports the stalest of them, so it never describes any part of the evidence as fresher than it is. A release whose files expose no provenance at all is recorded as skipped with no retrieved_at, since nothing was requested. A failure also warns and makes the envelope partial rather than discarding the artifact listing.

origin describes one locally installed distribution. result.origin is the structured direct_url.json record: kind is index (no direct_url.json), vcs, archive, directory, or unknown (present but unreadable or incomplete – a vcs_info without vcs and commit_id – or containing conflicting source descriptors, with a reason), alongside url, editable, vcs, requested_revision, commit_id, archive_hashes, and subdirectory, which is null unless it is the relative path PEP 610 requires, without an anchor or .. components. Credentials are removed from url. result.installer and result.requested report the INSTALLER and REQUESTED markers; requested is false both when the marker is absent and when the installer never writes one. result.import_packages and result.entry_points list what the distribution provides. result.files has one record per listed file with its path, state, on-disk size, recorded_size, and recorded_hash (algorithm=digest); state is one of verified, mismatch, missing, not_recorded, unverifiable, unchecked, or out_of_bounds, and size is null for a file that was not found or lies out of bounds. result.integrity names the record_source (RECORD, installed-files.txt, or null when there is no readable listing), whether hashes_verified ran (--verify), the file_count, the total_size found on disk, and a count per state. hashes_verified records that --verify was given, not that anything matched: read states.verified for that. File states are findings, not failures: a mismatched file never makes the envelope partial, and a distribution with no file listing is still success, since its origin and installer are a result on their own.

compare carries both packages under result.packages and their semantic diff under result.diff. result.diff.changes lists only what changed, in a fixed group order; each record has a group (release, python, dependencies, extras, license, vulnerabilities, artifacts, or provenance), a stable kind such as dependency_specifier_changed or vulnerability_added, a subject naming what changed (a canonical dependency name, advisory id, artifact slot, or field), before/after values — null on the side where the item is absent — and an expected boolean. expected is true for a difference that follows from what was compared rather than from drift, such as new digests on a new release’s differently named files; the record is kept so nothing is hidden, and human output summarizes it instead of listing it. Values are canonicalized first, so a formatting difference is never a change. result.diff.unknown lists the groups that could not be compared, each with a reason: a side with no evidence is reported there rather than diffed against an empty value. With --artifacts, each side’s file listing is a pypi source record for result.diff; a failed listing warns with enrichment_failed and makes the envelope partial. --changes-only affects human output only.

Source names identify the provider, not the lookup strategy: packages read from the installed environment are local and packages read from PyPI are pypi, matching the names used by versions and by network failures. The legacy remote value survives only in result.source.

Compatibility policy#

Schema version 1 replaces the original unversioned command-specific JSON. The previous top-level payload is now under result; for example, migrate output["name"] to output["result"]["name"].

Schema version 2 updates dependency nodes for declared-metadata resolution. Consumers should migrate installed_version to selected_version and replace the circular boolean check with the state field, whose "circular" value represents the former true case and whose other values describe additional resolution outcomes.

Within a schema version, consumers must tolerate new object fields and new warning/error codes. Existing fields will not be removed or change meaning. A backward-incompatible shape or semantic change increments schema_version. The installed application version is reported separately as peta_version and does not imply a schema change.