Trigon

Trigon

Does this published artifact correspond to that source?

Trigon downloads a published package, finds the source it claims to come from, rebuilds it in a sealed container and compares the two. It reports in one word how far they agree, and signs that answer so anyone can check it without trusting us.

It rebuilds npm, PyPI, crates.io and NuGet packages today. Comparing two artifacts you already hold needs no network.

The trigon: source, published artifact and rebuild A triangle with three labelled corners. The top corner is the source, what reviewers read. The lower left corner is the published artifact, what users install; the edge from it to the source is dashed and reads "claims to come from". The lower right corner is the rebuild, Trigon's own build; the edge from the source to it reads "rebuilt in a sealed container". The base joins the rebuild to the published artifact, marked with an equals sign, and reads "must meet the published bytes". = source what reviewers read published artifact what users install rebuild Trigon’s own build claims to come from rebuilt in a sealed container must meet the published bytes
The package names its source, and the registry does not check that claim. Trigon builds the source again and holds the result to the published bytes.

Source and artifact

Package registries ship artifacts: a .tgz on npm, a wheel on PyPI, a .crate, a .nupkg. Reviewers read source: the repository on GitHub. The registry does not check one against the other.

Attackers use that gap at build time. A malicious publisher, or an attacker on the build machine, can ship something the source never contained. In event-stream, ua-parser-js and the xz backdoor, the malicious code sat in the published artifact, outside the repository anyone was reading.

Trusted publishing and SLSA provenance record which workflow, in which repository, at which commit, produced an artifact. They do not show that the artifact corresponds to the source at that commit: a compromised workflow or a malicious build script still produces valid provenance. To find the source, Trigon reads the commit a package records about itself where it has one: npm’s gitHead, the <repository> commit in a .nupkg, a crate’s .cargo_vcs_info.json. Then it rebuilds to check the correspondence. Reading trusted-publishing and SLSA provenance is designed (docs/06 §5) and not built.

The limits of a verdict

Trigon does not ask whether a package is safe. A package that builds a backdoor from its own source reproduces, and that is the correct answer. A verdict tells you the artifact is what the source makes; you still judge whether the source is any good.

On npm, tarballs come close to full reproducibility with no link to their source, so a wall of green on an npm lockfile is true and close to worthless. The npm question worth asking is source attribution: whether the tarball corresponds to the repository it claims.

How it works

Rebuild it in a box

Trigon finds the source at the right commit, works out a recipe (a strategy: install these dependencies, run this build) and runs it in a podman container. The recipe is data, so Trigon can store, compare and replay it.

The mirror is the only way out

During the build the container can reach one thing: a mirror of the registry as it stood when the publisher released the package, so each dependency resolves to the version the publisher got. The mirror hashes and records everything it serves. If the build downloads the artifact Trigon compares it against, Trigon marks the run void. This is the mirror-only egress tier. At the default, open, the build can reach the internet, and the run publishes only as void.

Stabilizers remove harmless noise

Two honest builds of one source rarely match byte for byte. Each stabilizer removes one known kind of harmless difference, such as a timestamp or the order of files, the same way on both sides, and reports what it changed and how risky the change is. The report names every stabilizer that fired, so you can see when one you reject was used.

One word for the answer

The comparison ends in one of four outcomes, strongest first. On the wire each outcome is its string name.

outcomemeans
exact The raw bytes are identical, before any stabilizer ran.
normalized The stabilized forms are identical, and every stabilizer that fired ships with Trigon and is no riskier than metadata: timestamps, ordering, file modes.
normalized_with_caveats The stabilized forms are identical, but a stabilizer that fired changes content, or a person or a model wrote it. Read what fired.
divergent The stabilized forms differ, and Trigon names which files.
void Outside the four. The run proves nothing, and Trigon records why: the build could reach the open internet, it reached the artifact Trigon was comparing it against, or a stabilizer that a person or a model wrote fired.

trigon verify prints the four outcomes in these glyphs and colours. trigon check adds ⊘ for void, and colours normalized_with_caveats green with the other passes. The word and the glyph carry every distinction the colour does, so output without colour keeps them all.

Signed and published

