Metadata-Version: 2.5
Name: claims-ledger
Version: 0.0.1
Summary: A checked claims ledger: entries that separate assertion, grounds, warrant and backing, hold every quotation to its source, and derive status from an append-only verdict list
Project-URL: Homepage, https://github.com/Ybx-jp/claims-ledger
Project-URL: Issues, https://github.com/Ybx-jp/claims-ledger/issues
Project-URL: Changelog, https://github.com/Ybx-jp/claims-ledger/blob/main/CHANGELOG.md
Author: Jackson Porter
License-Expression: MIT
License-File: LICENSE
Keywords: argumentation,citation,nanopublication,provenance,research
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Science/Research
Classifier: Operating System :: MacOS
Classifier: Operating System :: POSIX :: Linux
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Scientific/Engineering
Classifier: Topic :: Text Processing :: Markup :: Markdown
Requires-Python: >=3.11
Provides-Extra: dev
Requires-Dist: hatchling; extra == 'dev'
Requires-Dist: pytest>=8.0; extra == 'dev'
Requires-Dist: ruff>=0.14; extra == 'dev'
Requires-Dist: ty>=0.0.78; extra == 'dev'
Description-Content-Type: text/markdown

# claims-ledger

A checked ledger of claims, in plain Markdown files, for a project that wants its
written record to be verifiable rather than merely earnest.

Every project writes down what it believes about itself: a README that promises no
runtime dependencies, a docstring that says a function refuses bad input, a lab note that
reports a number, a runbook that says which step is safe to repeat. Nothing holds those
sentences to the code, the data or the source they rest on, so they drift, and they drift
silently. `claims-ledger` gives each such sentence an *entry* — a small Markdown file that
separates what is claimed from what it rests on — pins the entry to the exact span of the
file or source that keeps it true, and fails the commit when the two stop agreeing. The
film below shows one claim being refuted, and everything that rested on it saying so.

<img alt="A 70-second film of the research-repo example. A claim file is shown with its Grounds; claims-ledger status reports it open and check is clean. A new claim, R0013, is written; a refuted verdict naming it is appended to R0001, and status now reports refuted. check fails for the two entries and two documents that cite R0001 as live; propagate --write appends their contested rows; git commit is refused by the pre-commit hook. Nothing falls silently." src="https://raw.githubusercontent.com/Ybx-jp/claims-ledger/main/docs/videos/nothing-falls-silently/nothing-falls-silently.gif" width="960">

An entry separates the four roles a sentence in a research note usually fuses — the
claim, the data it rests on, the rule that gets you from one to the other, and the
source's own words — holds every quotation to the bytes of the source it names, and
derives its status from a verdict list that only ever grows. Five checkers enforce that,
and a red-team corpus of 95 seeds with committed expected outcomes proves the checkers.

    pip install claims-ledger

Nothing is installed alongside it: the checkers use the standard library only, so
`python3 -m claims_ledger check` runs from a plain interpreter — which is how the
pre-commit hook invokes them, rather than by console-script name that git's own
environment may not have on PATH. Python 3.11 or newer
(L0011-no-runtime-dependencies, cites-as-live).

## Why

The schema was designed after an audit of a working ledger whose entries carried a
single frozen statement field. Comparing every entry that quoted a source against the
source it named, 24 of 47 quotations were defective. The failure was structural, not
careless: with quote, observation, inference and authority in one blob, a faithful quote
could continue seamlessly into unsourced inference and be sealed there by the freeze.

So an entry here has an Assertion in the project's words with **no quotation mark
allowed in it**, Grounds that are typed pointers, a Warrant that states the rule, and
Backing that holds the verbatim quotations — each one resolved, at check time, against
the stored bytes of its registered source.

<picture>
  <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/Ybx-jp/claims-ledger/main/docs/figures/four-roles-dark.svg">
  <img alt="A single frozen statement, where a faithful quotation runs seamlessly into unsourced inference, against an entry whose Assertion, Grounds, Warrant and Backing each sit on their own line and whose quotation is resolved against the stored source bytes." src="https://raw.githubusercontent.com/Ybx-jp/claims-ledger/main/docs/figures/four-roles.svg" width="960">
