Skip to main content
Glama
Judgment-Pack

judgment-pack-runtime

Official
README.md
# judgment-pack

> **Status: pre-1.0**
>
> Tagged binaries are official only when they appear on this repository's GitHub Releases page
> with `checksums.txt`. Development checkouts may temporarily embed unreleased JPS snapshots, but
> the release workflow rejects them.

`judgment-pack` is the vendor-neutral **reference runtime** for the Judgment Pack Specification
(JPS). It is a reference implementation, not the only valid one: the normative specification,
schemas, and conformance corpus are owned by the separate
[`judgment-pack-spec`](https://github.com/Judgment-Pack/judgment-pack-spec) repository, and an
independent implementation is judged against the complete normative requirements of the conformance
class it claims — not against this runtime. Results from the corpus published for the exact version
it names are required evidence for such a claim and are not exhaustive evidence of it (§3.4.1).

The runtime **validates documents**. It does not fetch a source, authorize an action, or establish
truth, organizational authority, safety, or operational fitness. It bundles two specification
versions, `0.1.0-draft` and `0.2.0-draft`, and validates a document against the exact version that
document declares; `0.1.0-draft` defines carrier, structural, and semantic document conformance
only, and `0.2.0-draft` changes no part of the document format.

One shared evaluator applies the specification's §§7–8 resolution model per
[ADR-0007](docs/adr/0007-experimental-evaluator.md), and ten surfaces reach it: `jpack experimental
evaluate` and `evaluate-corpus`, the project walk `jpack packs test`, the graph verbs
`jpack experimental graph evaluate` and `graph test`, the version comparison
`jpack experimental compare`, and the `experimental_evaluate`,
`experimental_test_packs`, `experimental_test_cases` and `experimental_test_graphs` MCP tools. The `experimental` namespace is a **stability**
statement: such a surface may change or be removed without compatibility promise. It is not a statement
about conformance. That evaluator implements the evaluator conformance class Core `0.2.0-draft` adds —
the §8.2 input preflight, the §8.3 portable disposition with its RFC 8785 byte agreement, the §8.4
error classes and their fixed precedence, and the §10 limits — per
[ADR-0010](docs/adr/0010-evaluator-aligned-to-core-0.2.0-draft.md) and
[ADR-0011](docs/adr/0011-first-evaluator-conformance-claim.md). Only a pack declaring `specVersion`
`0.2.0-draft` is evaluated: §11 makes the declared value exact and requires an unedited `0.1.0-draft`
pack to be re-declared — one edit, the `specVersion` string, and nothing else in the document — before
an implementation claiming this draft evaluates it, so any other version is refused as
`pack-not-conformant` in the `preflight` phase.

**This runtime's conformance claim is stated, in full and only, in
[CONFORMANCE.md](CONFORMANCE.md).** JPS §3.4.1 fixes the entire form such a claim may take — the class,
one exact `specVersion`, the corpus version, the results obtained, and in the claim's own words that
every row of that corpus version passed — so this README states no part of it and neither does any
other surface: a partial restatement would be the partial claim §3.4.1 forbids. Read that file for the
claim, its version scope, its evidence, and everything it does not assert, which includes anything at
all about a pack, its facts, or the wisdom of acting on a disposition (§3.5).
`jpack experimental evaluate-corpus` runs the bundled evaluation corpus and reports its rows —
the evidence §3.4.1 requires of a claim of this class, and explicitly not exhaustive evidence of one;
see [CONFORMANCE.md](CONFORMANCE.md).

That command carries one further opt-in, `--rfc0008-quantifiers`, which is a **draft-RFC
prototype** per [ADR-0009](docs/adr/0009-draft-rfc-quantifier-prototype.md): it admits the
collection quantifiers `exists`, `every`, and `uniform` proposed by the specification's RFC 0008
(Draft). Those operators belong to no published JPS version. A pack using one is **not valid** under
JPS `0.1.0-draft`, `spec validate` rejects it, the evaluator without the flag refuses it, and every
successful evaluation payload produced under the flag says so in band through a `draftPrototype`
member — a refusal is an operational error and carries none. The flag is CLI only; the MCP tool does
not expose it.

A second opt-in, `--rfc0016-outcome-values`, is a **draft-RFC prototype** on the same terms, per
[ADR-0039](docs/adr/0039-draft-rfc-outcome-values-prototype.md). It admits the value declarations
proposed by the specification's RFC 0016 (Draft): an outcome declares named values under
`org.judgmentpack.outcome-values`, each a constant or a copy of one fact, and the disposition of that
outcome carries them in a `value` member. That member is the draft's and no member of §8.3. A
declared value that does not resolve withholds the outcome, and the result is `unresolved` with
reason `unknown`. The name is reserved, so a pack carrying a declaration is **not valid** under any
published JPS version, `spec validate` rejects it, and the evaluator without the flag refuses it.
A value drawn from a fact is a copy of that fact and is not verified. The two flags are mutually
exclusive, and this one is CLI only too.

The command binary is `jpack`. The project, repository, and release archives keep the
`judgment-pack` name; the executable they carry is `jpack`.

## Implemented commands

```text
jpack version
jpack artifact version --base <x.y.z> [--candidate <x.y.z>]
jpack dialect contract
jpack dialect init [--dialect-version 2|3]
jpack dialect canonicalize <dialect-or->
jpack dialect digest <dialect-or->
jpack dialect validate <dialect-or->
jpack dialect schema [--dialect-version 2|3]
jpack dialect reference
jpack dialect check <dialect-or-> --pack <pack-or->
jpack spec validate <pack-or->
jpack spec test-conformance [suite] [--strict]
jpack spec schema <spec-version>
jpack spec examples [name] [--spec-version V]
jpack packs list        (jpack.json project convention; ADR-0012, not part of the spec)
jpack packs validate [--id X]
jpack packs test [--id X] [--require-matrix]   (EXPERIMENTAL SURFACE; claim: CONFORMANCE.md)
jpack packs suggest [--id X] [--base ROW] [--write F|-] [--max N] [--include-hugs]   (candidate row INPUTS, never rows; ADR-0024)
jpack packs lock        (declare the current documents as the project's reviewed set; ADR-0019)
jpack packs verify      (check the project against that reviewed set)
jpack packs lint [--producers M]   (every consulted pointer has a producer; ADR-0022)
jpack packs schema
jpack mcp
jpack experimental evaluate <pack-or->   (EXPERIMENTAL SURFACE; claim: CONFORMANCE.md)
jpack experimental evaluate --pack-id X   (EXPERIMENTAL SURFACE; resolves one decision id through jpack.json)
jpack experimental evaluate <pack-or-> --rfc0008-quantifiers   (DRAFT-RFC PROTOTYPE; not an input the class defines)
jpack experimental evaluate <pack-or-> --rfc0016-outcome-values   (DRAFT-RFC PROTOTYPE; a pack that declares values is not an input the class defines)
jpack experimental evaluate-corpus   (EXPERIMENTAL SURFACE; corpus results, the evidence §3.4.1 requires)
jpack experimental compare <old-pack> <new-pack> --inputs <file>   (EXPERIMENTAL SURFACE; the inputs two versions decide differently; ADR-0045)
jpack experimental graph list   (EXPERIMENTAL: the configured graphs, resolved; ADR-0029)
jpack experimental graph validate <graph-or->   (EXPERIMENTAL composition prototype; spec RFC 0002, Draft; ADR-0015)
jpack experimental graph evaluate <graph-or-> [--inputs <file-or->] [--rehearsal]   (EXPERIMENTAL SURFACE; claim: CONFORMANCE.md)
jpack experimental graph explain <graph-or->   (the evaluation plan; nothing is evaluated)
jpack experimental graph test <graph-or-> --rows <file-or->   (EXPERIMENTAL SURFACE; claim: CONFORMANCE.md)
jpack experimental graph schema
jpack experimental policy contract
jpack experimental policy init --kind policy --format-version 0.0.2 --id <id> --version <artifact-version>
jpack experimental policy schema --kind policy --format-version 0.0.2
jpack experimental policy canonicalize <document-or->
jpack experimental policy digest <document-or->
jpack experimental policy validate <document-or->
jpack experimental policy check <document-or-> --materials <materials-or->
```

The pack argument and `--pack-id` are mutually exclusive: one pack, one source, and supplying both
or neither is an invocation error rather than a precedence rule. `--pack-id` honors `--config` and
`JPACK_CONFIG` like every other command that reads a configuration.

The namespace is `jpack spec`, not `jpack jps`. JPS remains the name of the
specification and the prefix of its provisional diagnostic codes.

The `mcp` command serves the same offline operations to a Model Context Protocol client over stdio,
so an agent can validate documents as a tool call; see
[docs/mcp-clients.md](docs/mcp-clients.md) for per-client setup and
[docs/agent-testing.md](docs/agent-testing.md) for the agent-driven testing protocol.

## Install a tagged release

Download the archive for your operating system and architecture from
[GitHub Releases](https://github.com/Judgment-Pack/judgment-pack-runtime/releases). Each archive
includes the `jpack` binary, README, the `CONFORMANCE.md` claim the source it was
built from carries, Apache-2.0 license, attribution notice, and third-party notices.

Asset names follow this pattern:

```text
judgment-pack_<version>_<os>_<arch>.tar.gz
judgment-pack_<version>_windows_<arch>.zip
```

For example, release `v0.1.0` uses `judgment-pack_0.1.0_linux_amd64.tar.gz`. Linux and macOS users
can extract an archive and install the binary into a user-owned directory already on `PATH`:

```bash
tar -xzf judgment-pack_0.1.0_linux_amd64.tar.gz
install -m 0755 jpack "$HOME/.local/bin/jpack"
jpack version
```

On Windows, expand the `.zip`, move `jpack.exe` into a directory on your user `PATH`, and
run:

```powershell
jpack version
```

Verify the archive before extracting it. On Linux:

```bash
grep ' judgment-pack_0.1.0_linux_amd64.tar.gz$' checksums.txt | sha256sum --check
```

On macOS:

```bash
grep ' judgment-pack_0.1.0_darwin_arm64.tar.gz$' checksums.txt | shasum -a 256 --check
```

On Windows PowerShell:

```powershell
(Get-FileHash .\judgment-pack_0.1.0_windows_amd64.zip -Algorithm SHA256).Hash
Select-String -Path .\checksums.txt -Pattern ' judgment-pack_0.1.0_windows_amd64.zip$'
```

The two Windows hashes must match. Release archives also carry GitHub build-provenance
attestations, which a current GitHub CLI can verify:

```bash
gh attestation verify judgment-pack_0.1.0_linux_amd64.tar.gz --repo Judgment-Pack/judgment-pack-runtime
```

Packages for Homebrew, Scoop, `apt`, and `go install` are not published yet. In particular, source
installation with `go install` does not receive the release linker metadata, so use a release
archive when you need an accurately reported runtime version.

## Evaluate one decision

With `jpack` on your `PATH`, this is the whole loop: a pack, a facts document, and a disposition.
Save a pack that approves an expense unless its amount is over 5000:

```bash
cat > pack.json <<'EOF'
{
  "specVersion": "0.2.0-draft",
  "id": "https://example.invalid/judgment-packs/expense-approval",
  "version": "0.1.0",
  "title": "Expense approval",
  "decision": {
    "intent": "Approve ordinary expenses without review.",
    "question": "May this expense be approved?"
  },
  "outcomes": [
    { "id": "approve", "label": "Approve" },
    { "id": "decline", "label": "Decline" }
  ],
  "rules": [
    {
      "id": "over-limit",
      "description": "An amount over 5000 is declined.",
      "when": { "op": "fact", "path": "/expense/amount", "operator": "greater-than", "value": "5000" },
      "outcome": "decline",
      "onUnknown": "escalate"
    }
  ],
  "fallbackOutcome": "approve",
  "escalation": {
    "triggers": ["unknown"],
    "target": { "kind": "human-role", "name": "Finance reviewer" }
  }
}
EOF
jpack spec validate pack.json
```

Give it the facts of one expense and evaluate. The amount is a decimal string, as the
specification requires for a number a rule compares:

```console
$ echo '{"expense": {"amount": "120"}}' > facts.json
$ jpack experimental evaluate pack.json --facts facts.json
EXPERIMENTAL SURFACE evaluation (claim and scope: CONFORMANCE.md; this result authorizes nothing)
disposition: outcome approve
trace: rule over-limit: false
artifacts: immutable-git-ref · sha256 081cf18af9fe667a5da5acab465f4cf6118a00742bb6e5527d4d45fc09f25185
```

Now remove the amount. The rule cannot be decided, its `onUnknown` is `escalate`, and the pack
names who takes the case, so the answer is a handoff rather than a guess. The trace line names the
fact that was missing:

```console
$ echo '{"expense": {}}' > facts.json
$ jpack experimental evaluate pack.json --facts facts.json
EXPERIMENTAL SURFACE evaluation (claim and scope: CONFORMANCE.md; this result authorizes nothing)
disposition: unresolved (unknown)
handoff: requested -> human-role "Finance reviewer" (triggered by unknown)
trace: rule over-limit: unknown onUnknown=escalate [fact /expense/amount absent]
artifacts: immutable-git-ref · sha256 081cf18af9fe667a5da5acab465f4cf6118a00742bb6e5527d4d45fc09f25185
```

Releases up to 0.23.1 print the last trace line without the bracket naming the missing fact.
Both runs exit 0: producing a disposition is success, whichever it is. `--format json` gives the
same result as a payload, with the disposition in its canonical form. A disposition is data. It
authorizes nothing, and acting on it is the caller's decision.

From here, [docs/building-with-packs.md](docs/building-with-packs.md) covers a project of several
packs, their test matrices, and the audit trail; `jpack spec examples --spec-version 0.2.0-draft`
lists larger starting points the evaluator accepts, from 0.24.0.

## Build and try it locally

Go 1.24 or newer is required to build from source: `internal/fssecure` binds every project file
read to a directory handle through `os.Root`, which is a Go 1.24 standard-library type.

If an older WSL setup has persisted `GO111MODULE=off`, clear it once with
`go env -u GO111MODULE`. The commands below explicitly enable module mode as a compatibility
measure.

```bash
env GO111MODULE=on CGO_ENABLED=0 go build -trimpath -o ./bin/jpack ./cmd/jpack
./bin/jpack --help
./bin/jpack version
```

From a sibling checkout of `judgment-pack-spec`, validate a synthetic example:

```bash
./bin/jpack spec validate ../judgment-pack-spec/examples/minimal-expense-approval.json
./bin/jpack spec validate --format json ../judgment-pack-spec/examples/minimal-expense-approval.json
```

Standard input is accepted explicitly with `-`:

```bash
./bin/jpack spec validate --format json - < pack.json
```

Run a bundled document-conformance corpus — `0.1.0-draft` by default, or an exact bundled version:

```bash
./bin/jpack spec test-conformance
./bin/jpack spec test-conformance --format json
./bin/jpack spec test-conformance --spec-version 0.2.0-draft
./bin/jpack spec test-conformance --strict
```

By default a case passes when its expected diagnostic is among those reported; `--strict` also fails a case that reports a diagnostic its manifest entry does not expect, naming each one, which is what the corpus's single-defect rule needs.

Run the bundled **evaluation** corpus of JPS `0.2.0-draft` through the experimental evaluator. Every
row is compared by disposition equality as §8.3 defines it — both the row's expectation and the
produced disposition go through the same RFC 8785 canonicalizer, so a set's stored order is not a
difference — or by its expected §8.4 error class and phase.
This reports results: the required, non-exhaustive evidence for the claim in
[CONFORMANCE.md](CONFORMANCE.md), and not that claim. A mismatching row decides nothing by itself,
because §3.4 makes a divergence as likely to be a defect in the row as in this implementation, and only
a project-issued erratum can excuse a row:

```bash
./bin/jpack experimental evaluate-corpus
./bin/jpack experimental evaluate-corpus --format json
./bin/jpack experimental evaluate-corpus --spec-version 0.2.0-draft
```

Inspect or copy the bundled schema without network access:

```bash
./bin/jpack spec schema 0.1.0-draft
./bin/jpack spec schema 0.2.0-draft
./bin/jpack spec schema 0.1.0-draft --write schema.json
./bin/jpack spec schema 0.1.0-draft --write -
```

The schema command refuses to overwrite an existing file. It is a different write from the audit
trail below, which appends to a file the project's own configuration named and never replaces one.

## Validation behavior

Validation short-circuits in this order:

1. strict UTF-8 JSON carrier parsing, including duplicate-member rejection;
2. exact `specVersion` dispatch;
3. Draft 2020-12 structural validation with URI, date, and date-time assertions;
4. normative semantic reference and extension-declaration checks; and
5. required-extension capability negotiation.

An unknown but syntactically usable `specVersion` is `unsupported`, not `invalid`. The conformance
runner deliberately pins each case to the suite's declared specification version, so a fixture that
violates that pinned schema remains a structural negative case.

`--through carrier` and `--through structural` produce explicitly partial results. They never print
an unqualified “valid document” message.

The public MVP supports no JPS extensions. A structurally and semantically conforming document that
requires an extension is therefore reported as `unsupported`. Extension code is never discovered,
downloaded, installed, or executed during validation.

## Experimental evaluation behavior

The evaluator implements the evaluator conformance class of JPS Core `0.2.0-draft`. The conformance
claim for it is stated, in full and only, in [CONFORMANCE.md](CONFORMANCE.md) — with its evidence, its
version scope, and everything it does not assert — and this section states no part of it: this section
is the behavior.

**Only a pack declaring `specVersion` `0.2.0-draft` is evaluated.** The §8.2 preflight, the §8.3
disposition shape, and the §8.4 error classes are that version's, and §11 makes a declared version
exact: "an unedited `0.1.0-draft` pack is not structurally conforming to `0.2.0-draft` and must be
re-declared before an implementation claiming this draft evaluates it." So a pack declaring any other
version — `0.1.0-draft` included — is refused as `pack-not-conformant` in the `preflight` phase
(`JPS-EVALUATION-PACK-SPEC-VERSION`), and the message cites that rule and states the remedy: **one
edit, the `specVersion` string**, and nothing else in the document, since §11 says `0.2.0-draft` changes
no part of the document format. There is no second, unclaimed legacy path. Document validation is
untouched — `spec validate` still validates a `0.1.0-draft` pack against its own published schema, and
document conformance needs no evaluator (§3.4).

Every evaluation payload still names both versions — `specVersion` is the pack's own, and
`evaluatorSpecVersion` is the contract's — because they are two different facts and a consumer should
read the applied contract rather than infer it. A refusal names the contract too, on
`evaluationError.evaluatorSpecVersion`. Payloads also carry a `conformanceClaimReference` member whose
value is `CONFORMANCE.md`: a locator, not a claim.

**Inputs are admitted before anything is resolved (§8.2).** They are validated in one order — the
pack, then the facts document, then the evidence-availability document, then the pack's
`metadata.requiredExtensions` against the caller's `--supported-extension` set — and that validation
finishes before §8 step 1 runs. An omitted `--evidence` document is the implicit empty object, which
makes every declared requirement `unknown`; that is the only form its absence takes, and it is not an
error. An evidence input that is not a JSON object, names an undeclared requirement, or carries a
value outside `present`, `absent`, and `unknown` is refused.

**A produced result is the portable disposition (§8.3).** Under `--format json` without `--pretty`
the `disposition` member is written in its RFC 8785 canonical form: members ordered by name, both sets
sorted and duplicate-free, absent members omitted rather than serialized as `null`, and no whitespace.
`--pretty` indents the whole payload and that indentation reaches inside this member too, so the member
order and both sets stay canonical but those exact bytes are not present. §8.3 requires
canonicalization "where a byte comparison is required", so a byte comparison against another
implementation must recanonicalize either side it did not itself produce; under `--pretty` it must.
The pack's configured escalation target is reported beside the disposition, in `handoffTarget`, never
inside it. Human output is unchanged prose.

**A refusal is an evaluation error, and never a disposition (§8.4).** Every evaluation this runtime
refuses reports exactly one class in band — `evaluationError.class`, with `evaluationError.phase` and
`evaluationError.evaluatorSpecVersion` — and no disposition at all, not even a partial one. That
includes every §8.2 preflight condition on every surface: a document above the byte limit and an empty
supplied evidence document are classed and ordered like any other, not refused ahead of the preflight.
The finer `JPS-*` code stays beside the class as its detail. An invocation that never became an
evaluation — a missing flag, an input this runtime could not read as a bounded regular file — is an
ordinary operational error and carries no class, because §8.4 classes evaluation conditions and leaves
transport undefined. The four classes are evaluated in Core's fixed order, which is the preflight order
above:

| Class | Reached when |
| --- | --- |
| `pack-not-conformant` | the pack input is not a semantically conforming document, at any layer |
| `malformed-input` | an input failed the preflight: unusable JSON, a non-object or invalid evidence document, an undeclared evidence key, or a document limit reached while admitting an input |
| `unsupported-required-extension` | the pack requires a capability this caller does not support |
| `resource-exhaustion` | a documented §10 limit was reached while evaluating an admitted input: this runtime's evaluation-work limit, on the ordinary Core path as well as under `--rfc0008-quantifiers` |

The phase split is the one §10 draws: a limit reached while *admitting* an input is
`malformed-input`, because the input was refused rather than partly processed, and
`resource-exhaustion` is reserved for a limit reached while *evaluating* an input already admitted.
This runtime's admission limits are the carrier limits listed under [security
defaults](#security-defaults). The 250,000-node cap is one of them, and its §10 category is stated
rather than left implicit: it is a budget over the whole parsed document, so it is a document-size
limit like bytes and depth, reached while admitting an input, and it is reported as
`malformed-input` in the `preflight` phase. It is *also* this evaluator's collection-size bound, for the
reason the next section gives: a collection of n members is n+1 parsed nodes, so a document-size budget
over nodes bounds every collection inside it.

### The two §10 limits of the claimed class

§10 requires an implementation claiming this class to define and document at least its collection-size
and evaluation-work limits, and makes reaching one of them during an evaluation `resource-exhaustion`
rather than a disposition. Both are defined here and enforced in
[`internal/evaluation/limits.go`](internal/evaluation/limits.go):

- **Evaluation-work limit: 20,971,520 work units per evaluation.** A unit is one visited condition
  node, one §8 iteration over an authored evidence requirement, exception, or rule, one step of a
  pointer resolution, or one byte of a path, an object member name, or a scalar token that a comparison
  has to read. The charge for each condition tree is complete before any predicate in it runs and §8's
  own iteration is charged before step 1, so the total does not depend on evaluation order and an
  exhausted limit never truncates a disposition — it produces `resource-exhaustion` in the `evaluation`
  phase with no disposition at all.

  The number is **derived in code** from the carrier's byte cap rather than chosen: it is exactly twice
  10 MiB, the per-document byte cap every admitted input passes, and `limits.go` computes it from that
  constant so the two cannot drift. That ratio is an **arithmetic fact about two numbers**, and nothing
  more: **it gives no guarantee about any whole evaluation.** Three documents may be admitted rather
  than two — the pack, the facts document, and an optional evidence-availability document — each under
  the same cap; and a unit is charged per *use* rather than once per admitted byte, since the bytes of a
  pointer and of a selected value are charged again every time a condition reads them, with §8's fixed
  per-node and per-iteration charges on top. An earlier version of this section inferred from the ratio
  that "one full read of every admitted byte always fits" and that a single maximal cross-document
  comparison therefore sits exactly at the boundary; **both are withdrawn** — the premise bounds nothing
  under that charge model, and neither was exercised against an admitted input through the accounting
  path (ADR-0011 records the correction). Amplification is what the limit refuses in practice — the same
  large selected value re-read once per `in` candidate or once per condition — and this runtime does not
  claim that only amplification is refused. Against a 100 KB facts document the limit still admits about
  two hundred whole-document comparisons, and every row of the bundled evaluation corpus charges under
  1,000 units: measurements of those inputs, not bounds on inputs no row contains.
  Callers may configure a lower limit per evaluation; the draft RFC 0008 prototype has its own,
  smaller budget of 100,000 units (ADR-0009), and the draft RFC 0016 prototype is charged to this
  limit (ADR-0039).
- **Collection-size limit: 250,000 members** — the 250,000-node carrier cap above, stated as the §10
  limit it is. Every input is admitted under that cap, so no admitted document holds a larger
  collection, and every collection this evaluator traverses comes from an admitted document: Core
  constructs none of its own and has no operator that iterates one. Because the bound is enforced while
  *admitting* an input, reaching it is `malformed-input` in the `preflight` phase — §2.1 refuses such a
  document whole rather than processing part of it — which is stricter than an evaluation-phase check of
  the same bound, not weaker. That is why this runtime documents the determination instead of adding a
  second mechanism that could only report what the preflight already refuses.

Limits are not portable: two implementations may set different ones, and an input above either is
outside the portable claim (§10). The evaluation corpus therefore keeps every case well inside any
plausible limit rather than probing one.

## The `jpack.json` project convention

A project that owns several packs needs a name for each one that works the same from a shell, from
CI, and from an agent's tool call. `jpack.json` is that index — and it is a **convention of this
runtime, not part of the Judgment Pack Specification** ([ADR-0012](docs/adr/0012-jpack-project-convention.md)).
No other implementation is obliged to understand it, and a project that never writes one loses
nothing: every command still takes a pack by path, and every MCP tool still takes one as text.

```json
{
  "configVersion": "1",
  "packs": {
    "expense-approval": {
      "path": "packs/expense-approval-1.2.0.pack.json",
      "matrix": "packs/expense-approval.matrix.json",
      "description": "May this expense be reimbursed without a manager's sign-off?",
      "expectedVersion": "1.2.0",
      "facts": { "/expense/amountUsd": { "source": "Snowflake FINANCE.EXPENSES", "hint": "amount_usd as a decimal string" } },
      "evidence": { "itemised-receipt": { "source": "SharePoint /Finance/Receipts" } }
    }
  }
}
```

The file is selected by `--config`, then `JPACK_CONFIG`, then `./jpack.json`. Its schema is closed
and printable with `jpack packs schema`: every member it does not name is rejected.
`configVersion` is a single integer as a string, on the `outputVersion` precedent rather than
semantic versioning; `"1"` is the shape without graphs, `"2"` the shape with them (ADR-0017), `"3"`
the shape that may also ask for an audit trail (ADR-0018), `"4"` the shape that may also require the
reviewed set (ADR-0044), `"5"` the shape that may also require comparable facts (ADR-0046), `"6"`
the shape whose audit member may also say whether its trail is chained (ADR-0047), `"7"` the shape
whose audit member may also name the public key its signing key is expected to have, and this
runtime reads all seven.

There is **no templating, no target or environment blocks, and no selection**. A templated pack was
never the pack anyone reviewed; environments are one file per environment by convention
(`jpack.staging.json`, `--config`); and choosing which decision to ask is the application's, because
applicability is not authorization. Approval is your pull request — there is no approval state in
the file.

**A pack's identity is stated once, in the pack document's `id` and `version` members.** Everything
else that names a version is a validated reference to that statement, never a second one:
`expectedVersion` is a pin `packs validate` compares; the optional `<decision-id>-<semver>.pack.json`
filename is cross-checked when followed and skipped when not; and the `packId` and `packVersion`
members on every evaluation payload are echoes read off the document that was evaluated. Any of the
three may disagree with the document, and a disagreement is an error — none of them can win one.

The `facts` and `evidence` hints are non-normative guidance for an agent gathering inputs: they say,
in your words, where a value is held. **The runtime never reads a source** — it holds no credential
and opens no network connection — and every file the convention names is read through a reader bound
to a handle held open on the configuration's own directory. Containment is two checks and neither
substitutes for the other: a lexical one refuses an absolute or escaping path before anything is
read, and resolving against the held directory at read time refuses a path that reaches outside
through a symlinked component, which a lexical check cannot see. The second is a handle rather than a
pathname so that containment holds *through* the open: a path that is checked and then opened by
pathname can have an intermediate directory swapped for an outward symlink in between, and resolving
against the handle makes that impossible rather than unlikely. A final component that is a symlink is
refused whatever it points at, and only a regular file is read. Every surface that reaches a pack
through the configuration takes this one reader, `--pack-id` included, and none of them is handed a
pathname to open for itself.

The one thing this file can ask the runtime to **write** is a record of what it evaluated
([ADR-0018](docs/adr/0018-opt-in-evaluation-audit-trail.md)). Under `configVersion "3"`, an
`audit` member names a directory relative to the configuration — `"audit": { "dir": "audit" }` —
and each completed evaluation of `experimental evaluate` and the MCP `experimental_evaluate`
tool — unless it was declared a rehearsal (ADR-0028) — then appends one JSON line to
`evaluations.jsonl` in it, and each completed `experimental graph evaluate` appends one per node
and one for the composite, unless declared a rehearsal (ADR-0041). Each line holds the
pack's id, version, `specVersion` and the digest of its exact bytes, the facts and evidence
documents as evaluated, and the disposition in its canonical form. Test runs never record —
`packs test`, `experimental graph test`, and `experimental evaluate-corpus` are checks on packs,
not decisions anyone took — a refused evaluation records nothing, because it has no disposition to
record, and a record that cannot be written refuses the run (exit 4) rather than reporting a
disposition nothing kept. The write goes through the same held handle every read does, into the
project's own tree and nowhere else. Declare no `audit` member and nothing is written at all.
Citations given in such a project, by `--cites` or the MCP tool's `cites`, are refused before
anything is evaluated (`JPS-INVOCATION-CITES`, exit 3) rather than accepted and recorded nowhere,
unless the run is a rehearsal ([ADR-0051](docs/adr/0051-refuse-citations-where-no-trail-is-kept.md)).
Where the evaluation's trace noted a comparison across JSON types or a cause of an unknown, the
record also carries `typeMismatches` and `unknownCauses`, gathered from the trace: pointers, types
and causes, never values (ADR-0046). A graph run's node records carry their own node's notes, and
the composite carries none. A record with nothing to note is byte for byte what it was.

**The trail is chained** ([ADR-0047](docs/adr/0047-make-a-decision-record-defensible.md) §1). Each
record also carries `trail` (the trail's identity, 128 random bits in hex), `sequence` (its line
number, from 1) and `previous` (the SHA-256 of the exact bytes of the line before it, without its
newline). A trail written before chaining is never rewritten: the first chained record commits to
the whole file before it at once, so its `previous` is the SHA-256 of the file as it stood. A record
is otherwise byte for byte what it was, and `recordVersion` stays `"1"`. The writer holds an
exclusive, cooperative lock on the trail while it reads the last line, numbers the lines it writes
(a graph run's nodes, then its composite), writes, and syncs the file and its directory. It refuses
to append (`JPS-AUDIT-WRITE`, exit 4, with a message saying why) after a last line with no newline,
a write that did not complete; when a line it must read is longer than 128 MiB, or the record would
be; and when the lock cannot be taken for a reason that may pass. Only where the platform or file
system offers no lock at all does it write records unchained. A project that does not want the chain
sets `"audit": { "dir": "audit", "chain": false }` under configVersion `"6"`. Recomputing each
`previous` checks the committed prefix through the last chained record. Unchained lines after it are
uncovered and outside that prefix; a file with no chained record has no chained history. Within the
prefix, editing, inserting, deleting or moving a line before a later chained record breaks a link.
That is not authenticated history. The last chained line can be edited without breaking any link,
and a trail cut short, or rewritten from any line on with its links recomputed, is as consistent as
the real one; only a commitment held by someone other than the operator, covering those lines, tells
them apart. Keep the trail, and anything copied from it, byte for byte: a decoded and re-encoded line
is other bytes with another digest.

**Checking a trail.** `jpack audit verify` reads the project's trail, or `--trail <file>`, over its
exact bytes and checks every `trail`, `sequence` and `previous` from the first chained record on;
it exits 1 on any failed check, each named in the report, and 0 otherwise. Its size is read under
the writer's lock, shared, so it falls between two writes, and the bytes before it are read without
the lock: writers only append, so a verification neither delays nor races them. Without `--expect`
the report is the integrity of one supplied chain, limited to the committed prefix through its last
chained record, and says what it does not establish: that the last chained line, or lines rewritten
from some point on with their links recomputed, are the ones first written, or that the trail is
complete. It reports an unchained suffix as uncovered and outside that prefix. `jpack audit
checkpoint` prints the checkpoint of the last chained record, one canonical JSON line naming the
trail's identity, the record's sequence and the
SHA-256 of its exact bytes, for handing to someone who will keep it. `jpack audit verify --expect
<checkpoint>` then also fails a trail that is shorter, has another identity, or has another record
at that sequence, and reports the lines up to it as checkpointed.
Every report, with or without `--expect`, also says that the trail records decisions, not attempts,
so its silence is not evidence that no evaluation was refused at the gate, rehearsed, or failed
before a disposition (ADR-0048).
After a write that did not
complete, `jpack audit repair` keeps the damaged bytes in place as a line of their own and appends
a discontinuity record that names their digest and links over them, so the writer goes on (it never
repairs a torn discontinuity, since a repair does not repair a repair);
`verify` then reports the trail as segments, never as intact across the break. A discontinuity line
records no decision: it has `kind` `"discontinuity"` and no pack, inputs or disposition.

**Handing checkpoints to a holder.** `jpack audit checkpoint --since <sequence>` prints the
checkpoint of every chained record after that sequence, one canonical line each, for a deliverer
(Desk, a job, a hook) to hand to a holder the operator does not control: the counterparty, an
auditor, a store. The deliverer polls and remembers where it is; recording a decision never waits
for it, and a record not yet covered by a held checkpoint is reported as unwitnessed. A checkpoint
is a function of its record's bytes, so a retry hands over the same line. The runtime keeps no
record of what was handed over, since the operator could rewrite one: the holder's copy is what
counts. `jpack audit verify --expect <file>` takes a holder's file of checkpoints, one per line,
holds the trail to every one of them, and reports the records up to the highest one that matched,
with no failed check at or before it, as witnessed and the rest as unwitnessed; `--require-checkpoint-through <sequence>` fails while any
record up to that sequence is unwitnessed. A held checkpoint shows the records it covers are the
ones that existed when it was handed over, to anyone who trusts the holder's copy. It shows nothing
about later records, or about checkpoints the holder did not keep, and nothing about when.

**Stamping checkpoints.** A project may name an RFC 3161 time-stamping authority,
`"audit": { "dir": "audit", "timestampAuthority": "https://…" }` under configVersion `"6"`, and
`jpack audit stamp`, run by a scheduler, Desk or a person, asks it to stamp the SHA-256 of the
current checkpoint and keeps the token in `stamps.jsonl` beside the trail (ADR-0047 §2a). Nothing on
the decision path asks the authority: a decision is appended first, a record not stamped yet is
pending, and an authority that cannot stamp leaves the trail and every decision as they were. A
checkpoint already stamped is not asked for again. `jpack audit verify --tsa-roots <file>` checks
every token against the roots the verifier trusts: its digest, its signature, its certificate's
time-stamping usage and chain at the time the token states, its policy (`--tsa-policy`), and,
against revocation lists supplied with `--tsa-crls` that can speak for the stamp's time, its
status, reported as not checked where none can. A trusted stamp whose checkpoint the trail no
longer holds shows the trail was rewritten since. The report gives the records stamped and the lag
between each record's `at` and the first stamp covering it. Stamped `through` is the uninterrupted
prefix from line 1; the trusted-stamp count separately includes a matching individual record
checkpoint beyond an earlier chain break. `--require-stamped-through <sequence>` fails while the
records up to it are not stamped. A stamp establishes that the checkpoint existed
by the time the authority states, as that authority attests. It does not establish when a record
was made: `at` stays the operator's word. It establishes nothing against an authority that is not
independent of the operator, and nothing about revocation where it was not checked. The
token is parsed and verified with the standard library alone; the guide states every rule.

**Reading a witness's statements.** A witness is a gateway run by a party other than the trail's
operator that signs the checkpoints handed to it and chains its statements per trail (gateway
ADR-0013; no gateway release serves them yet). `jpack audit verify --witness-key <file>` reads the
statements you fetched from it, `--witness <file>` and the head you fetched, `--witness-head
<file>`, under a key you trust: every statement checked under its key, the chain read from index 0,
never late, and to the head, which makes the reading current as of your fetch; without a head it is
historical. The checkpoint of every checkpoint statement that verifies is held to the trail as a
held checkpoint is, and a chain with no witness finding is credited: the coverage's `countersigned`
says how far it reaches, and `--require-countersigned-through <sequence>` fails while the records
up to it are not countersigned. A conflict statement carries a checkpoint that was offered and
refused, and is credited nothing. A longer chain is read in steps, `--witness-save` and
`--witness-resume`; `--witness-save` refuses a destination that is a file the verification reads,
other than the continuation it resumes from, or anything already there that is not a continuation,
and never hides a finding by refusing (the guide says what the check does not guard). A credited
checkpoint
statement shows the lines up to its checkpoint are the ones that existed when the witness signed
it, if the witness is independent of the operator; not who submitted it, and its time is the
witness's own clock's.

**Signing the trail.** A project may name an Ed25519 seed held outside the project, by `"audit": {
"dir": "audit", "signingKey": "<absolute path>" }` under configVersion `"6"` or by the
`JPACK_SIGNING_KEY` environment variable, and then every chained record is signed (ADR-0047 §2b):
after the record is written, and under the same lock, one line is appended to `signatures.jsonl`
beside the trail, binding the record's `trail`, `sequence` and the SHA-256 of its exact bytes. The
record line is not changed. The key must be named by its real path, with no symbolic link in it,
outside the project's directory (compared by device and inode as it is opened), one file with one
name, owned by the user the runtime runs as and readable by nobody else, in directories owned by
root or that user that nobody else can write unless their sticky bit is set; a key that is not signs
nothing (a refused directory is named: `chmod go-w` fixes one others can write, and one another user
owns must be given to root or that user, or the key moved where every directory on its path passes),
and on Windows, whose ACLs the runtime does not read, no key signs at all. The environment variable
is process-wide: inherited, it signs every chained trail the process records for, under any
configVersion, including projects that never named a key. The public key the seed is expected to
have may be named beside its path, by `"signingPublicKey": "<64 lowercase hexadecimal characters>"`
under configVersion `"7"` or by the `JPACK_SIGNING_PUBLIC_KEY` environment variable, which takes
precedence in the same way but is read exactly as set, with nothing trimmed; then a seed whose
public key is another is refused each time records are about to be signed, so a seed put at the path
in the key's place signs nothing, and an expected key not of that form, in either place, is refused
as a key before any seed is read, its value never printed. In every case `packs validate` says why
(`audit-signing-key`), and for the expected key it names neither the path nor either key. Signing
never fails a decision: a signature that cannot be written leaves the record unsigned. `jpack audit
key generate` writes a key and prints its public key, `jpack audit key public` prints a key's public
key, and `jpack audit key rotate --next <seed>` hands signing over to a next key with a line the
current key signs. `jpack audit verify --public-key <file>` checks every signature and rotation,
reports how far they authenticate one uninterrupted prefix and how many individual records have a
valid signature of their own, including beyond an earlier chain break, and fails on a bad signature,
a signature for another record, a line naming no record, a rotation the key in force did not sign, a
revoked key's line, or lines out of order; `--require-signed-through <sequence>` fails while the
signatures do not reach that sequence. Revocation is the verifier's: `--revoked <file>` names keys
not to trust from a sequence on, and `--public-key` given once per key pins the keys a trail may
rotate through. A signature shows only that whoever held the key signed. It establishes nothing
against the operator, who holds the key, and nothing after the key is copied or stolen; the
sidecar's format and the verification rule, with a test vector, are in
[docs/building-with-packs.md](docs/building-with-packs.md#record-signatures-exactly).

**Binding a caller's own material.** `experimental evaluate --binding <file>`, and the MCP
`experimental_evaluate` tool's `binding` argument, take exactly `{"kind": "<scheme>", "digest":
"sha256:<64 lowercase hex>"}`: a companion's commitment to material of its own that the runtime
never sees, such as the preparation Runner makes before a run
([ADR-0050](docs/adr/0050-record-a-callers-binding-digest.md)). `kind` is 1 to 128 characters of
`a-z`, `0-9`, `.`, `_`, `/` and `-`. The record carries it as given, as `binding` beside `cites`,
compact with `kind` first, so the chain and the record signature cover it as they cover every other
member; a record written without one is byte for byte what it was, and `recordVersion` stays `"1"`.
The runtime records the binding and does not interpret it: it never verifies or recomputes the
digest, and the evaluation does not depend on it. The companion's own verifier checks its value
against the material. A document not of that shape, one over 1 KiB, or one given with
`--rehearsal` or in a project that keeps no trail, either of which would record nothing, is refused
before anything is evaluated (`JPS-INVOCATION-BINDING`, exit 3), and nothing is written. `audit verify` checks only the shape: a
recorded binding that is malformed is the finding `binding-malformed`, which fails the verification
and moves no coverage, and a well-formed one is not mentioned. Against the operator, who holds the
signing key, a binding establishes no more than the signature does.

**Refusing a fact no comparison can match.** There is no coercion between JSON types: a flag sent
as `"true"`, `1` or `null` makes `equals true` false, not unknown, so `onUnknown: escalate` never
sees it and a detector falls through to its fallback. A project whose `jpack.json` sets
`"requireComparableFacts": true` (configVersion `"5"`, ADR-0046) has `experimental evaluate`, the
MCP `experimental_evaluate` tool, and `experimental graph evaluate` for each node refuse such an
input before evaluating (`JPS-FACTS-COMPARABLE-REQUIRED`, exit 1, no record). A run is refused when
a fact that some comparison anywhere in the pack reads is present and of a type that comparison can
never match: a type no `equals`, `not-equals` or `in` operand has, or, for an ordered comparison,
anything but a decimal string, so a string outside the decimal grammar is refused as well as a JSON
number. The check covers every comparison, including ones evaluation would not reach. Its work is
bounded by the evaluation's work limit, and a check that would pass that limit refuses under the same
code, whatever the pack compares, because it cannot then say the facts are matchable. The refusal is
the project's and not an evaluation error, so it carries no §8.4 class. An absent fact is not
refused, because it is unknown and `onUnknown` governs it. Declared rehearsals are refused too. `packs
test`, `experimental graph test`, `experimental evaluate-corpus`, `experimental compare` and the MCP
test tools are not, because their rows may probe such facts on purpose. The refusal names each
pointer, the fact's type and what the comparison can match, never a value.

### The reviewed set

`jpack packs lock` writes `jpack.lock.json` beside the configuration: the digest of the
configuration's exact bytes and of every pack and graph it declares
([ADR-0019](docs/adr/0019-reviewed-set-lock.md)). Running it **is** the amendment — it is how a
project says, in a file a reviewer diffs, that the law changed on purpose — and it approves nothing:
your pull request is still the approval. The file is generated and deterministic, so re-running it
over an unchanged tree leaves no diff. `jpack packs verify` names every difference from it:
`config-drift`, `document-drift`, `document-missing`, `lock-entry-missing`,
`locked-but-undeclared`, and `path-mismatch`.

Its **presence** is the opt-in, and it is found by convention rather than declared: no
`configVersion` moves, the schema does not change, and a project with no lock file behaves exactly
as it did. With one, the deciding surfaces — `experimental evaluate`, `experimental graph evaluate`,
and the MCP `experimental_evaluate` tool — hold the law they are about to apply to it, declared
rehearsals excepted (ADR-0028, ADR-0041), and refuse a
mismatch (`JPS-LOCK-VERIFY`, exit 1) with the two honest ways forward: declare the amendment, or
restore the reviewed bytes. `packs test`, `experimental graph test`, and `experimental
evaluate-corpus` consult it never: the author's loop is free and decisions are classified. A pack
named by path, or passed as text over MCP, is a draft — evaluated, never refused for being unlocked,
and recorded as a draft. Where an audit trail is configured, each record carries `reviewed`: `true`
when every document applied was declared and matched, `false` for a draft, absent when the project
declares no lock. The evaluation payload carries the same `reviewed`, with `reviewedSet` naming the
lock's revision when it is `true`, so a caller learns it without reading the trail (ADR-0044). A
rehearsal consulted no lock and carries neither.

**Requiring it.** A project whose `jpack.json` sets `"requireReviewed": true` (configVersion `"4"`,
ADR-0044) has the deciding surfaces refuse, before evaluating, every run that applies a draft and
every run while the project has no lock (`JPS-LOCK-REVIEW-REQUIRED`, exit 1). A declared rehearsal
is not a decision and is not refused. This binds a caller that neither chooses the configuration a
run reads nor can edit it or its lock, such as an agent limited to the tools of an MCP server
someone else launched. It cannot bind whoever chooses the configuration (`--config`, `JPACK_CONFIG`, the working
directory) or can edit `jpack.json` or the lock, and turning it off is itself an edit the lock
records, so a by-id run is refused until the project locks again.

**What it is not.** It is not a wall. Anything that can edit a pack can run `packs lock` again, and
this runtime cannot tell that from an author amending policy on purpose — they are the same act.
What the lock buys is that the amendment stops being silent.

Seven commands, one CI line, and one amendment step:

```bash
jpack packs list                                            # the resolved inventory
jpack packs validate && jpack packs lint && jpack packs test && jpack packs verify   # the CI gate
```

`packs lock` is deliberately not in that line. It is the amendment — you run it when the law changes
on purpose, and you commit its output with the change it pins. Running it immediately before
`packs verify` would make the verification vacuous, which is the one way to hold this convention
backwards.

`packs validate` reports six named checks per pack — path containment, document validation, the
`expectedVersion` pin, the filename cross-check, the hint keys, and matrix well-formedness — each as
`passed`, `failed`, or `skipped`. `packs test` runs each pack's instance matrix through the experimental
evaluator and compares every row the way the bundled evaluation corpus is compared: the RFC 8785
canonical §8.3 disposition byte for byte, or the expected §8.4 error class and phase, plus the one
optional per-row assertion about the escalation target §8.3 keeps *outside* the disposition
(`expectedHandoffTarget`, which needs `matrixVersion: "2"`). A row may also carry `cites` (which
needs `matrixVersion: "3"`): the gateway receipts its facts were transcribed under, validated when
the matrix loads and carried on the row's result as values — metadata the comparator never reads
(ADR-0034). The two carriers share the fields the
comparator reads, which is what makes the comparison one implementation rather than two — not a
claim that a row moves between them untouched. Corpus admission additionally requires its own
members (`pack`, `origin`, `supportedExtensions`, `focus`, `specSection`), and its closed schema
forbids `expectedHandoffTarget` and `cites`, so lifting a project row means supplying those and
removing any target assertion and any citations. Both commands exit `1` on any failure, a pack with no
matrix is reported *skipped* rather than passed by default (with `packs test --require-matrix` it is
reported *mismatch* instead; ADR-0042), and a `packs test` run in which no row ran at all is reported
*skipped* and exits `1` unless a mismatch was found: a green gate over zero rows would say a project
was tested when nothing was. A new project may start with an empty `packs` object: inventory and structural
validation accept it, while `packs test` remains skipped with exit `1`. `packs lint` closes the gap neither of them covers: a pack consulting a pointer no
source feeds raises no error anywhere — the condition is unknowable, so a rule touching it
escalates, or under `onUnknown: ignore` is skipped and the fallback answers without it — so the lint holds every consulted
pointer to a producer declaration (the configuration's own hints, or an explicit `--producers`
manifest) and fails the build where that defect otherwise hides (ADR-0022).

From a shell, `jpack experimental evaluate --pack-id expense-approval --facts facts.json`
reaches the same pack by the same name. Over MCP the same inventory is `list_packs`, one document is
`get_pack`, and `experimental_evaluate` accepts `pack_id` instead of pasted `pack` text; the graph
convention has the same pair, `experimental_list_graphs` and `experimental_get_graph`, beside the
CLI's `experimental graph list` (ADR-0029). With no configuration,
`list_packs` answers empty with an explanation of where the runtime looked, rather than failing.

[docs/building-with-packs.md](docs/building-with-packs.md) is the builder's guide: the packs-as-code
lifecycle, the three-owner model (the application selects, the agent gathers and never invents, the
pack judges), hints in practice, and the data-sufficiency-as-another-pack pattern.

## Process contract

| Exit | Meaning |
| ---: | --- |
| `0` | Command succeeded; validation passed the reported scope. |
| `1` | Document invalid, an expectation mismatched, or a check the command makes failed — a conformance mismatch, a difference from the reviewed set, or law that left it. |
| `2` | Exact JPS version or required extension unsupported. |
| `3` | Invocation or suite configuration invalid. |
| `4` | Input/output or resource-limit failure. |
| `5` | Internal runtime or bundled-artifact failure. |

`--format json` writes exactly one versioned JSON object plus a newline to standard output for
normal valid, invalid, unsupported, mismatch, and handled operational results. It never mixes human
prose or ANSI controls into that stream. Results include `diagnosticsTruncated` so automation can
detect a reached output limit. `--quiet` is available only with human output.

Human document results, including `invalid` and `unsupported`, use standard output. Invocation,
input/output, resource, and internal failures use standard error.

## Security defaults

The current implementation:

- fetches nothing for evaluation or input acquisition and never dereferences document locators; the
  sole exception is requested time-stamping, when it requests and receives a time-stamping
  authority's token;
- accepts one explicitly selected regular file or standard input, not URLs or special files;
- writes only where it was told to, in three ways and no others: a copy of a bundled schema or
  example at the target an operator names with `--write`, which refuses to overwrite an existing
  file; one appended record per completed non-rehearsal evaluation (ADR-0028), and for a non-rehearsal graph evaluation one per node and one for the composite (ADR-0041), when a project's `jpack.json` declares an
  `audit` directory ([ADR-0018](docs/adr/0018-opt-in-evaluation-audit-trail.md)), into that
  directory, through the handle held open on the configuration's own directory, under an advisory
  lock on the trail file itself when the trail is chained (ADR-0047) — a record is not a
  diagnostic, and it carries the documents the project asked to have recorded; and the reviewed-set
  lock `jpack packs lock` generates beside the configuration when an operator runs that command
  ([ADR-0019](docs/adr/0019-reviewed-set-lock.md)), replaced in place through the same handle and
  refused outright if it would land on a document the configuration declares;
- rejects duplicate decoded member names at every depth, invalid UTF-8, trailing JSON, and
  non-JSON constants;
- caps a document at 10 MiB, nesting at 128, parsed nodes at 250,000, and diagnostics at 100;
- caps local conformance suites at 10,000 cases and 100 MiB total;
- caps diagnostics retained across one conformance result at 1,000;
- validates suite metadata before resolving fixtures and rejects traversal and symlink paths;
- treats extension values as inert data; and
- emits sanitized, value-free human diagnostics.

See [SECURITY.md](SECURITY.md) for reporting and boundary details.

## Artifact provenance

Runtime validation uses only files embedded in the binary and verified against one lock per bundled
specification version:
[`internal/artifacts/jps/0.1.0-draft/lock.json`](internal/artifacts/jps/0.1.0-draft/lock.json) and
[`internal/artifacts/jps/0.2.0-draft/lock.json`](internal/artifacts/jps/0.2.0-draft/lock.json).
Each lock records the source repository, exact commit/ref and source state, plus SHA-256 and size
metadata for every imported file — 50 for `0.1.0-draft`, and 56 for `0.2.0-draft`, whose bundle adds
the evaluation corpus of §3.4.1 (its manifest, that manifest's schema, and the four pack fixtures its
rows name). The release gate checks every bundle: a development snapshot remains visibly labelled
`unreleased-local-snapshot` and cannot pass it.

> **Provenance note.** The `0.2.0-draft` bundle is pinned to the specification tag `v0.2.0-draft`.
> The `0.1.0-draft` bundle stays pinned to an exact commit rather than to its tag: the `v0.1.0-draft`
> tag carries schema `$id`s under a temporary repository-hosted URL, while the permanent
> `https://judgmentpack.org/schema/` identifiers landed after it, and the specification's release
> tooling requires the tag string to equal `specVersion`, so they cannot be published under a second
> `0.1.0-draft` tag. A full-length commit digest is an explicitly supported immutable reference here,
> and the release gate accepts it.

Artifact bundle and conformance-corpus digests use `sha256-length-prefixed-v1`: each sorted path and
file body is encoded as an unsigned 64-bit big-endian byte length followed by those exact bytes.
The corpus digest covers `manifest.json`, `manifest.schema.json`, and every manifest fixture, so an
equivalent bundled and local corpus produces the same value. Human and JSON conformance output both
report it.

Before any release, those files must be re-imported from an approved immutable specification
commit or tag. The lock must say `immutable-git-ref`, record a clean source worktree, and identify
the exact ref and commit. Mutable `main` is never a runtime validation authority.

Maintainers can create a new, initially absent snapshot directory with:

```bash
env GO111MODULE=on go run ./tools/sync-spec-artifacts \
  --source ../judgment-pack-spec \
  --destination ./internal/artifacts/jps/<exact-version> \
  --allow-dirty
```

`--allow-dirty` is deliberately required for an unreleased snapshot. A release candidate instead
uses `--source-ref <exact-commit-or-tag>`; that mode verifies the official repository origin, a
clean worktree, and that the ref resolves to checked-out `HEAD`. Artifact updates are reviewed
source changes; the runtime never runs this tool.

## Public and commercial boundaries

This repository is intended to remain a self-contained Apache-2.0 public core. It must build,
install, validate, and run its conformance tests without private repositories, credentials,
services, package indexes, or feature flags.

Commercial capabilities should live in separate private repositories; this public repository must
never depend on them. The current supported integration boundary is the `jpack` executable
and its versioned JSON output. Go packages are intentionally `internal`, and there is not yet a
stable in-process SDK or plugin API. Before commercial commands are composed into one binary, the
public project must define and version that contract deliberately. Future commands should use
distinct namespaces such as `jpack cloud` or `jpack org`; they must not override
`jpack spec` conformance semantics or auto-load during validation.

The normative specification, schemas, and public corpus remain in the separate
[`judgment-pack-spec`](https://github.com/Judgment-Pack/judgment-pack-spec) repository.

## Development

```bash
env GO111MODULE=on go fmt ./...
env GO111MODULE=on go vet ./...
env GO111MODULE=on go test ./...
env GO111MODULE=on CGO_ENABLED=0 go build -trimpath ./cmd/jpack
```

`scripts/dev/check.sh` runs every check CI runs, in order, and prints one summary block of passes,
failures and skips; `--setup` runs the install steps first and `--only NAME` re-runs one check. The
checks are listed in [`dev.json`](dev.json), the manifest of the Judgment Pack development kit
(v0.1.0), whose runner is vendored here unchanged. `AGENTS.md` and `CLAUDE.md` carry the same
instructions for agent sessions, shared rules first and this repository's own after them; CI checks
that the two are identical (`scripts/sync-agent-docs.sh` writes `CLAUDE.md` from `AGENTS.md`).

See [CONTRIBUTING.md](CONTRIBUTING.md), [docs/architecture.md](docs/architecture.md), and the
[maintainer release runbook](docs/releasing.md).

In VS Code, **Terminal → Run Task → judgment-pack: Build CLI** builds `bin/jpack`; the test
and bundled conformance tasks are available from the same menu. The tasks explicitly enable Go
module mode for older WSL configurations.

## License

Apache License 2.0. See [LICENSE](LICENSE).

### Graph evaluation revisions

Experimental graph evaluations include `graphSha256`, `configSha256`, and a
`packSha256` for each node. These identify the exact bytes loaded for that
run; they do not establish reviewed status or an atomic project snapshot.
See [ADR-0049](docs/adr/0049-bind-graph-evaluations-to-loaded-bytes.md) and the
[composition fixtures](internal/graph/testdata/composition/README.md) for
fan-in, fan-out, repeated packs and unresolved results.

## Dialect document tools

`jpack dialect` provides offline document encoding and version-scoped validation tools for the draft dialect
companion pinned at Spec commit `b7cc8906c23d92c63f4e42c4706cc1e21b9d324f`.
Use `jpack dialect contract --format json` to discover the implemented operations,
encoding and validation versions and limits. This is separate from the Core versions reported
by `jpack version`.

```sh
jpack dialect init --dialect-version 2 > draft.dialect.json
jpack dialect canonicalize draft.dialect.json > canonical.dialect.json
jpack dialect digest canonical.dialect.json
jpack dialect validate draft.dialect.json --format json
jpack dialect canonicalize draft.dialect.json --format json
```

The starter has no sources or field copies and follows the reference Runner
spelling for its version. JSON reports identify it with `starterTemplate: true`.
Saving and editing are client operations. Encoding checks strict JSON, safe
integer literals, the declared version and version-3 identity spelling; it does **not** validate the complete dialect schema, map it against a
pack, establish executable source support or verify a preparation. Reports say
`encoded` and `fullDocumentConformance: false`. The separate `validate` operation checks version-2 and version-3 document forms and internal
references: closed members, parameters and dependency order, templates, reads,
conditions, target collisions, admissions and document limits. A pass says
`document-valid` with `validationScope: "version-2-document"` or `"version-3-document"`; it is not a
preparation-verifier conformance claim. Profile-dependent compatibility (including
request/adapter fit), acquisition and preparation verification are **not checked**.
Declared pack inventory coverage is available through the separate `check` operation.
Both formats support `operation` and `selected-file` sources. Version 3 also
checks unbound operation declarations (`platform`, `operation`, `shape`, `record`)
without resolving or verifying their bindings. Partial declarations and mixed
unbound names/profile pins are refused. Version 3 additionally checks `research`
questions, parameters, typed requested fields, capability declarations, profile
assignment shape, and closed reconciliation reads. The version-3 kinds `http`,
`sql` and `web` return unsupported; unknown kinds return malformed-document errors.
No partial coverage is returned for unsupported sources.

Contract discovery advertises `validationVersions`, `schemaVersions` and
`packCheckVersions: [2, 3]`, `validationSourceKinds: ["operation", "selected-file", "research"]`,
`unboundOperationValidation: true`, and `bindingVerification: false`.
Runner still owns binding and preparation. These integer format versions are not
artifact release versions. Named formats are separately advertised as
`formatVersions: ["2.0.0", "3.0.0"]`; they require `id` and a string artifact
`version`. Both named formats use strict omission rules and preserve all identity
fields in canonical bytes. Their preparation families remain integer 2 and 3.

```sh
jpack dialect init --format-version 3.0.0 --id urn:example:dialect:companies --artifact-version 1.0.0
jpack dialect schema --format-version 3.0.0
```

MCP `dialect_init` accepts `formatVersion`, `id` and `artifactVersion`;
`get_dialect_schema` accepts `formatVersion`. Never combine the named selector
with the legacy `version` argument (`--dialect-version` on CLI). This does not
authorize a job, perform publication or enable new source kinds.

Human `init` and `canonicalize` print the exact canonical document bytes without
a trailing newline. Human `digest` prints `sha256:…` followed by a newline. JSON
output carries the exact canonical bytes in its `document` string and their
`digest`, plus scope, diagnostics and the pinned reference. `--pretty` changes
only the outer JSON report. Canonicalization preserves members and array order;
negative zero becomes zero under the document's canonical rules. Fractions,
exponent spellings, unsafe integers, malformed Unicode and duplicate keys are
refused. Preparation canonicalization and decision-disposition JCS are different
forms and are not exposed by these commands.

The MCP equivalents are `get_dialect_contract`, `dialect_init`,
`dialect_canonicalize`, `dialect_digest`, `dialect_validate`, `get_dialect_schema`,
`get_dialect_reference` and `dialect_check`. They take text, never file paths,
and perform no network calls, project reads, saving or evaluation. Unsupported
encoding versions or defined but unsupported source kinds return CLI exit 2; malformed or over-limit documents return 1;
bad invocation and read failures use the existing exits 3 and 4. A refused MCP
operation carries the same structured report and `isError: true`.

### Dialect authoring resources and pack coverage

`jpack dialect reference` prints the exact pinned companion offline. The text
includes proposed forms; inspect `dialect contract` for implemented capabilities.
`jpack dialect schema` prints a **Runtime-derived** version-2 or version-3 JSON Schema editing
aid (`--dialect-version 3`), not a normative Spec artifact. It cannot enforce strict JSON spelling or
cross-references; run `dialect validate` before treating a document as checked.
JSON/MCP resource reports identify the text's exact bytes and SHA-256 separately
from dialect identity. No resource is fetched at runtime.

```sh
jpack dialect schema > dialect-v2.schema.json
jpack dialect reference
jpack dialect check draft.dialect.json --pack decision.json --format json
```

`check` validates both supplied documents and compares targets with Runtime's
existing pack inventory. It reports `mapped`, `partial`, `acknowledged` or
`missing` for each consulted pointer or evidence requirement. An ancestor target
declares a subtree; a child target only partially covers an aggregate read.
Explicit `unmapped` declarations stay visible. Undeclared evidence targets or
omissions fail the check; unused fact targets and stale omissions produce notes.
The inventory over-approximates reads and can include condition-shaped data.

Overall `complete` or `review-required` exits 0; inspect the status and notes
before treating a mapping as complete. `incomplete` or `not-applicable` exits 1;
unsupported capabilities exit 2. A refusal emits no partial coverage. The pack
SHA-256 binds the report to the exact supplied bytes. `valueTypesChecked` and
`executionChecked` remain false: this does not establish runtime fact presence,
type compatibility, correct decisions or runnable sources. Profiles and
preparation verification remain Runner responsibilities. CLI permits `-` for
one operand only; MCP takes exact `document` and `pack` strings. Neither uses the
ambient project or changes files. Capabilities declare supported versions and
input/work limits. See ADR-0054 for scope and status details.

## Artifact version options

`jpack artifact version --base 0.1.0 --format json` returns the patch, minor
and major choices `0.1.1`, `0.2.0` and `1.0.0`. Optional `--candidate` reports
numeric ordering (`lower`, `equal` or `greater`). The equivalent read-only MCP
tool is `get_artifact_version_options` with `base` and optional `candidate`.
Both transports use the pack's exact three-component grammar: no leading zeroes,
prerelease suffixes or build metadata. Components use exact integer arithmetic.

This is version arithmetic, not compatibility analysis or publication admission.
It reads no project, artifact, job history or registry, reserves no version and
saves nothing. Application policy chooses an appropriate bump, verifies usage
and enforces immutable publication. In particular, using a version in a job
does not become an implicit Runtime instruction to rewrite a document.

The tool accepts at most 256 bytes per version and refuses a base whose next
choices exceed that bound. This resource limit does not narrow the Core schema.
Invalid input returns no partial choices. Scope and compatibility-not-checked
are reported in-band. See ADR-0055.

## Experimental offline Policy prototype

`jpack experimental policy` implements the **nonnormative** RFC 0023 prototype,
exact version `0.0.2`, for `policy` and `governing-adoption` documents
([ADR-0059](docs/adr/0059-experimental-offline-policy-tools.md)). This retained
prototype is an experimental authoring aid; its results establish no approval,
governing-adoption authority or standard conformance. Discover the exact scope,
limits and pinned source revision with `experimental policy contract --format json`.
No stable Policy format is selected by default.

```bash
jpack experimental policy contract --format json
jpack experimental policy init --kind policy --format-version 0.0.2 --id urn:example:policy --version 1.0.0 > policy.json
jpack experimental policy schema --kind governing-adoption --format-version 0.0.2
jpack experimental policy validate policy.json --format json
jpack experimental policy canonicalize policy.json
jpack experimental policy digest policy.json
jpack experimental policy check policy.json --materials retained-materials.json --format json
```

`init` prints a starter template with missing retained materials, identified in
its JSON report as `starter-template`; it does not produce a checked publication.
Human output keeps the template as JSON on stdout and prints the missing-material
caveat on stderr. The client supplies and saves the materials.

The schema resource is a self-contained Runtime editing copy derived from pinned
prototype schemas; strict validation remains a separate operation. Both selectors
are required for `init` and `schema`; `init` also requires an artifact `--id` and `--version`,
which are separate from the prototype format version.

`validate` checks strict document forms, schemas and local references.
`check` additionally verifies the exact supplied retained-material reference
closure, raw byte digests and lengths, Policy section anchors, structural Core
packs and the particular consumer references the prototype defines. It does not
perform full Core semantic validation or evaluate rules. Encoding tools check
the canonical number domain, and return canonical identity separately from raw
byte identity. A supplied Core pack is held to canonical encoding only when a
reference explicitly requests its canonical identity.

The required `--materials` input is a **private Runtime transport** of supplied
bytes, not a portable release envelope:

```json
{"materials":[{"content":{"bytesDigest":"sha256:<64 lowercase hex>","byteLength":12,"mediaType":"text/plain"},"dataBase64":"<base64 of exact retained bytes>"}]}
```

The example describes the transport shape; replace its placeholder values with
the exact content reference and bytes. No path or URL is accepted inside it.
CLI reads only named bounded regular files or `-` for standard input, with at
most one standard-input operand; it performs no implicit project reads or writes.
Documents are bounded at 256 KiB, retained source originals at 4 MiB, narrative
and review-basis material at 1 MiB each, distinct retained bytes at 16 MiB, and
material JSON text at 24 MiB. Reports and errors do not echo retained bytes.
The MCP equivalents and their smaller effective stdio envelope are documented
in [MCP clients](docs/mcp-clients.md#experimental-policy-prototype-tools).

The RFC, prototype schemas and synthetic fixtures are bundled from specification
revision `8a56a95d555c17640d2db4984363b779536b3c2a`, with per-file digests and
licensing provenance. They are never fetched at runtime.

### Official Policy draft selection

The same experimental commands explicitly support the independently released
Policy draft `formatVersion: "1.0.0"`
([ADR-0061](docs/adr/0061-adopt-official-policy-draft-resources.md)). Resources are
pinned to `policy-v1.0.0-draft.1`, Spec commit
`0799b37b74e35c4a86bfbf70f7bee80bdb7bbe94`, in
[the official provenance manifest](internal/policy/official/provenance.json).
The full published archive is retained with exact source paths and per-file
hashes, including the public HTTPS schemas, prose, complete 124-row corpus,
examples, reference reader and its frozen dependencies. Runtime verifies that
inventory before schema compilation and in the release gate; it fetches nothing.
This is draft support through the existing experimental surface. Runtime's
conformance claim remains solely in [CONFORMANCE.md](CONFORMANCE.md).

Default `contract` output and all prototype reports keep their existing bytes
and shape for strict clients. Explicit discovery returns a separate
`registryVersion: "1"` report; selecting one contract returns that format's
provenance and capabilities:

```bash
jpack experimental policy contract --all-formats --format json
jpack experimental policy contract --format-version 1.0.0 --format json
jpack experimental policy init --kind policy --format-version 1.0.0 --id urn:example:policy --version 7.3.42
jpack experimental policy schema --kind governing-adoption --format-version 1.0.0
jpack experimental policy check adoption.json --materials retained-materials.json --format json
```

`--all-formats` and contract `--format-version` are mutually exclusive. Documents
with `formatVersion` select the draft grammar explicitly; supported prototype
inputs continue using their exact wrapper. Optional `--format-version 1.0.0` on
canonicalize/digest/validate/check also selects draft refusal handling for
malformed or selector-less input. The document must still carry its exact
selector. No tool inserts it, migrates bytes or changes a Job or approval.
Draft Policies use `workspace-managed` or `externally-managed`; `desk-managed` is
refused. Artifact `version` remains independent of the format selector. Draft
adoptions require retained 1.0.0 Policy bytes; prototype Policies cannot satisfy
those references.

Explicit 1.0.0 reports identify `profile: "jps-policy-companion"`,
`profileVersion` and `formatVersion: "1.0.0"`, `maturity: "draft"`, the immutable
source commit and its `spec/policy.md` URL. The existing `candidateCapabilities`
member now contains the exact official draft capability artifact. Only a full
`check` includes `candidateResult`, whose complete value is one of:

```json
{"status":"valid","formatVersion":"1.0.0","maturity":"draft"}
{"status":"refused","code":"invalid-selector"}
```

The finite refusal codes and phase ordering are defined by the
[pinned draft contract](internal/policy/official/spec/policy.md#5-resource-limits-and-refusal-classifications).
Runtime retains exit 0 for success, 1 for invalid content and 2 for unsupported
input; the nested result supplies the portable classification. Environment
failures supply no portable result. Canonicalize/digest establish encoding and
identity only; validate checks document forms and local references and leaves
retained materials unchecked. These narrower reports omit `candidateResult`.
The scopes are `closed-draft-document-and-local-references` for validate and
`draft-supplied-exact-bytes-anchors-and-scoped-core-references` for check.
`experimental` stays true and `fullDocumentConformance`, approval, execution,
live-authority and network-acquisition capabilities stay false. Full Core
semantics and evaluation remain outside these checks.

**Migration from Runtime v0.36.0:** explicit 1.0.0 report metadata changes from
`proposed-draft` to `draft`, with a new source pin, URL, capability exclusions and
scope strings. Strict report consumers must deliberately update their Runtime
and Spec pins and expected metadata. The report member names, output/contract
versions, refusal codes and exact Policy/adoption grammar remain unchanged.
Candidate reports retained from the old Runtime keep their original source and
maturity; they are historical evidence, never relabeled. The old
[candidate vendor tree](internal/policy/candidate/provenance.json) and prototype
bytes remain frozen. Promotion changes no Policy/adoption, Pack, source,
narrative or review-basis bytes. Prototype migration requires the separate
[explicit migration](internal/policy/official/conformance/policy/1.0.0/MIGRATION.md),
new immutable identities and dependent review; nothing migrates implicitly.

Draft schema reports return self-contained Runtime editing copies with a new
release-specific URN `$id`; `resource.sourceId` names the original immutable
`https://judgmentpack.org/schema/policy/1.0.0/{kind}.schema.json` resource. The
editing copy is not served under the upstream ID. Raw upstream resource bytes
and hashes remain in the official vendor tree.

The Go harness compares all 124 official portable result objects, acquiring
vector sources lazily and preserving raw Pack numbers and retained-byte
identities. The unchanged reference reader runs against the same retained
archive. Portable tests use the shared raw registration/checking engine:
distinct-byte bundles can exceed the private 24 MiB JSON/base64 carrier or MCP's
16 MiB request limit, especially with repeated bytes under multiple media types.
These ceilings remain unchanged. Agreement is same-vendor implementation
evidence and establishes no stable maturity, independently authored
interoperability, approval, current authority or permission.