Trigon signs its answer as an in-toto statement in a DSSE envelope. Anyone holding the two artifacts can re-run the comparison with a verifier build that has no network client. trigon publish writes the answers the publication gate allows to an evidence repository, an ordinary git repository with an append-only signed log. A consumer clones it and queries it locally, so a thousand-package lockfile costs no request per package, and no server learns which packages you use.

Search and judgement, kept apart

When Trigon finds the source, guesses the recipe or repairs a failed build, it is searching, which can be messy and may use the network or a language model. When it decides whether two artifacts are the same, it is judging, which must be exact and repeatable. A language model can help the search when you name a provider; the crates that judge link no model code.

may use the network, and a model if you name one

The search half

  • Registry clients and source discovery
  • Recipe inference from checked-in definitions, ecosystem rules and the project’s own CI
  • The sandbox, and the time-filtered mirror
  • The optional model, off unless you name a provider
  • trigon-registry
  • trigon-sandbox
  • trigon-mirror
  • trigon-ai

no network client, no model code

The judgement half

  • Archive readers, and hand-written tar, zip and gzip writers
  • The stabilizers and their risk tiers
  • The comparison: one pass, six digests
  • Signing and verification
  • trigon-core
  • trigon-archive
  • trigon-stabilize
  • trigon-compare
  • trigon-attest

The judgement half links no network client, no async runtime and no model code. cargo run -p xtask -- policy fails the build if that changes, and the verifier build, cargo build -p trigon --no-default-features, contains only this half. Its line coverage is 88.4% whether or not the tests that need a network run, so tests that open no socket reach every line the suite covers there.

Trigon signs in a separate step, on purpose: trigon rebuild runs a stranger’s build scripts, so the key belongs to another process. trigon attest reads the stored bytes back by hash and re-derives the verdict before it signs. Trigon publishes a verdict only after a second, independent attempt agrees: on another machine, or on the same machine starting cold where the repository sets same_host_confirmation = true, as the end-to-end script does.

One run, start to finish

Output from one run of scripts/evidence-e2e.sh pkg:npm/wrappy@1.0.2.

The script rebuilt pkg:npm/wrappy@1.0.2 at --egress mirror-only, confirmed it with a second, cold build, signed the verdict and published it to a local evidence repository. We ran the commands below against what it left in work/evidence-e2e/.

trigon verifyexit 0
$ trigon verify wrappy-1.0.2.tgz rebuilt-wrappy-1.0.2.tgz
✔ normalized

  format       tar+gzip
  stabilizers  tar-gzip (cadb3a863443…)

               upstream           rebuild
  raw          aff3730d91b7…      25633255e22d…      ≠
  container    fa8a7ca19526…      56837aed83af…      ≠
  stabilized   441b279b8b59…      441b279b8b59…      =

  containers differ as well as the framing

  applied
    gzip-meta-v2             metadata        1 entries
    tar-entry-order-v2       structural      4 entries
    tar-mode                 metadata        4 entries
    tar-owners               metadata        4 entries
    tar-time                 metadata        4 entries

  members  4 identical, 0 differ, 0 upstream-only, 0 rebuild-only
The published tarball against the rebuilt one. The script copied both out of the run’s store.
raw ≠
The published tarball and the rebuild differ as files.
container ≠
They still differ after decompression, so the difference goes deeper than gzip framing.
stabilized =
After the tar-gzip set ran on both sides, they are identical. Trigon takes three digests of each side because “the same tar in different gzip framing” and “a different tar” are different findings.
applied
Five stabilizers fired: the gzip header, the order of members, and the modes, owners and timestamps of four files. Each ships with Trigon and is no riskier than metadata, so the outcome is normalized. Structural, the tier of tar-entry-order-v2, ranks below metadata: it reorders or reframes without changing any file’s content.
members
All four members are identical. On a divergence, Trigon names the member that differs and exits 1.

The build behind that verdict could reach the mirror and nothing else. Its network transcript lists the 403 responses that crossed into the build, each with its URL and SHA-256: 178 index requests, 224 package tarballs from registry.npmjs.org and one Node.js toolchain download. The mirror withheld 3,695 versions that did not yet exist when the publisher released wrappy 1.0.2. The source was github.com/npm/wrappy at commit 71d91b6d, the commit npm’s own gitHead names.

A consumer who trusts the evidence repository looks the package up in a verified local clone:

