# Research Contract: five rules and a five-check quick check (free sample)

Version 1.0, 2026-10-28. By PaperChaseWebb. For anyone who asks Grok Bot, Claude Code, Codex or Hermes to research something before a decision. One Markdown file and one Python 3 script, standard library only. The script was tested on its worked example (5 findings, then 0) and on a missing file (exit 2). It was not run inside any agent harness.

Agents are fast at research and bad at saying where an answer came from. The usual failures are familiar: a link that was never opened, a quote that is really a paraphrase, a listicle treated as proof, a confident number with no date, and a gap filled with a guess instead of the word unknown. This sample gives you the five rules that stop most of that and a script that checks them on one file.

## Install in one line

Save this page as FREE-SAMPLE.md, then:

```
python3 -c "import re;t=open('FREE-SAMPLE.md').read();open('contract_mini.py','w').write(re.search(r'~~~python\n(.*?)~~~',t,re.S).group(1))"
python3 contract_mini.py research.json
```

## Five rules

1. **Every claim names its source.** A claim with no source is not a finding. Its verdict is unknown, and that is an honest answer.
2. **Quote word for word, with a location.** Page, section, paragraph, table row or timestamp. If the exact words are not in the source, it is a paraphrase and must be labeled as one, or dropped.
3. **High confidence needs a primary source.** The maker's docs, a filing, a dataset, a changelog, a transcript. A blog post about a blog post is a lead, not proof.
4. **Every source carries two dates.** When it was published (or unknown) and when you read it. Facts about prices, limits, laws and versions go stale; the dates tell the reader how stale.
5. **The owner says yes before anything is published or acted on.** The full pack ties that yes to the sha256 of the report, so an edit after approval voids it.

## The five checks

| Id | Check |
|---|---|
| M1 | every cited source exists in the file |
| M2 | every quote is word for word in the source excerpt and has a location |
| M3 | high confidence rests on a primary source |
| M4 | every source has a published date (or unknown) and an accessed date |
| M5 | a claim with no source has the verdict unknown |

Run `python3 contract_mini.py research.json`. Exit 0 means clean, 1 means findings, 2 means the file is missing or unreadable. It writes nothing and opens no network connection.

## Worked example

The company and pages are made up. A first draft from an agent (example/mini/before.json):

```json
{
 "question": "Does the Relaybox v2 Team plan allow enough requests for our order emails?",
 "as_of": "2026-10-28",
 "sources": [
  {"id": "S1", "url": "https://example.org/relaybox/docs/v2/limits", "tier": "primary", "published": "2026-10-02", "accessed": "2026-10-27", "excerpt": "| Team | 600 requests per minute |"},
  {"id": "S2", "url": "https://example.com/top-email-apis-2026", "tier": "tertiary", "published": "", "accessed": "2026-10-27", "excerpt": "Relaybox v2 is 40% faster than v1, according to our tests."}
 ],
 "claims": [
  {"id": "C1", "claim": "The v2 Team plan allows 600 requests per minute.", "verdict": "supported", "confidence": "high", "sources": ["S1"]},
  {"id": "C2", "claim": "v2 is 40% faster than v1.", "verdict": "supported", "confidence": "high", "sources": ["S2"]},
  {"id": "C3", "claim": "Relaybox has a 99.99% uptime promise on Team.", "verdict": "supported", "confidence": "medium", "sources": ["S7"]},
  {"id": "C4", "claim": "Most teams never hit the limit.", "verdict": "supported", "confidence": "low", "sources": []}
 ],
 "quotes": [
  {"claim": "C1", "source": "S1", "quote": "Team plans get 600 requests a minute", "location": "rate limits table"}
 ]
}
```

```
$ python3 contract_mini.py before.json
M3 C2 is high confidence with no primary source
M1 C3 cites S7, which is not in sources
M5 C4 has no source, so its verdict must be unknown
M2 quote for C1 is not word for word in S1, or has no location
M4 S2 needs a published date (or unknown) and an accessed date
5 checks, 5 findings
```

The fixes: the paraphrased quote was replaced with the exact table row and its location. The listicle claim was moved to unknown, since a tertiary source cannot carry it. The uptime claim cited a source that was never collected, so it became unknown with no source. The unsourced "most teams" claim became unknown. The listicle got published: unknown.

```
$ python3 contract_mini.py after.json
5 checks, 0 findings
```

Three of the four claims are now unknown. That is the point: the report says what is known, and the rest is listed as open instead of dressed up.

## One prompt to try

```
Research this question: <question>. Write research.json with sources (id, url, tier primary/secondary/tertiary, published or "unknown", accessed, excerpt with the exact text you read), claims (id, claim, verdict supported/partly/contradicted/unknown, confidence high/medium/low/none, sources) and quotes (claim, source, exact quote, location). Cite only pages you opened in this session. Never write a citation from memory. If you found nothing, the verdict is unknown. Then run python3 contract_mini.py research.json and fix every finding before you show me the answer.
```

## What the full pack adds

Research Contract for Agents (pp-research-contract, $19) is the standing contract for every research run: claims registered before testing, saved snapshots of every source read, a fifteen-check read-only checker, contradiction and unknown sections the checker enforces, freshness rules for time-sensitive claims, a separate auditor, and an owner approval tied to the report hash. It comes with a skill file for Claude Code, Codex and Hermes, an AGENTS.md snippet, a Grok Bot rules block, templates, ten prompts and the tests. For checking one claim at a time with verdicts and arithmetic, see Check the Claim (expanded edition) (pp-check-the-claim) and its free sample (pp-check-the-claim-sample).

## The script

~~~python
#!/usr/bin/env python3
"""contract_mini.py: five checks from the Research Contract on one research.json file.
Usage: python3 contract_mini.py research.json   Exit 0 clean, 1 findings, 2 missing or unreadable file."""
import datetime as dt, json, sys

def d(s):
    try: dt.date.fromisoformat(s); return True
    except Exception: return False

def n(t):
    return " ".join(str(t).replace("\u2019", "'").replace("\u201c", '"').replace("\u201d", '"').split())

def main(path):
    try:
        r = json.load(open(path, encoding="utf-8"))
    except Exception as e:
        print(f"cannot read {path}: {e}", file=sys.stderr); return 2
    S = {s["id"]: s for s in r.get("sources", [])}
    out = []
    for c in r.get("claims", []):
        for s in c.get("sources", []):
            if s not in S: out.append(f"M1 {c['id']} cites {s}, which is not in sources")
        if c.get("confidence") == "high" and not any(S.get(s, {}).get("tier") == "primary" for s in c.get("sources", [])):
            out.append(f"M3 {c['id']} is high confidence with no primary source")
        if not c.get("sources") and c.get("verdict") != "unknown":
            out.append(f"M5 {c['id']} has no source, so its verdict must be unknown")
    for q in r.get("quotes", []):
        s = S.get(q.get("source"), {})
        if not q.get("location") or not q.get("quote") or n(q["quote"]) not in n(s.get("excerpt", "")):
            out.append(f"M2 quote for {q.get('claim')} is not word for word in {q.get('source')}, or has no location")
    for s in S.values():
        if not (s.get("published") == "unknown" or d(s.get("published", ""))) or not d(s.get("accessed", "")):
            out.append(f"M4 {s['id']} needs a published date (or unknown) and an accessed date")
    for line in out: print(line)
    print(f"5 checks, {len(out)} findings")
    return 1 if out else 0

if __name__ == "__main__":
    sys.exit(main(sys.argv[1]) if len(sys.argv) > 1 else 2)
~~~

PaperChaseWebb 💸