</picture>

<picture>
  <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/Ybx-jp/claims-ledger/main/docs/figures/nothing-falls-silently-dark.svg">
  <img alt="Three layers: two documents on top, five claims in the middle, three sources at the bottom, with lines tying every document citation to a claim, every claim to a document that cites it, and each claim's quotation to a span of a source. One claim in the middle row carries a refuted verdict. From it, a highlighted path runs to the claim that cited it, now marked contested; to the document citation that named the refuted claim; and to the document citation that named the contested one. Every other line, claim and source is unmarked." src="https://raw.githubusercontent.com/Ybx-jp/claims-ledger/main/docs/figures/nothing-falls-silently.svg" width="960">
</picture>

## Where it sits

Three files in your repository, held together by one command. A sentence in a document
you already write cites an entry; the entry names the span of a file the claim rests on —
a function, a configuration key, a section of a note, the stored bytes of a paper; and
`claims-ledger check` reads all three and never reports a pass over anything it did not
read. The example is this repository's own README: the sentence under the install line
above cites an entry that is pinned to one key of `pyproject.toml`.

<picture>
  <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/Ybx-jp/claims-ledger/main/docs/figures/where-it-sits-dark.svg">
  <img alt="Three panels in a row, joined by arrows labelled cites and pins: the sentence in README.md that promises no runtime dependencies, carrying a citation of entry L0011; the entry file, whose Grounds name one key of pyproject.toml at a commit and whose Verdicts hold a corroborated row; and pyproject.toml itself, with the line dependencies = [] highlighted as the span the claim rests on. Below, the three are gathered into one bar, claims-ledger check, which runs in a shell, in the pre-commit hook, in CI and from a coding agent's hooks. Below that, what happens when the span changes: freshness flags the ground as moved, references fails the sentence that still cites the entry as live, and git commit is refused until a person re-judges the claim." src="https://raw.githubusercontent.com/Ybx-jp/claims-ledger/main/docs/figures/where-it-sits.svg" width="960">
</picture>

Nothing here decides whether a claim is true. When the pinned span changes, the checkers
report that the ground *moved*, fail every sentence that still cites the entry as live,
and refuse the commit until a person re-judges the claim and records the judgement as a
verdict.

## Quickstart

Every command below is run by `tests/test_readme_quickstart.py` against a fresh project
and its output compared with what is printed here, so this transcript is reproducible
rather than illustrative — including the digests, which is why the source file it starts
from is written out rather than assumed.

```console
$ cat > paper.txt <<'EOF'
Okafor and Lindqvist authored the study. A stale fraction of 0.1 produced an error of 0.04. The full run log is in the appendix.
EOF

$ claims-ledger init
wrote /home/you/project/claims-ledger.toml
created /home/you/project/ledger/entries, /home/you/project/ledger/cache and …

$ claims-ledger source add paper.txt --id fx-paper --type paper \
    --citation "Okafor and Lindqvist (2026)" --authors Okafor Lindqvist
registered fx-paper (f09680d85993…) in ledger/sources.jsonl
bytes at ledger/cache/f09680d85993…

$ claims-ledger new stale-fraction-governs-error
wrote ledger/entries/A0001-stale-fraction-governs-error.md
Fill in Assertion, Scope, Grounds, Warrant and Backing, then `claims-ledger sha --write`…
A ground wider than the claim goes stale for edits the claim does not name, and…
Once the Grounds are filled in, `claims-ledger neighbours A0001-stale-fraction-governs-…

$ cat > ledger/entries/A0001-stale-fraction-governs-error.md <<'EOF'
---
id: A0001-stale-fraction-governs-error
kind: claim
stated: 2026-09-06T09:00:00-07:00
author: main
grade: asserted
supersedes: none
verbatim_sha: 0
---

## Assertion

A stale fraction of 0.1 produces an embedding error of 0.04.

## Scope

