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 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, 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 and
ADR-0011. 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. 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.
That command carries one further opt-in, --rfc0008-quantifiers, which is a draft-RFC
prototype per ADR-0009: 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. 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.
jpack version
jpack spec validate <pack-or->
jpack spec test-conformance [suite]
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
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 for per-client setup and
docs/agent-testing.md for the agent-driven testing protocol.
Download the archive for your operating system and architecture from
GitHub 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:
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:
tar -xzf judgment-pack_0.1.0_linux_amd64.tar.gz
install -m 0755 jpack "$HOME/.local/bin/jpack"
jpack versionOn Windows, expand the .zip, move jpack.exe into a directory on your user PATH, and
run:
jpack versionVerify the archive before extracting it. On Linux:
grep ' judgment-pack_0.1.0_linux_amd64.tar.gz$' checksums.txt | sha256sum --checkOn macOS:
grep ' judgment-pack_0.1.0_darwin_arm64.tar.gz$' checksums.txt | shasum -a 256 --checkOn Windows 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:
gh attestation verify judgment-pack_0.1.0_linux_amd64.tar.gz --repo Judgment-Pack/judgment-pack-runtimePackages 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.
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:
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.jsonGive it the facts of one expense and evaluate. The amount is a decimal string, as the specification requires for a number a rule compares:
$ 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 081cf18af9fe667a5da5acab465f4cf6118a00742bb6e5527d4d45fc09f25185Now 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:
$ 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 081cf18af9fe667a5da5acab465f4cf6118a00742bb6e5527d4d45fc09f25185Releases 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 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.
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.
env GO111MODULE=on CGO_ENABLED=0 go build -trimpath -o ./bin/jpack ./cmd/jpack
./bin/jpack --help
./bin/jpack versionFrom a sibling checkout of judgment-pack-spec, validate a synthetic example:
./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.jsonStandard input is accepted explicitly with -:
./bin/jpack spec validate --format json - < pack.jsonRun a bundled document-conformance corpus — 0.1.0-draft by default, or an exact bundled version:
./bin/jpack spec test-conformance
./bin/jpack spec test-conformance --format json
./bin/jpack spec test-conformance --spec-version 0.2.0-draftRun 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, 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:
./bin/jpack experimental evaluate-corpus
./bin/jpack experimental evaluate-corpus --format json
./bin/jpack experimental evaluate-corpus --spec-version 0.2.0-draftInspect or copy the bundled schema without network access:
./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 short-circuits in this order:
- strict UTF-8 JSON carrier parsing, including duplicate-member rejection;
- exact
specVersiondispatch; - Draft 2020-12 structural validation with URI, date, and date-time assertions;
- normative semantic reference and extension-declaration checks; and
- 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.
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 — 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. 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.
§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:
-
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-exhaustionin theevaluationphase 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.gocomputes 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 perincandidate 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-inputin thepreflightphase — §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.
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).
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.
{
"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), and this runtime
reads all six.
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). 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.
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 §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 shows whether the lines are consistent with one another: a line edited, inserted, deleted
or moved anywhere before the last breaks a link. That is not authenticated history. The last 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, and says what it does not establish: that the
last line, or lines rewritten from some point on with their links recomputed, are the ones first
written, or that the trail is complete. 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.
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; --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.
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; a key that is not signs
nothing, 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. In every case
packs validate says why (audit-signing-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 the signatures reach and
how many records are unsigned, 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.
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.
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). 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:
jpack packs list # the resolved inventory
jpack packs validate && jpack packs lint && jpack packs test && jpack packs verify # the CI gatepacks 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 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.
| 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.
The current implementation:
- performs no runtime network requests and never dereferences document locators;
- 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'sjpack.jsondeclares anauditdirectory (ADR-0018), 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 lockjpack packs lockgenerates beside the configuration when an operator runs that command (ADR-0019), 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 for reporting and boundary details.
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 and
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-draftbundle is pinned to the specification tagv0.2.0-draft. The0.1.0-draftbundle stays pinned to an exact commit rather than to its tag: thev0.1.0-drafttag carries schema$ids under a temporary repository-hosted URL, while the permanenthttps://judgmentpack.org/schema/identifiers landed after it, and the specification's release tooling requires the tag string to equalspecVersion, so they cannot be published under a second0.1.0-drafttag. 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:
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.
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 repository.
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/jpackSee CONTRIBUTING.md, docs/architecture.md, and the maintainer release runbook.
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.
Apache License 2.0. See LICENSE.