Skip to content

Architecture

Schepherd has two independent halves that share one wire format.

flowchart LR
  subgraph maintainer["Maintainer side (schepherd-publisher, weekly job)"]
    U["upstream catalog\n(one commit)"] --> P["prepare:\nreuse unchanged,\nfetch, bundle, verify"]
    ST[("catalog/state.json\nin Git")] --> P
    P --> PK["package:\nonly new and changed content"]
    ST --> PK
    PK --> PUB["publish:\nschemas, then catalog index,\nthen the new state"]
    PUB --> ST
  end
  PUB --> REG[("OCI repository\none artifact per schema\n+ catalog index over them")]
  REG -- "schepherd mirror" --> MIR[("private mirror\nsame catalog digest")]
  subgraph client["Client side (schepherd)"]
    CFG["pinned repository\n+ catalog index digest"] --> CAT["verified catalog"]
    CAT --> SCH["requested schema only"]
    SCH --> FILE["schema.json in the cache"]
    FILE --> RUN["optional external consumer"]
    LOC["local [schemas] file,\nused in place"] --> RUN
  end
  REG --> CAT
  MIR --> CAT

Invariant

Schepherd delivers a chosen JSON Schema from a pinned OCI catalog to a verified local file. Every schema is its own artifact. The catalog moves between registries without changing its digest. What happens with the file afterwards is decided by the user's consumer, not by Schepherd. The catalog is an OCI image index that references every schema artifact, so the whole snapshot is one OCI graph that registries and generic tools follow. Schemas of the project itself can be declared as local schemas: they are checked and used in place, next to the catalog or without one, and never touch a registry or the cache.

What Schepherd does not do

  • It does not validate documents and contains no JSON Schema engine.
  • It does not parse your JSON, YAML, TOML, JSONC or JSON5 files.
  • It never contacts SchemaStore or any upstream schema URL at runtime.
  • It never resolves a mutable tag implicitly.
  • It runs no daemon, telemetry, update check or background cache cleanup.
  • It never executes anything that came from a registry.

Packages

Package Responsibility
cmd/schepherd Client entry point.
cmd/schepherd-publisher Maintainer entry point (not shipped to users).
internal/cli Cobra commands, flag/env/config precedence, output discipline.
internal/config TOML loading, local extends, validation, precedence, local [schemas] and the checks of their files.
internal/interp ${ENV} and {placeholder} templates, single-pass expansion.
internal/match fileMatch dialect and resolution precedence.
internal/catalog Catalog document model, strict parser, canonical encoder.
internal/artifact OCI wire contract: deterministic packing, strict manifest and index parsing, index/catalog.json cross-check.
internal/registry ORAS-based transport: TLS, plain-HTTP opt-in, credentials, verified fetch.
internal/cache Content-addressed cache, atomic materialization, inter-process locks.
internal/store Catalog and schema retrieval on top of cache and registry; strict offline. FetchCatalog verifies a catalog snapshot without the cache, shared by pin, mirror and the publisher.
internal/mirror Full-snapshot, byte-preserving mirroring of the catalog's OCI graph with verification.
internal/runner Planning and executing consumers (batch, per-file, stdin).
internal/calver Catalog revisions: YYYYMMDD.HHMM (the UTC minute of a publication) and their catalog-<revision> tags.
internal/buildinfo, internal/buildinfo/semver Build information of the binaries; SemVer handling of client release versions.
internal/digest, internal/jsonutil, internal/fault, internal/env Shared primitives.
internal/publisher/... Upstream import, SSRF-safe fetching, IDs, license policy, bundling, reuse of unchanged schemas, packaging and publishing.
internal/publisher/state catalog/state.json, the Git record of what was published (load, save, validation; api/state.schema.json).
internal/publisher/publish The registry-free plan shared by diff and publish (artifact reuse by content digest, metadata-only changes, held and excluded schemas, the check that a prepared set matches its state), time-based revision allocation and the publication order.
internal/publisher/licensedetect Automatic license detection for sources no rule covers (GitHub REST API, npm registry and CDNs).
internal/testutil/..., internal/cache/linktest Test-only helpers (in-memory OCI registry, client smoke scenario, junction and symlink setup).
tools/release SemVer release-tag checks (version, with the PyPI and RubyGems forms and the previous tag), the npm packages, PyPI wheels and RubyGems gem built with npm pack, uv build and gem build and their publication plans (internal/wrappers), license texts and notices, the workflow policy test.
tools/catalogbot The Git side of the weekly catalog update: signed state commit, catalog-<revision> tag and release, release notes, job summary, status of the recorded revision, source commit rewrite.

The client binary links none of the publisher packages, none of the test-only helpers (internal/testutil/..., internal/cache/linktest) and no JSON Schema library. go list -deps ./cmd/schepherd is checked in CI (and by task lint:client-deps) to keep it that way.

The client and the schema catalog are released independently: client releases are SemVer tags vX.Y.Z, catalog revisions are the UTC minute of their publication with tags catalog-YYYYMMDD.HHMM (see Versioning). The weekly catalog update is described in Catalog automation.

Data flow of the weekly catalog update

prepare reads the upstream snapshot and catalog/state.json. Published schemas whose inputs did not change are reused with their recorded artifact; the other records are fetched, bundled, verified and license-checked, and a published schema that cannot be refreshed is held. publish uploads new artifacts only, then the catalog index over them, and writes the next state. See Publishing.

Data flow of schepherd path <id>

sequenceDiagram
  participant CLI as schepherd
  participant Cache as cache (local)
  participant Reg as registry
  CLI->>Cache: catalog index, metadata manifest, catalog.json by pinned digest
  alt cached and verified
    Cache-->>CLI: catalog
  else missing (online only)
    CLI->>Reg: GET index by digest, GET metadata manifest, GET catalog blob
    Reg-->>CLI: bytes (digest and size verified by Schepherd)
    CLI->>CLI: catalog.json lists exactly the index's schema children
    CLI->>Cache: atomic write
  end
  CLI->>Cache: schema manifest + materialized schema.json
  alt cached and verified
    Cache-->>CLI: absolute path
  else missing
    CLI->>Reg: GET schema manifest, GET payload blob
    CLI->>Cache: verify, decompress within limits, atomic rename
  end
  CLI-->>CLI: print path

A warm cache performs no network request of any kind: no manifest HEAD, no token request, no credential helper.

Trust model

  • The pinned catalog index digest is the root of trust. Everything else is verified by digest and size before use: the metadata manifest, catalog.json, schema manifests, payload blobs, decompressed content.
  • A digest proves integrity relative to that pin, not who published it. It is not a signature. Deliver the pin through a channel you trust (a reviewed config file).
  • The runner configuration is a local, trusted, executable contract that is loaded only from an explicit path. Registries cannot contribute commands, arguments, environment variables or configuration.
  • Credentials stay with the host they are configured for; they are not sent across redirects to other hosts, and two registry entries (a host or a repository prefix) never share credentials or tokens.

See Security for details.