metric: embedding error
cohort: dynamic graph embeddings
condition: stale fraction 0.1

## Grounds

- source: fx-paper · summary

## Warrant

Restates the source's own reported result.

## Backing

- source: fx-paper · summary
  speaker: Okafor and Lindqvist
  quote: "A stale fraction of 0.1 produced an error of 0.04."

<!-- APPEND BELOW THIS LINE ONLY -->

## Verdicts

## References
EOF

$ claims-ledger sha --write ledger/entries/A0001-stale-fraction-governs-error.md
ledger/entries/A0001-stale-fraction-governs-error.md: 0… → 84d9b6514b98…

$ claims-ledger check
validate: 0 failure(s), 0 flag(s)
resolve: 0 failure(s), 0 flag(s)
references: 0 failure(s), 0 flag(s)
propagate: 0 failure(s), 0 flag(s)
freshness: 0 failure(s), 0 flag(s)
```

An entry looks like this:

```markdown
---
id: A0001-stale-fraction-governs-error
kind: claim
stated: 2026-09-04T12:00:00-07:00
author: main
grade: measured
supersedes: none
verbatim_sha: 3b47609dbcdf…
---

## Assertion

The mean aggregation error is governed by the stale fraction and not by degree.

## Scope

metric: mean L2 error of the aggregated representation
cohort: the synthetic demo graph
condition: mean aggregation, one layer

## Grounds

- lab: notes/001.md § "Observation" =sha256:1c9e2b7d5a03…
- source: fx-paper · whole text

## Warrant

A measured error at a known stale fraction, with the source stating the same rule,
supports the assertion over this cohort.

## Backing

- source: fx-paper · whole text
  speaker: Okafor
  quote: "The error under mean aggregation scales with the stale fraction and does not grow with degree."

<!-- APPEND BELOW THIS LINE ONLY -->

## Verdicts

