Audit
Reasoning audits
Every specific in the agent's written rationale, checked against the rows that run actually served. A figure, a name, a date or an event that appears in none of them is quoted back with its position and scored.
BUILDING
The package is pit-audit, Apache-2.0, Python 3.11 or
newer, with no runtime dependencies. It installs from the repository
and runs offline; metered pricing per decision audited is not live
yet.
- pip
- pit-audit run
- CLI · Python
- exit 1 on unsupported
The artifact
A figure that appears in no row the run served
Our own test agent, walking the SVB window over four sessions in
March 2023, wrote that the 8-K announced “an urgent $2.25B
capital raise”. The rows it had been served that session were
filing metadata, whose entire content is strings like
SVB FINANCIAL GROUP 8-K. The figure is correct and it
came out of the model's training data, since it appears in nothing
the run served. The trajectory ships with the package, so
pit-audit calibrate reproduces this on your machine
with no key and no model.
unsupported: 9 flagged of 12 claims over 5 rationales (5 at or above entity), worst number 2023-03-08 main/pit number[number] unmatched '$2.25B' 2023-03-08 main/pit number[number] unmatched '$2.25 billion' 2023-03-08 main/pit event[entity] unmatched 'Form 8-K (2023-03-08) announcing large …' 2023-03-07 main/pit vague[vague] unmatched 'The recent 10-K highlights mounting …' cell main/pit: unsupported, 6 flagged, worst number cell redacted/pit: grounded, 1 flagged, worst vague cell shifted/pit: grounded, 2 flagged, worst vague
The finding is the part that makes the verdict checkable rather than asserted. It reports how many served values were considered, names the nearest one, and gives the gap:
{
"claim_id": "940eadc8ea9c109c",
"context": "Form 8-K (2023-03-08) announcing large realized portfolio losses and an urgent $2.25B capital raise",
"decision": {
"arm": "main",
"clock": "pit",
"known_at": "2023-03-08T23:59:59Z",
"seq": 9,
"session_date": "2023-03-08"
},
"detail": {
"quantity": {
"currency": "USD",
"kind": "money",
"raw": "2.25",
"scale": "b",
"value": 2250000000.0
}
},
"end": 85,
"extractor": "rules",
"field": "meta.key_signal",
"flagged": true,
"kind": "number",
"match": {
"candidates_considered": 966,
"claimed_value": 2250000000.0,
"matched": false,
"nearest_served": {
"absolute_gap": 405447766.0,
"raw": "1844552234",
"relative_gap": 0.180199,
"value": 1844552234.0
},
"rung": "none"
},
"quote": "$2.25B",
"severity": "number",
"start": 79,
"unmatched": true
}
966 candidate values were considered for that cell. The closest was
1,844,552,234, a magnitude derived from an accession number, 18.0%
away and outside the loosest 5% rung. The redacted and shifted arms
read the same 86 rows with the issuer removed and with every date
moved ten years forward, and neither produced an unmatched number or
entity; their findings are all at vague.
The failure it prevents
An agent that remembers the answer and calls it analysis
Control arms bound contamination at the level of a score. They tell you the main arm beat the shifted arm by some margin. They do not tell you which sentence did it, and a team reading a leaderboard has nothing to point at.
A score gap is an aggregate
It moves for many reasons and averages over every decision in the run. A single fabricated figure can carry a whole arm and leave the delta looking ordinary.
A quoted span is evidence
The finding carries the claim verbatim, the sentence it sat in, character offsets into the field it came from, and the arm, clock and session it was written at, which is enough to go and read the original.
How it works
Extract with a model if you like, match without one
The served set is rebuilt from the trajectory
The set a claim is checked against is rebuilt from the trajectory's
recorded tool results, prompt text and probe items. It is
cumulative and per cell: a claim made on the 2023-03-08 session in
main on the PIT clock is checked against everything
that cell had been served by 2023-03-08, never against a row the
run revealed on 2023-03-10 and never against what
redacted saw. Prompts count as served, because a
prompt states the session date and an agent quoting it back is
quoting something it was handed.
Matching runs strict to generous, and reports where it stopped
| Rung | Clears when |
|---|---|
exact | the claimed magnitude equals a served magnitude |
rounded | they differ by less than half the last digit either was written to, in the units it was written in |
scale_shift | they differ by a factor of 100 up to 1e12 within 0.5% |
within_0p5pct | the relative gap is 0.5% or less |
within_2pct | the relative gap is 2% or less |
within_5pct | the relative gap is 5% or less |
The loose rungs are there because a model writing from thin context reconstructs figures it half-remembers rather than copying served bytes (arXiv 2504.14765). An exact-string matcher would report ordinary paraphrase as fabrication, and the fabrications that matter would sit underneath the noise. The rung that cleared a claim is written into the finding, so a reader who wants a stricter audit can see which claims cleared only on the loose rungs, and pass a shorter ladder.
A percentage is only ever compared with a percentage. Entities
match on alias forms, so SVB clears against a served
SVB FINANCIAL GROUP, and the alias table ships empty
because an alias asserts something the trajectory does not.
Severity, and what fails a build
Claims are typed number, entity,
date, event or vague, and
severity runs number above entity above
date above vague. The verdict is
unsupported when the count of flagged findings at or
above --severity (default entity) exceeds
--max-flagged (default 0). run exits 1 on
unsupported, so a CI gate can fail on it.
Nothing is averaged across cells. A control arm answering “only the filing metadata is knowable” scores badly against the filing metadata, and a run-level number would blend its hedging into the main arm's fabrications.
The anachronism check
| Kind | Fires when | Evidence |
|---|---|---|
future_date | the rationale writes a date later than the decision instant | the rationale's own words |
refused_row | the rationale quotes a row key a tool refused as not_yet_knowable | the trajectory's own refusal record |
future_event | the rationale names a term in a supplied event calendar dated after the decision instant | the calendar entry's source string |
The first two need nothing external. The event calendar ships empty and is never inferred, and the report records its name, provenance and entry count so a reader can see what the check ran against.
Integration
Point it at a trajectory
$ cd audit && uv venv && uv pip install -e '.[dev]' $ pit-audit run --trajectory trajectory.jsonl \ --out findings.json --certificate audit.json $ pit-audit calibrate # grade the extractor on nine findings a person made
--certificate audit.json writes the document that fills
the reasoning-audit slot in a
contamination certificate.
pit_harness.certify owns that block and recomputes its
counts from the findings rather than copying them, so a certificate
cannot state a total its own findings do not support.
Extraction can be model-assisted, and matching never is. A model chooses spans; typing, matching and severity are computed here. Every returned quote must be a verbatim substring of the rationale or it is dropped and counted, and a provider error falls back to the rule extractor and is counted too.
What it measures, and what it does not
- A cleared rung means the run served a close-enough value. Whether that row was right, and whether the agent read it correctly, are outside what this checks.
- Vague findings are noisy by construction. An agent stating what it cannot see scores badly against what it was shown, so those findings are reported and left below the default threshold rather than dropped.
-
A run with no written rationale cannot be audited. The report says
so in
scope.decisions_with_rationalerather than reading as clean. - Contamination that never reached the written text does not show up here. The control arms and the positive-control probe cover that.
Related
What sits either side of it
Alongside
Benchmark Datasets
The redacted and shifted arms whose rationales this compares against the main one.