specification
Rendered verbatim from SPEC.md in the reference implementation — the same bytes the verifier is built against. Raw copy: /spec.md. Structure is normatively defined by claims.schema.json.
Version: 1
Status: draft, stable enough to build against
Schema: claims.schema.json (JSON Schema 2020-12) is normative for structure
License: MIT (this document and the reference implementation)
A claimset says: this exact sentence appeared at this exact URL on this exact date, and after this date do not trust that it still does. A verifier re-fetches the source and tells you whether it is still true.
The difference from every "freshness" signal that came before: a claimset is falsifiable by a stranger. It does not assert that content is current. It publishes what would have to be checked to find out, and anyone can run the check without asking permission.
{
"claimset": "1",
"publisher": "Denial Facts",
"contact": "https://example.org/corrections",
"claims": [
{
"claim_id": "external-review-deadline-four-months",
"assertion": "you must file a written request for an external review within four months",
"source_url": "https://www.healthcare.gov/appeal-insurance-company-decision/external-review/",
"checked_on": "2026-08-14",
"expires_on": "2027-02-14",
"source_sha256": "149ad94e…",
"superseded_by": null,
"note": "Cited on /how-to-appeal and in the Kit."
}
]
}
Conventional filename claims.json, committed to version control next to the
content it backs. Larger publishers MAY split by topic
(claims/appeals.json, claims/pricing.json).
| Field | Required | Meaning |
|---|---|---|
claimset | no (default "1") | format version; a verifier MUST refuse an unknown one |
title, publisher, contact | no | who stands behind the register, and where to report an error |
claims | yes | non-empty array |
Per claim:
| Field | Required | Meaning |
|---|---|---|
claim_id | yes | stable, unique within the file; matches ^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$ |
assertion | yes | a verbatim span from the source (§2) |
source_url | yes | http, https, or file |
checked_on | yes | YYYY-MM-DD, the date the assertion was last confirmed present |
expires_on | yes | YYYY-MM-DD, strictly after checked_on |
superseded_by | no | a claim_id in this file that replaces this one |
source_sha256 | no | 64 lowercase hex, SHA-256 of the raw response body at checked_on |
note, tags | no | for humans |
Unknown fields are rejected, not ignored. A typo in a field name must not silently disable a check.
expires_on is requiredBecause a claim with no expiry never becomes wrong — it just quietly stops being right, and nothing ever says so. Requiring it forces the publisher to answer "how long am I willing to vouch for this?" at the moment they know the answer. A regulation page might get 180 days; a price might get 30.
Content published elsewhere cites claim_ids. Deleting a claim breaks that
citation silently; superseding it leaves a forwarding address. A superseded
claim is reported, skipped, never fetched, and never fails a run.
superseded_by MUST name a claim_id present in the same file, and MUST NOT
be the claim's own id.
The assertion is a span copied from the source. Not a paraphrase, not a
summary, not a conclusion drawn from the source.
This is the single rule the format lives or dies on, and it is not stylistic. A paraphrase cannot be checked by string comparison, and a "verifier" that scores paraphrases with a similarity threshold is making a judgement call — at which point you are trusting the verifier instead of the source, and you are back where you started.
Measured failure this exists to prevent. An early checker was given the
assertion free and reported PASS, because the page contained the word "free"
in an unrelated sentence. The claim was true, the check was worthless, and the
green result was actively misleading — it consumed the reader's trust without
earning it.
A conforming verifier MUST reject an assertion that fails any of:
| Rule | Test (on the normalised assertion, §3) |
|---|---|
| W1 | at least 12 characters |
| W2 | at least 2 word tokens |
| W3 | at least 4 significant words, or at least one number and at least 2 significant words |
A significant word is a token that is not in the verifier's stopword list
(common English function words: the, of, to, is, you, may, must,
and so on). Numbers always count as significant.
Rejected assertions get status WEAK and fail the run. They are not
fetched: an unfalsifiable claim has nothing to check.
W1–W3 are verifier rules, not schema rules. claims.schema.json
deliberately accepts a weak assertion, so that a verifier can load the file and
report exactly which claim is weak and why. A file that failed to parse would
tell the author nothing useful.
Worked examples:
| Assertion | Verdict | Why |
|---|---|---|
free | WEAK | W1, W2 |
you have the | WEAK | W3: no numbers, 0 significant words |
appeal it | WEAK | W1 |
within 180 days of the denial | ok | number + 4 significant words |
no later than 45 days | ok | number + 3 significant words |
required by law to accept the reviewer's decision | ok | 5 significant words |
The exact stopword list is implementation-defined and MAY differ between verifiers; the thresholds MUST NOT. A verifier SHOULD publish its list.
The assertion and the fetched source are put through the same pipeline, in this order, before comparison. Order is normative.
Content-Type, or by
sniffing a leading HTML element). Remove comments; remove script, style,
noscript, svg and template elements including their content; replace
every remaining tag with a single space.
Tags become spaces, never nothing — otherwise 180<b>days</b> becomes
180days and a true claim fails. , ’, —, numeric references).-,
… → ..., every Unicode space → U+0020, zero-width and soft-hyphen
characters → removed. These are CMS artefacts, not content.casefold, not ASCII lower).1,000 → 1000).Comparison is then plain substring containment: the normalised assertion must appear in the normalised source.
Substring containment, not fuzzy matching, on purpose. A fuzzy threshold is a knob, and a knob is an argument. Containment either holds or it does not, and two implementations cannot disagree about it.
Measured failure this exists to prevent. A source said "within four months"; the published guide said "within 4 months". A naive checker reported DRIFT. That is a false alarm, and false alarms are how a checker dies — the second time a green build turns red for nothing, people stop reading the output.
Written cardinals are rewritten as digits on both sides:
zero–nineteen, tens twenty–ninety, scales hundred,
thousand, million, billiontwenty five → 25, one hundred eighty → 180,
one hundred and eighty → 180, two thousand → 2000twenty-five → 25)and is a connector only when a number word follows it inside a runA run is not combined when the sequence is not a well-formed number:
one two three → 1 2 3, not 6. Concretely, a run continues only when the
next word is a scale, or the previous word was a scale, or a unit follows a ten.
Ordinals are deliberately excluded. second is also a unit of time, and
rewriting "within 30 seconds" would be worse than not trying. If you need an
ordinal in an assertion, quote it as the source spells it.
No stemming, no synonym expansion, no punctuation stripping, no stopword removal. Each of those would turn near-misses into passes, which is the failure mode §2.1 exists to prevent. Normalisation only ever collapses distinctions that are presentational.
Evaluated in this order; the first match wins.
| # | Status | Condition | Fetches? | Fails the run? |
|---|---|---|---|---|
| 1 | SUPERSEDED | superseded_by is set | no | no |
| 2 | WEAK | the assertion fails §2.1 | no | yes |
| 3 | EXPIRED | expires_on < today | no by default | yes |
| 4 | UNREACHABLE | the source could not be fetched (§5) | attempted | yes |
| 5 | DRIFT | fetched, but the assertion is not present | yes | yes |
| 6 | PASS | fetched, and the assertion is present | yes | no |
Notes:
EXPIRED before fetching because expiry is a statement about the
publisher's own attestation, not about the page. A verifier MAY offer a mode
(--recheck-expired) that fetches anyway and reports whether the assertion
still holds — useful, and it does not change the status.UNREACHABLE fails by default. "I could not check" is not "nothing is
wrong." A verifier MAY offer --allow-unreachable for flaky sources; it MUST
NOT be the default.source_sha256 mismatch is not, by itself, drift. A page can be edited
anywhere without touching the sentence you cited. A mismatch is reported as
source_changed and only fails under a strict mode.| Code | Meaning |
|---|---|
0 | every claim is PASS or SUPERSEDED |
1 | at least one DRIFT, EXPIRED, UNREACHABLE or WEAK |
2 | the run could not happen: bad arguments, missing file, schema violation, unknown format version |
Schema violations are exit 2, not 1. A malformed register is not a failed
check; it is an absent one.
A conforming verifier:
User-Agent identifying itself. The default is
claimset-verify/0.1. Operators SHOULD append contact information, e.g.
claimset-verify/0.1 (+https://example.org/bot).robots.txt for http/https sources and honour a
disallow, reporting UNREACHABLE with a reason rather than fetching anyway.
Per RFC 9309: a 4xx on
robots.txt means no restrictions; an unreachable robots.txt (5xx,
timeout, transport error) means assume complete disallow.file: URLs skip robots and rate limiting.A claimset verifier is a politeness-critical tool: it re-fetches other people's pages, forever, on a schedule, on behalf of people who did not write it. A verifier that behaves badly gets the whole format blocked.
Nothing is required beyond committing the file. A publisher SHOULD additionally:
/claims.json is the suggested convention,verify in CI on a schedule, not only on push. Sources change on
their own calendar; a repository with no commits this month is exactly the one
whose facts have quietly rotted..well-known/claims.json is a plausible future home. It is not claimed here:
registering a .well-known name before anyone uses the format would be
standardising first and finding out later, which is the wrong order.
llms.txt asks readers to
believe a publisher's summary of their own site; measured outcome, 97% of
published files never got a single request. A claim you can check without
asking anyone's permission is a different kind of object from a claim you are
asked to accept.claimset increments when the file shape changes. A verifier MUST refuse an
unknown version rather than best-effort it: silently skipping a field it does
not understand could turn a failing check into a passing one, which is the one
outcome the format must never produce.
Changing the normalisation pipeline (§3) or the falsifiability floor (§2.1) in a way that changes any existing verdict is a breaking change and requires a version bump.