## References
```

<picture>
  <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/Ybx-jp/claims-ledger/main/docs/figures/entry-anatomy-dark.svg">
  <img alt="An entry file annotated part by part: frontmatter, then Assertion, Scope, Grounds, Warrant and Backing in a region that is frozen once committed; the APPEND marker as a seam; then Verdicts and References, which only ever grow." src="https://raw.githubusercontent.com/Ybx-jp/claims-ledger/main/docs/figures/entry-anatomy.svg" width="960">
</picture>

The schema in full — every field, every rule, and what each heuristic is known to miss —
is in [docs/SCHEMA.md](https://github.com/Ybx-jp/claims-ledger/blob/main/docs/SCHEMA.md). Running one over time — what a squashed or
rebased history costs, and how a drifted claim is repaired — is in
[docs/OPERATING.md](https://github.com/Ybx-jp/claims-ledger/blob/main/docs/OPERATING.md).

## The five checks

| command | holds |
|---|---|
| `claims-ledger validate` | entries are well-formed; the frozen region above the APPEND marker never changed after the commit that created it, and verdicts only ever grew (both read from git history) |
| `claims-ledger resolve` | every pointer resolves; every quoted span is a contiguous span of the named source's stored bytes, with elisions marked |
| `claims-ledger references` | citation acts agree with the target's current status, entry to entry and document to entry, both directions |
| `claims-ledger propagate` | when an entry falls or is challenged, its dependents carry the `contested` flag that says why (`--write` appends them) |
| `claims-ledger freshness` | every pinned ground still names the artifact the claim was established on: the path is in the tree and the digest of its section matches the anchor — stated by value, or read out of the commit a by-reference anchor names — and a by-reference anchor is a commit rather than a name that moves (`--write` appends the missing `contested` verdicts) |

<picture>
  <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/Ybx-jp/claims-ledger/main/docs/figures/five-checks-dark.svg">
  <img alt="A matrix of the five checkers against what each reads: entries, git history, the source registry and cache, the documents, and the working tree or index; with what each holds, and the two, propagate and freshness, that write a contested verdict under --write." src="https://raw.githubusercontent.com/Ybx-jp/claims-ledger/main/docs/figures/five-checks.svg" width="960">
</picture>

`claims-ledger check` runs all five. Each exits non-zero on a failure and zero on a
flag, because a flag is a report a human judges rather than a gate.

A check that could not run never reports that it passed. Pointed at a directory with no
entries directory in it — the wrong `--root`, a configuration file moved away from its
ledger — every checking command stops with exit 2 and says nothing was checked, rather
than printing five clean lines over an empty room
(L0010-a-missing-entries-directory-stops-the-command, cites-as-live). Where a check is genuinely skipped
rather than passed, it is named on stderr: outside a git repository, or with no `git` on
PATH, validate's frozen-region and append-only checks cannot run and say so.

Statuses are never stored (L0006-status-is-derived-from-the-verdicts, cites-as-live).
They are derived from the last verdict — `open`,
`corroborated`, `contested`, `refuted`, `superseded`, `retracted`, `non-comparable` —
so the filter a reader would otherwise have to apply by hand is applied at check time,
and a document that still cites a refuted entry as live fails.

## Proving the checkers

A checker nobody has tried to fool is a checker nobody should trust. The package ships
the red-team corpus it was built against: 95 seeds, each a small ledger with committed
expected outcomes, one per defect class the audit found, one per rule about not silently
passing, plus known-good seeds every checker must leave alone. Its README says which
rules the corpus does *not* hold up and which the unit suite holds instead — a coverage
claim nobody has tried to falsify is worth as little as an unfooled checker.

```console
$ claims-ledger corpus
PASS D01-unmarked-deletion
…
95/95 seeds pass
```

The contract is symmetric: a seed passes when every expected failure is produced at the
named place **and** no checker trips where the seed does not say it should. An unlisted
catch is a finding about the seed or the checker, never a bonus, one row is satisfied by
one report and not by two, and a run that checked nothing — an empty corpus, a seed name
that matches none — exits non-zero rather than reporting a clean run over nothing. See
`src/claims_ledger/corpus/README.md` for the coverage table, including the rows where
the machinery only makes a defect visible and a human has to judge it.

<picture>
  <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/Ybx-jp/claims-ledger/main/docs/figures/corpus-contract-dark.svg">
  <img alt="Top, a defect seed run: on the left its expected.json with two rows, validate pass at all and resolve fail at A0001 Backing quote 1; on the right the run's five checker reports, with resolve's failure at that place matched to its row, and references, propagate and freshness bracketed as checkers no row names, which must exit clean. Bottom, three ways the runner fails: a report no row names, a row two reports satisfy, and a run that checked nothing." src="https://raw.githubusercontent.com/Ybx-jp/claims-ledger/main/docs/figures/corpus-contract.svg" width="960">
</picture>

The corpus is one of four things holding the checkers; `QUALITY.md` is the short account
of all four, and of what happens to a defect once it is found.

## One schema, your names

What an entry is does not change between projects. Everything around it is
configuration: where the ledger sits, which documents may cite it, what an evidence
pointer is called and how its section is found, who may write a verdict. And the checks
run wherever a plain `python3` does — a shell, the pre-commit hook, CI, a coding agent's
hooks, or your own code through the library.

<picture>
  <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/Ybx-jp/claims-ledger/main/docs/figures/one-schema-your-names-dark.svg">
  <img alt="A strip at the top lists what the schema fixes: kind, grade, status, citation act, the verbatim fingerprint, the frozen region and the append-only tail. Below it, three columns. What may cite an entry: README.md, docs/*.md, reports/*.md and ROSTER.md, src/**/*.py, pyproject.toml — any glob from the project root. What a ground may name: two fixed types, source and entry; then four the project names itself — lab, a Markdown heading; code, a def or class or export function by pattern; toml-key, one key of a table; experiment, a whole file — each anchored by value as a digest or by reference as a commit. Where the checks run: a shell, the pre-commit hook, CI, the Python library, and a coding agent's hooks for Claude Code, Codex, Cursor or any other. At the bottom, the four example repositories — a TypeScript UI, a Python service, a study, a handbook — each with the evidence names it chose, under the caption: same entries, same five checks, same corpus; only the names differ." src="https://raw.githubusercontent.com/Ybx-jp/claims-ledger/main/docs/figures/one-schema-your-names.svg" width="960">
</picture>

The [example portfolio](examples/) is the proof: a TypeScript UI whose `code:` section is
an `export function`, a Python service whose section is a `def`, a research repository
that pins lab notes by heading and experiments by file, and an operator handbook — four
ledgers, one schema, all five checks passing in each.

## Configuration

`claims-ledger.toml` at the project root, or a `[tool.claims-ledger]` table in
`pyproject.toml`. Everything is optional; the defaults are shown.

```toml
[tool.claims-ledger]
ledger = "ledger"                    # entries/, sources.jsonl and cache/ live here
entries = "entries"
registry = "sources.jsonl"
cache = "cache"                      # empty string: no cache, rows name their bytes