trigon lookupexit 0
$ trigon lookup pkg:npm/wrappy@1.0.2
key       pkg:npm/wrappy@1.0.2

source    `e2e`, from
          …/evidence-e2e/evidence.toml;
          as of 1 leaves of `example.org/trigon-e2e-evidence`
checkpoint example.org/trigon-e2e-evidence 1 KnTTCmL6O1enzpv/eyZctnyfq+OFB3kSH/qyKfaO1QM=
answer    normalized — from 1 record(s) its log holds for it
record    sha256:489556d6f1e8a16f7e0d2db9a6614485bc8ec007dd573ad8accdedbcd5185ea9 at leaf 0 of `example.org/trigon-e2e-evidence`, current
subject   wrappy-1.0.2.tgz (aff3730d91b7b1e143822956d14608f563163cf11b9d0ae602df1fe1e430fdfb)
purl      pkg:npm/wrappy@1.0.2
predicate https://trigon.dev/equivalence/v2
claims    normalized
set       tar-gzip,
          sha256:cadb3a863443a703bcfb7bd35e08581cee2a2d54c78b661bd2c43e483120bdbb
run       1790790643-aff3730d, 2026-09-30T17:49:55Z to 2026-09-30T17:50:43Z
trigon    built by 0.0.0+git.f4f622e4f8774f2bef41ee715444788691125986.dirty, signed by
          0.0.0+git.f4f622e4f8774f2bef41ee715444788691125986.dirty
egress    mirror-only, attestable
derived   heuristic
falsify   trigon verify-attestation --lookup
          sha256:aff3730d91b7b1e143822956d14608f563163cf11b9d0ae602df1fe1e430fdfb
          --predicate https://trigon.dev/equivalence/v2 --origin
          example.org/trigon-e2e-evidence --rerun-comparison --upstream <file>
dispute   https://example.org/trigon-e2e-evidence/issues
evidence  not checked here: comparison, guardManifest, rebuiltArtifact,
          stabilizerSetManifest, stabilizerSetModule, strategy. A clone keeps
          `evidence/` out, and `verify-attestation --lookup` fetches what it
          re-derives the claim from

exit      0: every answer at or above the threshold
The published record, read from a clone after trigon evidence sync; we shortened the configuration path. The signature covers the stabilizer set, the egress tier, how Trigon derived the recipe, the command that would falsify it and where to dispute it. example.org is the placeholder origin the script gives its local repository.

Evidence anyone can check

Signed statements

trigon attest signs in-toto statements in DSSE envelopes, in a process that runs no build and opens no socket. It fetches both artifacts from the store by hash, recomputes the comparison, and refuses to sign if the answer differs from the run’s record. For a run the publication gate calls void (its guard tripped, it ran at open egress, or a stabilizer somebody wrote fired), it signs void/v1 and nothing else, which says why and never which way the comparison went. For any other run it signs equivalence/v2 or divergence/v2, beside rebuild/v1 and buildobservation/v1.

A repository with an append-only log

trigon publish asks the publication gate about each run and writes what it allows to a git repository: the signed records, the evidence needed to re-derive each one, an index, and an append-only log whose checkpoint trigon log sign signs. It makes one commit per publication and never force-pushes. Nobody edits a published record: to correct one, you publish a new record that supersedes it, and both stay in the log.

work/evidence-e2e/evidence-checkoutthe published repository
$ git log --oneline
91a2028 publish: 1 record, tree 0 → 1
96e9ee6 log init: example.org/trigon-e2e-evidence, tree 0

$ cat log/checkpoint
example.org/trigon-e2e-evidence
1
KnTTCmL6O1enzpv/eyZctnyfq+OFB3kSH/qyKfaO1QM=

— example.org/trigon-e2e-evidence Wzm6XaeHEQA4U5lfIy4YjaSbrN4LeCVc76VjHMq1TeSert5X6/HCV8JAZMrW63AuPuSqfdzdkGhFT5DAVenJM+wMCAE=

