Metadata-Version: 2.4
Name: charactercheck
Version: 0.6.2
Summary: Deterministic D&D Beyond character-sheet derivation with provenance — every stat computed and traceable, every unhandled case named, never guessed.
Author: chaoz23
License: MIT
Project-URL: Homepage, https://github.com/chaoz23/charactercheck
Keywords: dnd,dndbeyond,character-sheet,ttrpg,agents,mcp,deterministic
Classifier: Programming Language :: Python :: 3
Classifier: License :: OSI Approved :: MIT License
Classifier: Topic :: Games/Entertainment :: Role-Playing
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
License-File: NOTICE
Dynamic: license-file

# charactercheck

**Deterministic D&D Beyond character-sheet derivation with provenance — every stat computed and traceable, every unhandled case named, never guessed.**

The D&D Beyond API returns *build data* — there is no computed AC, attack bonus, or save anywhere in the payload. Everyone who consumes it re-derives the math, usually inside a host app (a VTT module, a browser extension) or, worse, by letting a language model guess. charactercheck is that derivation as a **standalone, dependency-free library and CLI**: the character accountant for agents.

> **Cold-boot probe (2026-07-24):** a fresh agent session given only this repo URL derived a live character correctly in **2 commands, zero failures** (install → derive), ~seconds end-to-end.
>
> **Status: 0.1 — young but real.** The derivation surface is the [100-question QA pass](#the-qa-pass) below, run in CI on synthetic fixtures. Unrecognized data is *reported, never silently defaulted* — that honesty contract is the product.

> **Agent?** → **[AGENTS.md](AGENTS.md)** is written for you: the six things
> that matter, in one page. Everything below is the same story with more prose.

## Start here (agent or human): 2 commands

```console
$ pip install charactercheck                                   # stdlib only, no dependencies
$ charactercheck derive https://www.dndbeyond.com/characters/<id>
```

> **`pip install` blocked by an "externally-managed-environment" error?** That
> is Homebrew/Debian Python protecting the system site-packages, not a problem
> with this package. Use a venv — `python3 -m venv .venv && .venv/bin/pip
> install charactercheck` — or skip installing entirely and use the clone path
> below. There are no dependencies either way.

**Want a real character to try right now?** This one is public and kept that
way on purpose:

```console
$ charactercheck derive https://www.dndbeyond.com/characters/150991647 --brief
Shalia — Cleric 3
  AC 12 (confirm) · HP 21/21 · init -1
  trusted: hp, initiative, saves, skills, attacks, weapons, speeds, proficiency_bonus, spellcasting, spell_save_dc, spell_attack_bonus, inventory
  ASK: ac, prepared_spells, spell_slots
  UNSUPPORTED: spell_output (bonus:spell-group-healing)
    ? Which armour are you actually wearing right now?
    ? Your sheet shows no spell slots, but a caster of your level should have 4×L1, 2×L2. How many do you have?
    ? Which leveled spells do you have prepared today?
```

She is a deliberately *useful* example rather than a tidy one: a real sheet
from a running campaign that exercises **all three trust lanes at once** —
armour not flagged equipped, spell slots missing from the payload, no prepared
list, and one unsupported healing-group modifier. A clean character would show
you nothing.

> **The example deliberately includes a negative case.** Shalia carries one
> homebrew feat (`Dark Bargain`) that is **not** in the SRD 5.2.1 feat table,
> and that is the point — it is what produces the `unsupported` lane in the
> output above. A tidy all-SRD character would demonstrate the happy path and
> nothing else, whereas real sheets are full of homebrew, legacy options and
> manual overrides. This one shows the honesty machinery doing its actual job:
> naming content it does not model, scoping the blast radius to
> `spell_output`, and leaving the other twelve families trustworthy.
>
> charactercheck categorises it as *"outside SRD 5.2.1 feat table"*; the
> sibling [srdcheck](https://github.com/chaoz23/srdcheck) independently refuses
> it as out of jurisdiction. Two tools agreeing from different directions.


**First, prove it works — offline, no account, no character of your own:**

```console
$ charactercheck selftest
  [PASS] name: 'Torvald Brightmantle'
  [PASS] level: 3
  [PASS] abilities derived
  ...
  offline derivation works — no network, no D&D Beyond account, no character of your own required.
```

That one command separates *"the tool is broken"* from *"I cannot reach that
character"* — two failures that look identical from outside and are the two you
will actually hit. There is a sample character in the box
(`examples/sample-character.json`) you can derive directly too.

**Cloned the repo instead of installing?** That works too, with no install step
— there are no dependencies to install:

```console
$ git clone https://github.com/chaoz23/charactercheck && cd charactercheck
$ python3 -m charactercheck derive https://www.dndbeyond.com/characters/<id>
```

That is the whole happy path. Works on any **public** D&D Beyond character —
URL, bare id, or a saved character-service JSON file. No login, no cookies, no
API key, ever.

### If it does not work, read the exit code

Every failure prints **structured JSON with an `action` field** — the one next
thing to do. **Never a traceback.**

| exit | meaning | what to do |
|---|---|---|
| **0** | derived clean | use the output |
| **1** | lint findings — the sheet disagrees with itself | usable output; resolve `lint[]` with the player |
| **2** | unhandled content present | **not a failure.** Output is complete and usable; resolve the named `unhandled` items with a human. Don't retry |
| **3** | **could not retrieve the sheet** | read `action` in the JSON — private sheet, bad id, or no network |

```console
$ charactercheck derive https://www.dndbeyond.com/characters/1
{
 "ok": false,
 "error": "not_public",
 "message": "D&D Beyond returned 403 Forbidden for this character — it is private or not shared.",
 "action": "Open the character on D&D Beyond, set Character Privacy to Public, and retry. If you cannot change it, save the character-service JSON and pass that file path instead — charactercheck reads a saved file with no permissions at all, and never asks for credentials.",
 "exit_code": 3
}
```

Error kinds are stable and matchable: `not_public` · `not_found` · `bad_ref` ·
`network` · `rate_limited` · `bad_json` · `upstream`.

### Stuck? One command diagnoses it

```console
$ charactercheck doctor <ref>          # add --json for machine output
  [PASS] python: 3.12.1
  [PASS] dns: character-service.dndbeyond.com resolves
  [PASS] network: outbound HTTPS works
  [FAIL] character: not_public: D&D Beyond returned 403 Forbidden ...
         -> Open the character on D&D Beyond, set Character Privacy to Public ...
```

### Private character? Two supported answers

charactercheck **never asks for credentials** — that is a deliberate boundary,
not a gap. So:

1. **Make it public.** Character sheet → *Character Privacy* → Public. This is
   the usual answer and takes ten seconds.
2. **Or work from a file.** Save the character-service JSON anywhere you can
   read it and pass the path: `charactercheck derive ./shalia.json`. No
   permissions involved at all — useful for private campaigns, air-gapped
   hosts, and CI.

## The trust map — read this before you state a number

`derive` returns a `trust` block that routes every family into exactly one
lane. It is a re-shaping of `verified_clean`, `lint` and `unhandled` into the
one question a caller actually has: **may I say this out loud?**

```console
$ charactercheck derive <ref> --brief
Shalia — Cleric 3
  AC 12 · HP 21/21 · init -1
  trusted: hp, initiative, saves, skills, attacks, weapons, speeds, proficiency_bonus, spellcasting, spell_save_dc, spell_attack_bonus, inventory
  ASK: ac, prepared_spells, spell_slots
  UNSUPPORTED: spell_output (bonus:spell-group-healing)
    ? Which armour are you actually wearing right now?
    ? Your sheet shows no spell slots, but a caster of your level should have 4×L1, 2×L2. How many do you have?
    ? Which leveled spells do you have prepared today?
```

| lane | meaning |
|---|---|
| **trusted** | nothing outstanding touches it — safe to state |
| **ask_player** | derived, but a lint puts it in doubt; **the resolving question is right there** |
| **unsupported** | the engine saw content it does not model targeting this family — say what is missing rather than stating a value |

Unsupported content is **never applied** to any derived value, so the rest of
the arithmetic is unaffected by it. Trust the computed fields; do not improvise
around the named unsupported feature.

**Every lint finding carries the question that resolves it** — the difference
between a tool reporting a caveat and an agent resolving one at the table.

```console
$ charactercheck intake <ref>     # one pre-session packet: settled, unresolved, player-authority
```

> **Breaking in 0.6.0:** `lint` entries are now objects
> (`{code, message, ask, affects}`) rather than plain strings. Join `message`
> for the old rendering.

## What a derived character looks like

```console
$ charactercheck derive https://www.dndbeyond.com/characters/<id>
{
 "combat": {
  "ac": {"value": 16, "provenance": "Breastplate 14 + DEX +1 + +1 [manual adjustment]"},
  "initiative": {"bonus": 6, "provenance": "DEX +1 + 5 [bonus:initiative]"},
  "hp": {"current": 51, "max": 51, "provenance": "base 30 + CON +2×7 + 7 [per-level bonuses]"},
  "stance": {"main_hand": "Night Rapier", "off_hand": "Boot Knife (off hand)",
             "ac_states": {"current": 16,
                           "shield raised (+2)": {"ac": 18, "cost": "requires the off hand"}}},
  ...
 },
 "unhandled": {"modifier_patterns": ["munch:cookies"]},
 "lint": []
}
```

## For agents

- **`tool.json`** at the repo root and **`charactercheck --schema`** describe the full I/O contract.
- **`--pipe`** reads refs from stdin for batch runs.
- **MCP server**: `charactercheck-mcp` (stdio) exposes `derive`, `stance`, `qa`, `report`.
- Every derived number carries a **provenance string** — the arithmetic that produced it — so a downstream agent (or a suspicious player) can audit any value without re-deriving it.

```console
$ charactercheck stance <ref>    # what's in each hand, AC states with costs
$ charactercheck seatpack <ref>  # everything a seat needs before a session
$ charactercheck report <ref>    # ONLY the honesty lanes — resolve these before play
$ charactercheck qa <ref>        # the 100-question pass, per-question OK/PARTIAL/NO
$ charactercheck diff <ref> --baseline intake.json   # what the player changed mid-session
```

> **Why this matters, from a real table:** an agent was handed a D&D Beyond
> link and reported *"D&D Beyond is showing me the signed-out shell, not the
> sheet stats"* — then played a whole session rolling flat d20s with no
> modifiers, while the GM hand-derived her numbers and **got them wrong twice**
> (a throwaway script matched `wis-score`; D&D Beyond calls it
> `wisdom-score`, silently dropping a feat's +2 WIS). `charactercheck derive`
> gets that sheet right in one command. Hand-derivation is the bug this product
> exists to delete.

## Why provenance and refusal, not just numbers

A character sheet is a mix of derivable core-rules content and everything else — homebrew, legacy-edition options, manual overrides someone typed in three campaigns ago. Tools that guess produce confident wrong numbers; at a real table those become wrong rulings. charactercheck's contract:

1. **Derived** values carry their arithmetic (`AC 17 = Breastplate 14 [equipped] + DEX +1 [medium cap] + 2 [manual adjustment]`).
2. **Unhandled** data is surfaced by name (an unknown modifier pattern, an unrecognized characterValue type) and flips the exit code — your cue to ask the player, not to guess.
3. **Lint** catches sheets that disagree with themselves — including **slot
   gaps caught by an independent anchor**: the SRD multiclass table says how
   many slots a caster level *must* have, so a Cleric 3 reporting none is a
   data gap, and a sheet reporting `available 0` while `used 3` is a
   contradiction (those slots were spent, so they existed). Also: nothing flagged equipped, stale damage, a caster with slots and zero prepared spells, gear stashed in a container that was left somewhere else entirely (yes, the container graph is modeled — a chest labeled "stashed @ the docks" stops contributing weight and armor candidates).

## The QA pass

`tests/` ships a 100-question QA suite covering the surface a table actually uses — vitals, saves, all 18 skills, passives, weapons and masteries, spell slots (pact included), resources with used-counts, encumbrance, attunement. CI runs it on synthetic fixtures on every push; the scorecard is generated, never hand-edited. Current: **92 OK / 7 PARTIAL / 1 NO** per fixture-class, with every PARTIAL/NO carrying a named reason in the output.

## What it does NOT do (on purpose)

- **No rules adjudication** — "is this action legal" is a different product ([srdcheck](https://github.com/chaoz23/srdcheck), this project's sibling: srdcheck judges actions, charactercheck derives the actor).
- **No private sheets** — public share links only; this tool will never ask for credentials.
- **No VTT output, no homebrew content database, no character building.**
- **No guessing** — the whole point.

## As a library

```python
from charactercheck import derive, stance, fetch

r = derive("https://www.dndbeyond.com/characters/<id>")
r["combat"]["ac"]              # {'value': 17, 'provenance': 'Breastplate 14 + ...'}
r["unhandled"]                 # what you must resolve by asking a human
stance(fetch("<id>"))          # hands / AC states / attack lines
```

## Credits

- Schema semantics for the D&D Beyond v5 payload were partly informed by reading the source of [MrPrimate/ddb-importer](https://github.com/MrPrimate/ddb-importer) (MIT) — the most complete derivation math in the ecosystem, coupled to FoundryVTT. No code was copied; see `NOTICE`.
- The 100-question QA schema was authored for this project.
- D&D Beyond is a trademark of Wizards of the Coast. charactercheck is unofficial, unaffiliated, and reads only what a character's owner has made public.

<!-- MCP registry ownership marker (do not remove): binds this repo's PyPI package to its registry namespace. -->
mcp-name: io.github.chaoz23/charactercheck


## The settlement quiz (v0.3)

```
charactercheck quiz <ref>
```

Questions the GM asks **out loud** at a ledger-flush boundary, each with the
silently-held expected answer where derivation has authority (AC, HP max,
total slots, attunement — with provenance strings). Live state only the
player tracks (current HP, expended slots) is `expect: null,
authority: "player"` — the engine never estimates. Grade privately, remind
diplomatically; `diff` against the intake snapshot is the reality check.
Unhandled patterns propagate as a caveat naming what the answer key cannot
verify. Unhandled items now also carry the payload's own verbatim `text`
so intake interviews read the source's words, never a paraphrase.


## The seat pack (v0.4)

```
charactercheck seatpack <ref> [--for-dm]
```

Everything a seat needs at session start, in one call: abilities, saves,
skills with proficiency flags, passives, DCs, combat block, resources,
inventory, **vision** (species darkvision plus Devil's Sight-class features,
with provenance — born from a live table where a DM narrated a warlock blind),
and a persona section carrying the sheet's own trait/ideal/bond/flaw text
verbatim with an explicit `not_derivable` list: charactercheck never invents
personality. `--for-dm` redacts player-authority live state per the
settlement contract.