# Documents that may cite an entry, as globs from the project root. Both keys are
# matched the same way, segment by segment: `*` and `?` stop at a separator and `**`
# spans any number of segments, so a whole subtree is `docs/private/**` rather than
# `docs/private/`. An excluded document is not checked at all — every citation rule is
# off for it, not just the one you had in mind.
documents = ["*.md", "docs/*.md"]
document-excludes = []

# The named artifacts an entry may rest on. A sectioned type is written
#   lab: <path> § "<section>" =sha256:<digest>
# and a plain one
#   experiment: <path> =sha256:<digest>
# The anchor names the datum by value — the digest of the section as the checkers
# compare it, which `sha --write` fills in for `=?` — or by reference, `@<commit>`, as
# the section stood at that commit.
evidence-sectioned = ["lab"]
evidence-plain = ["experiment"]

# How a sectioned type finds its section. `{name}` is the only substitution; the rest is
# an ordinary regex, matched line by line. Omitted, a type gets the Markdown heading that
# `§` always meant. A section runs from its own header to the next one, so anchor the
# pattern at the granularity the section really has — allowing leading whitespace in the
# Python pattern below would end a function at its first nested definition and leave the
# rest of it uncompared.
[tool.claims-ledger.section-patterns]
code = '^(?:def|class)[ \t]+{name}\b'

# A pattern is how narrow a ground can be, and narrowness is what keeps a claim from
# going stale for a reason it does not care about. A claim about one setting should not
# rest on the table holding it:
#   toml     = '^\[{name}\]'    # the whole table
#   toml-key = '^{name} = '      # one key of it, single-line values only
#
# One pattern decides both ends of a section, so a key's section runs to the next line
# the same pattern matches. Over a value written across several lines that is the key's
# own first line and nothing else — a ground that can never go stale, which is worse than
# one that goes stale too often. And a key name that is not unique in the file matches
# the first one, whichever table it is in.

verdict-authors = ["main", "propagation"]
propagation-author = "propagation"   # the name machinery writes under

roster = "ROSTER.md"                 # the hand-maintained view of open hypotheses
archived-prefixes = []               # id series a previous ledger quarantined