$ find . -path ./.git -prune -o -type f -print | sort
./evidence/sha256/94/53/94534b0c30d7cb4babd914554a2f6f7ff089812ab4b2f156e81f4e55e0a741f1
./evidence/sha256/a3/20/a3202e95bd1ad16c3477e69b294ecb9c42c129028e8a79a7a6fc8f8d7d247ca5
./evidence/sha256/bd/46/bd4688433cbcae27bf0f2f1c8527b1d9448ed9320127ddaceb23d6c2ba29d42a
./evidence/sha256/c2/bf/c2bfc22d0363c6b87316e3aad46081fd9935cea1bec2e8550b758bd4ba660884
./evidence/sha256/e5/a2/e5a2d4a4ca749ece8545bf332214fdc535c87a0e716e8890cce1d905a4f7652e
./index/pkg1/df/52/df5243a83ef7ccf4493e4d70a55ba3c17bf9588a03cfae9bd39fbbb9c6486bb9.json
./index/purl1/66/d7/66d7067b6b146e9c4c8a3f8c1fdc6b4bc7ab152181eea1de0371dd98851db07a.json
./index/sha1/b5/24/b5243d8f3ec1aa35f1364605bc0d1036e30ab69f.json
./index/sha256/af/f3/aff3730d91b7b1e143822956d14608f563163cf11b9d0ae602df1fe1e430fdfb.json
./index/sha512/97/84/9784a9fc346c7a8afdc0be84bd5dbe4ee427eb774c90f8d9feca7d5e48214c46d5f4a94f4b5c54b19deeeff2103b8c31b5c141e1b82940f45c477402bdeccf71.json
./keys/attestation.pub
./keys/log.vkey
./log/checkpoint
./log/tile/0/000.p/1
./log/tile/entries/000.p/1
./README.md
./records/48/95/489556d6f1e8a16f7e0d2db9a6614485bc8ec007dd573ad8accdedbcd5185ea9.json
The local evidence repository the end-to-end script published into: the log’s first commit, one publication, and a signed checkpoint over a tree of one leaf. A consumer can find the record by sha1, sha256, sha512 or purl, the keys a lockfile might hold it by, and under the package name alone, which lists every version. evidence/ also holds the stabilizer-set module the verdict names, so the claim can still be re-derived after Trigon stops carrying that set.

Checking a lockfile

A consumer trusts a repository once, by pinning its log key and its attestation key. trigon evidence sync verifies each clone whole, and refuses a checkpoint that does not extend the one it accepted before. After that, trigon check answers every package in a lockfile from the clone, with no request per package, so no server learns which packages you asked about. It reads package-lock.json, npm-shrinkwrap.json, requirements.txt and SPDX JSON.

trigon checkexit 0
$ trigon check package-lock.json
package-lock.json · 1 package(s) · 1 source(s) · threshold at least normalized_with_caveats
source    `e2e`, from
          …/evidence-e2e/evidence.toml;
          as of 1 leaves of `example.org/trigon-e2e-evidence`
checkpoint example.org/trigon-e2e-evidence 1 KnTTCmL6O1enzpv/eyZctnyfq+OFB3kSH/qyKfaO1QM=

  ✔ normalized                   1   ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓
  ✖ divergent                    0   ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
  ? never checked                0   ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░

  `never checked` is a package no source that answered holds a record for, and `unknown`
  one no source could answer for at all. Neither is a statement about the package, and
  neither is summed with the verdicts.

exit      0: every answer at or above the threshold
The script’s lockfile, which names wrappy 1.0.2 by the sha512 integrity npm records for it.

A package no source holds a record for reads never checked, and never counts as a pass. The exit codes are for CI:

0
every package is at or above the threshold
1
a divergence
2
a package never checked, or withdrawn
3
a void, or a result below the threshold you set with --min or --max-risk
4
a deleted record, a record or source that failed verification, a required source that is unknown, or a package no source could answer
5
Trigon could not run the check

Checking a verdict yourself

Each published verdict carries the command that would falsify it. Run that command with the two artifacts and it re-derives the verdict from the bytes, then checks the published comparison report against it, member by member. If someone changes one byte of a record, verification fails, because no leaf of the log names it.

trigon verify-attestationexit 0, then exit 4
$ trigon verify-attestation --lookup sha256:aff3730d91b7b1e143822956d14608f563163cf11b9d0ae602df1fe1e430fdfb \
    --predicate https://trigon.dev/equivalence/v2 --origin example.org/trigon-e2e-evidence \
    --rerun-comparison --upstream wrappy-1.0.2.tgz --rebuild rebuilt-wrappy-1.0.2.tgz
