How the pieces fit

Ten artifacts, one trust boundary. This page is the map: what each piece does, what depends on what, and — the part that matters — which side of the boundary each one sits on.

The trust boundary

  UNTRUSTED                          |  TRUSTED (and small enough to read)
  ---------------------------------- | ----------------------------------
  whatever produced the certificate  |  certkit          ~30 lines of arithmetic
    a solver, an LP, a model, you    |  certkit-js       the same, written separately
                                     |
  the specification                  |  <- NOT trusted by the checker, but not
    (a human wrote these relations)  |     checked by it either. See below.

The checker trusts nothing the certificate says: it rebuilds every obligation from the specification and ignores any atoms the certificate carries. What it cannot do is tell you the specification describes your program. That is the one assumption the whole system rests on, and it is why specs are kept to a handful of readable relations.

Dependency direction

  certkit  (no dependencies, standard library only)
     ^
     |-- exploit-counter   uses certkit's atom type; adds exact counting
     |        ^
     |        |
     |-- crs-mcp           wraps both, exposes them to agents
     |
     |-- certkit-action    runs the CLI in a workflow
     |
  soundnessbench           deliberately does NOT import certkit
                           (it has to be able to grade certkit)

  certkit-js               shares NO code with certkit, by construction

The two non-arrows are the interesting ones. soundnessbench computes its ground truth by its own exhaustive enumeration, with its own atom representation, because a benchmark that imported the tool it grades would be measuring agreement rather than correctness. certkit-js is a separate implementation for the same reason: two programs that share no code agreeing on 246 vectors is evidence; one program agreeing with itself is not.

Inside certkit

ModuleRoleTrusted?
atoms.pyThe one canonical relation shape: sum(c*v) + k <= 0. Exact rationals.yes
farkas.pyThe refutation check itself. No search, no solver, no floats.yes — this is the core
cert.pyReconstruction, fingerprint binding, the three verdicts.yes
report.pyEvery output format from one place, so a refusal is worded identically everywhere.presentation
schemas/JSON Schema for both formats, so other tools can emit them.data
smtlib.pyExport is total; import is deliberately partial and refuses by name.bridge
scaffold.py, explain.pyAuthoring and prose. Never change a verdict.convenience
lsp.py, notebook.py, precommit.pyFrontends. Each re-uses the same verdict; none of them softens it.frontend

The rule for everything below cert.py: a frontend may change how a verdict is displayed and may never change what it is. That is enforced by tests rather than by convention — the notebook renderer, the SARIF output, the JUnit output, the LSP diagnostics and the pre-commit hook each have a test asserting that UNVERIFIED does not render as a pass.

Where the expensive work happens

Checking is linear in the size of the obligation and takes microseconds. The costly parts of the toolkit are elsewhere, and each is capped rather than allowed to degrade:

Caches, and why they cannot go stale

Two caches ship as committed data files: the benchmark's ground truth, and the minimal-break offsets its hard split needs. Both are keyed by a hash of the relations they answer. Editing a task changes the key, misses the cache, and recomputes. Staleness is not detected — it is unreachable, which is a different and much stronger property.

Every other cache in the toolkit is in-process and keyed by input: the suite memo, and the schema loader. A test asserts that a mutated task produces a different key, so the property is checked rather than asserted.

The three data artifacts

Each is generated by a committed script, and CI regenerates and compares rather than trusting the file.