# Whether a citation has to sit inside the section its entry pins: "off", "flag" or
# "fail". The sentence that states a commitment and the span that keeps it then move
# together, and a reader who finds one finds the other. Off by default, because a project
# adopting it mid-life wants to see where it stands before it gates anything
# (L0172-citation-placement-is-configured-and-defaults-to-off, cites-as-live). Asked by
# `references`, and again by `sha --write` on the entry being written, which is the first
# moment the entry's grounds and the citation both exist
# (L0174-the-placement-question-is-asked-when-the-entry-is-written, cites-as-live).
citation-placement = "off"
```

The schema itself is not configurable. Grades, kinds, statuses, citation acts, the
fingerprint and the immutability rules are the claims model, not a project's naming, and
a project that changed them would no longer be running the checks the corpus proves.

An unknown key is an error rather than a silent no-op: a misspelled key that quietly
changes nothing is how a project ends up unchecked
(L0003-unknown-configuration-key-is-an-error, cites-as-live).

Every path a configuration names — `ledger`, `entries`, `registry`, `cache`, and the
`documents` globs — has to stay under the project root
(L0004-configured-paths-stay-under-the-root, cites-as-live). An absolute path, one that walks
out through `..`, and a `ledger` that is a symlink to somewhere else are all refused by
name rather than honoured: cloning a repository should not hand its `claims-ledger.toml`
the right to say where this tool writes, or point the checkers at a file the project does
not contain.

A checker that cannot read something says so and exits non-zero. An entries directory it
cannot list, a documents directory it cannot enter, a document it cannot open or decode,
and a git that runs but cannot answer, are all reported rather than read as empty —
`0 failure(s)` over a ledger that was never read is the one report this tool must never
produce.

Writing is confined the same way reading is checked. Nothing is written through a link
that leaves the project root, including an entry file inside `entries/` that is a symlink
to somewhere else; reads follow such a link, writes refuse it.

## Authoring

| command | does |
|---|---|
| `claims-ledger new <slug>` | scaffold an entry at the next free id (`--kind`, `--grade`, `--author`, `--supersedes`, `--credence`, `--resolves-when`) |
| `claims-ledger sha <path>` | report the fingerprint recomputed from Scope and Backing; `--write` rewrites the declared value |
| `claims-ledger source add <file>` | register a source and store the bytes its quotations are checked against |
| `claims-ledger source list` | the registered sources, and whether their bytes are present |
| `claims-ledger status` | every entry with its kind, grade and derived status |
| `claims-ledger neighbours <id\|path\|ground>` | the entries already about the same span or a nesting cohort; `--count` summarizes the whole ledger |
| `claims-ledger hook` | print the pre-commit hook; `--install` writes it |
| `claims-ledger --version` | the installed version |

`sha --write` refuses on an entry git already has
(L0007-sha-write-refuses-a-committed-entry, cites-as-live). The region above the APPEND marker is
immutable once committed, so a new fingerprint there is a new entry: supersede it, or
pass `--force` if the commit has not left the machine.

Registering a source stores its bytes in the same call that writes the row, because a
registry row without its bytes is a check that cannot run. The row is committed and the
bytes are not — `ledger/cache/.gitignore` is written by `init` — so the row carries the
sha256, and the `--url` and `--extraction` that regenerate them.

That means a fresh clone of a project's ledger has rows without bytes, and `check` fails
loudly, per quotation, until they are back: re-run `claims-ledger source add` on the
bytes named by each row's url and extraction, and `claims-ledger source list` will say
`bytes present`. There is no command that fetches them for you, because how a source's
bytes were produced from its url is a decision with a record, not a download.

`neighbours` is the one command here that is not a check. Two entries about the same
function, written months apart by people who never read each other, name nothing of each
other, so every rule the checkers apply to an `entry:` edge is out of range of them by
construction. What is mechanically available is that the two are *near* — the same pinned
span, or a cohort whose words nest inside another cohort's — and near is not inconsistent.
So it is asked rather than enforced: it reports nothing, appends nothing, exits 0, and
`check` does not run it
(L0162-neighbours-reports-nothing-and-exits-zero, cites-as-live). Run as a gate over this
repository's own ledger, the same heuristic flags a few hundred pairs to surface the two
worth reading, which is a rate that teaches people to ignore it. Asked one entry at a
time, it answers with a handful.

The answer ends with the ground line that would record a distinction, written out for
every pair with no relation recorded
(L0171-the-lookup-hands-over-the-line-it-would-take, cites-as-live). That is as far as a
lookup goes: the line is the same text whichever neighbour it names, and whether to write
it at all is the judgement being handed over.

What to do with an answer is yours. Reconcile the two, supersede one, or record that they
are different claims with a `distinguishes` ground — an act an entry may perform on
another entry and a document may not. It is legal against a target of any status, because
it is a claim about two Scopes rather than about a truth
(L0158-a-distinction-is-legal-against-any-status, cites-as-live); it propagates nothing
when its target falls
(L0161-a-distinguishing-ground-propagates-nothing, cites-as-live); and it is not support,
so an entry whose every ground is one rests on nothing
(L0160-a-distinction-is-not-support, cites-as-live). Grounds are frozen once committed, so
a distinction is written by the newer entry and the older one never points back —
`neighbours` is what finds it from the other side, and says so, rather than proposing a
pair somebody has already read.

The hook `--install` writes names the interpreter it was installed by, absolutely, and
reaches the package with `-m`
(L0001-hook-names-the-interpreter-absolutely, cites-as-live). Git runs hooks with its own environment: a hook that said
`claims-ledger` would fail with `not found` on every commit for anyone who installed
into a virtualenv that was not active.

### Landing an entry takes one commit

An entry's citation usually sits *inside* the section that entry rests on — the
docstring is in the function. A ground stated by value is the digest of that section's
text, computed from the working tree before any commit exists, so the code, the citation
and the entry land together: write each ground's anchor as `=?`, run
`claims-ledger sha --write` on the entry to fingerprint it and fill the anchors, and
commit all three. The pre-commit hook passes on the first try. A ground may still be
stated by reference, `@<commit>`, and that is the form a squashed or rebased history
destroys; the by-value form loses only the diff a person would read during repair.

`docs/OPERATING.md` covers this, what a rewritten history costs a pinned ledger, and the
order in which a drifted claim is repaired.

## In a coding agent

A ledger that pins claims to code has two failures no checker can catch in time: an edit
lands inside a pinned span and nothing says so until `git commit`, and prose promises
something and cites nothing, which passes every check there is. The package ships four
hooks and three skills for that, and installs them:

```console
$ claims-ledger harness install --agent claude
wrote      .claude/skills/repair-a-drifted-pin/SKILL.md
…
wrote      .claude/hooks/pin-guard.sh
wrote      .claude/settings.json
```

`--agent` is `claude`, `codex`, `cursor` or `agent`. Each gets the same skills and the
same scripts under its own directory, wired in the file that agent really reads —
`.claude/settings.json`, `.cursor/hooks.json`, and for codex `$CODEX_HOME/hooks.json`,
which is the only file it loads hooks from; `claims-ledger harness list` prints the table.
The scripts read both payload dialects and answer in the one they were called in. The
hooks report drift at edit time, refuse the squash and
rebase merges that would destroy every ground stated by reference, and lay out the four repairs when a
citation's act stops matching its target's status; the skills carry the procedures behind
them. Nothing already in the project is written over — a file that differs is left alone
and named, and `--force` is what changes that — and a settings file that already exists
is never edited, only printed to.

They need `jq` when they run. The package still declares no runtime dependencies of its
own.

## As a library

```python
from claims_ledger import open_ledger, load_entries, LedgerError
from claims_ledger import validate, resolve, references, propagate