…
signature verified under 5eccadc90c9ef3f4, the source's attestation key at its leaf
claims    normalized
…
current   yes: nothing the log holds supersedes it
answer    normalized — what this source says of the artifact now, from 1 record(s) the log holds for it
rederived normalized under tar-gzip@cadb3a863443 — the claim holds
report    the published comparison report agrees with the re-derivation on all it was held to: its outcome, set, digests, differences, passes, member counts, every member, and its field edits
          not held to it, so unchecked: `diff.progression`, `notes`, `the members' raw paths`

$ trigon verify-attestation --record record-tampered.json --source e2e
…
record    sha256:0cdbe5802a8d61cce1a0b0ae3b273fdb558d17dc8793a11654a9603d78fcc3c8
verified  NO — the record failed verification: no leaf of the log names the record sha256:0cdbe5802a8d61cce1a0b0ae3b273fdb558d17dc8793a11654a9603d78fcc3c8: it is unlogged, and a record the log does not hold is never shown as a verdict (docs/19 §8)
$ echo $?
4
The record’s own falsifying command, then the copy of the record the script changed by one byte. We omitted the lines marked with an ellipsis.

--lookup reads a synced clone and fetches the evidence it re-derives from, so it belongs to the full build. The network-free verifier build carries eight commands, and checks a record with --record <file> and --evidence <dir> instead. Its dependency tree holds no tokio, reqwest or hyper:

shellfrom docs/using-trigon.md
$ cargo tree -p trigon --no-default-features --edges normal --prefix none | grep -Ei '^(tokio|reqwest|hyper) '
$ echo $?
1
grep matched nothing. scripts/cross-machine-verify.sh tests the same claim end to end. It clones fresh, builds into a target directory of its own, takes the network away where the kernel allows it, and requires the verifier to catch an overstated outcome, an edited payload and a substituted artifact.

Not built or not settled yet

  • No public evidence repository exists yet, and the client ships no default source. Both wait on one decision, the repository’s owner and namespace (docs/19 D3). Until then you configure a source yourself, such as the local repository the end-to-end script creates.
  • By default trigon publish refuses every divergence. A divergence is a public accusation, and notifying the maintainer has no channel until docs/19 D7 decides one. A repository that sets divergences = "feed" publishes each divergence with an entry in feed/divergences.atom.
  • Trigon has no witnessing yet (docs/19 phase 7b). Until witnesses cosign checkpoints, the log’s owner can rewrite the log, and only a client that kept an older checkpoint, or one comparing mirrors, will notice.
  • A statement carries no third-party time, so a verifier cannot tell when the signer signed it, and nothing limits what a stolen key can sign until the maintainers settle docs/19 D6 (threat model D24).
  • Trigon has no standalone npx trigon-check or uvx trigon-check client yet (docs/19 phase 9), and no published release. To check a lockfile today, you build Trigon from source.
  • scripts/evidence-spike.sh publishes against a scratch GitHub repository. Nobody has run it yet: it needs a repository the owner has yet to name.

Ecosystems and status

Trigon rebuilds four ecosystems today. Each rate below names the corpus it came from.

Ecosystems
ecosystem artifact rebuilds stabilizer set
npm.tgzyestar-gzip
PyPI.whlyeswheel
crates.io.crateyescrate
NuGet.nupkgyesnupkg
RubyGems.gemnot yetgem
GitHub releasesanynot yetby format

npm, PyPI, crates.io and NuGet have registry clients, so Trigon resolves, fetches and rebuilds their packages. The design covers RubyGems and GitHub releases, and they come next; until then Trigon refuses a target in either by name. trigon verify compares two files from any of them and chooses the stabilizer set from the file name. With the gem set it compares two .gem files, but Trigon cannot yet resolve or rebuild a gem. An npm tarball takes the generic tar-gzip set, because nothing inside a .tgz says whose it is; pass --profile npm-tarball to select the npm set.