ledger = open_ledger(root="/path/to/project")
for entry in load_entries(ledger):
    print(entry.id, entry.status(), entry.scope["metric"])

reports = validate.run(ledger)  # [Report(outcome, entry, part, message)]
failed = any(r.outcome == "fail" for r in reports)
```

`open_ledger` finds the configuration; `Ledger` can also be built from a `Config`
directly, which is how the corpus runner points the checkers at a seed.

A file the user maintains that cannot be read or parsed — an entry that is not UTF-8, a
`sources.jsonl` line that is not JSON — raises `LedgerError`, naming the file and the
line. The CLI turns that into a diagnostic and exit 2; nothing reaches a user as a
traceback. `Report` is a finding about a well-formed ledger, `LedgerError` is the ledger
being unreadable in the first place, and the two are not mixed.

## What this does not do

It does not decide whether a claim is true. Resolution shows a span exists in the named
artifact and nothing more. A load-bearing elision, an undercut recorded as a refutation,
a widened scope, a citation chain that never reaches evidence — the machinery can make
each of those visible, and the classification is a human's, recorded as a verdict. An
entry that passes every check is not thereby right.

Both heuristics it does apply (the absence-claim trigger words and the relayed-quotation
flag) are documented with the cases they are known to miss, in `docs/SCHEMA.md`.

## Working on it

```console
$ pip install -e ".[dev]"
$ ruff check . && ruff format --check .
$ ty check
$ pytest -q
$ claims-ledger corpus
$ claims-ledger check
```

The last one checks the ledger this package keeps about itself, under `ledger/`. A design
commitment the README states and a docstring explains — the hook names its interpreter
absolutely; a misspelled configuration key is refused — is an entry, pinned to the function
or the table that keeps it true, and both the sentence and the docstring cite it. When a
pinned function changes, `freshness` flags the entry, and the sentence and the docstring
that cite it fail until someone re-judges the claim: a new entry with a new pin if it still
holds, a fallen verdict and rewritten prose if it does not. The entries are not shipped; they
are about this checkout, and `pyproject.toml` says which files may cite them.

All six are what CI runs, over Python 3.11, 3.12, 3.13 and 3.14 on Linux and macOS, plus a
job that builds the wheel, installs it into a clean environment and runs the corpus from
a directory that is not the checkout — because the claim that an installed copy can prove
itself is only worth anything if it is tested that way.

`ty` is pointed at the 3.11 floor rather than the newest interpreter, so a construct that
only exists on a newer Python cannot pass here and fail for a user. The ruff rule set is
listed explicitly in `pyproject.toml` rather than inherited from ruff's defaults, which
grow between releases.

A change that moves a corpus seed's expected outcome is a methodology change, not a bug
fix: record it in `CHANGELOG.md` with the seed named.

Cutting a release is `RELEASING.md`'s job to describe, not this one's — it names the
exact PyPI and GitHub setup a first publish needs and the steps every release after it
repeats.

## Where to read next

| you want to | read |
|---|---|
| try it on a project this afternoon | *Quickstart* above, then *Landing an entry takes one commit* |
| know exactly what an entry is and what each rule misses | [docs/SCHEMA.md](https://github.com/Ybx-jp/claims-ledger/blob/main/docs/SCHEMA.md) |
| run one for months — rewrites, branches, repairing a drifted claim | [docs/OPERATING.md](https://github.com/Ybx-jp/claims-ledger/blob/main/docs/OPERATING.md) |
| understand the fifth checker and what a pin is | [docs/FRESHNESS.md](https://github.com/Ybx-jp/claims-ledger/blob/main/docs/FRESHNESS.md) |
| decide whether to trust the checkers | [QUALITY.md](https://github.com/Ybx-jp/claims-ledger/blob/main/QUALITY.md), then the corpus [README](https://github.com/Ybx-jp/claims-ledger/blob/main/src/claims_ledger/corpus/README.md) |
| see every capability exercised, with excerpts | [examples/FEATURES.md](https://github.com/Ybx-jp/claims-ledger/blob/main/examples/FEATURES.md) |
| wire it into a coding agent | *In a coding agent* above; the installed skills under `.claude/skills/` |
| the record: what changed, what was audited | [CHANGELOG.md](https://github.com/Ybx-jp/claims-ledger/blob/main/CHANGELOG.md), [docs/audits/](https://github.com/Ybx-jp/claims-ledger/tree/main/docs/audits) |

## Provenance

Extracted from the claims ledger built for a research project on dynamic graph embedding
refresh, where the schema, the checkers and the corpus were developed together. The
extraction changed what was project-specific into configuration — where the ledger sits,
which documents may cite it, what an evidence pointer is called, who may write a verdict
— and changed nothing about the schema or the checks. Every one of the sixty-two seeds
the corpus held at extraction still passes unchanged; it has since grown to 95.

MIT licensed.

## Examples

[`examples/`](examples/) contains an executable four-repository portfolio: a TypeScript
UI, a Python backend, a research repository, and an operator-handbook repository. The
content is synthetic, but materialization creates real Git histories, real commit pins,
registered cross-repository source snapshots, installed hooks, and four ledgers that pass
all five checks.
The [concept-grouped feature guide](examples/FEATURES.md) documents every exercised
capability with exact repository excerpts.