Milestones
milestonestatewhat it covers
M0 the judgement half done Differential against the reference implementation: 34 match, 24 deviate by a declared entry, 0 unexplained.
M1 first rebuilds done npm and PyPI rebuild end to end, under an enforced egress tier, against a time-filtered index.
M2 attestations done Signed statements, re-derivable on another machine and through an archived stabilizer set run under wasmtime. Publishing them: the evidence repository, trigon publish, evidence, lookup and check.
M3 the search half begun The deterministic parts first: failure signatures, log compression, the repair-loop policy, the Builder.

Rates, with their corpus

The figures come from the M1 common-path corpus: 197 npm and 200 PyPI targets, which we stratified by build system rather than popularity and ran at --egress mirror-only. Reproduce counts the targets whose rebuild matched, out of those that reached a comparison. Reach counts the targets that got that far.

Reproduction and reach on the M1 common-path corpus
ecosystemreproducereach a comparison
npm100 of 132 (76%)132 of 197 (67%)
PyPI136 of 163 (83%)163 of 200 (81%)

npm’s 76% spans 88% for packages with no lifecycle script down to 33% for monorepo members, and 3 of the 17 monorepo members reach a comparison. PyPI’s 83% spans 100% for flit and hatchling down to 54% for maturin and C extensions.

npm, by stratum
stratumcomparedreproducedrate
no lifecycle script74 of 906588%
prepare/prepack32 of 602475%
TypeScript build23 of 301043%
monorepo member3 of 17133%
PyPI, by stratum
stratumcomparedreproducedrate
flit / hatchling47 of 5047100%
setuptools + pyproject49 of 604081%
setuptools + setup.py30 of 401860%
poetry-core26 of 302596%
maturin / C extension11 of 20654%

Sixteen of npm’s 65 targets that reached no comparison were our fault: a missing tool, a workspace: protocol npm does not speak, a workspace sibling the recipe did not build first. The reproduction rate leaves them out by design and the reach figure counts them, so read reach as a floor.

crates.io and NuGet have no rate, because neither has a stratified corpus yet. Both rebuild end to end at mirror-only. Of the twelve crates we have tried by hand, six reproduce, hashbrown@0.17.1 and serde@1.0.219 among them; each remaining divergence is the Cargo.toml manifest rewrite that backlog item B20 is about.

A rate describes the pipeline on one corpus. The strata are weighted by build system, not by how common each build system is, so neither total says how much of an ecosystem reproduces. docs/15-corpora.md argues why the strata matter more than the totals.

Get started

Rust 1.85 or later, edition 2024. Rebuilds also need podman.

Build it

# the full tool: target/release/trigon
cargo build --release -p trigon

Compare two files you already have

This runs offline, with no container. The exit code is 1 for divergent and 0 otherwise, so it drops into a script.

trigon verify upstream.tgz rebuild.tgz

Rebuild a package behind the mirror

Build the two helper images once: a base image to build in, from a parent pinned by digest, and the image the mirror runs from. At --egress mirror-only, --image auto uses a base image already on the machine and refuses to build one. --timewarp auto pins the mirror to the moment the publisher released the package, and --store keeps the run so a separate process can sign it.

trigon base-image --from docker.io/library/debian@sha256:88200866dfff7ea7f5cbcb6ec7c8a701889efe6fe859fe64d6990e4b07ea4171
trigon mirror-image
trigon rebuild pkg:npm/wrappy@1.0.2 --image auto --egress mirror-only --timewarp auto \
    --work ./work --store ./store

Run the whole loop

scripts/evidence-e2e.sh creates keys and a local evidence repository, rebuilds the package you name behind the mirror, confirms it with a second cold build, publishes the verdict, then checks it the way a consumer would, including two checks that must fail. The package has no default: pkg:npm/wrappy@1.0.2 is small and reproduces behind the mirror. The script needs the base image from the step above, and builds the mirror image itself if it is missing. It takes a few minutes, needs podman and the network the mirror fetches through, and keeps everything it makes in work/evidence-e2e/.

cargo build -p trigon && scripts/evidence-e2e.sh pkg:npm/wrappy@1.0.2

Build the verifier

For checking somebody else’s claim. It writes the same target/release/trigon as the full build, so give it a target directory of its own, as scripts/cross-machine-verify.sh does.

CARGO_TARGET_DIR=target/verifier cargo build --release -p trigon --no-default-features

Then read Using Trigon, the task-oriented guide, starting with its section on what a verdict does not tell you.