Skip to main content

spec-receipt

Your spec says POST /items. Receipt it — every spec item IMPLEMENTED, MISSING, CONTRADICTED or UNTRACED, every code surface DOCUMENTED or UNDOCUMENTED, with file-and-line evidence.

tests: 87 passing python 3.10 | 3.11 | 3.12 version 1.0.0 dependencies: none license: MIT

spec-receipt demo: the drifted fixture's receipt — a changed default, a deleted flag, a forbidden flag and a POST endpoint the code only registers as GET

A spec is a promise made at design time. Then the code moves: a flag is deleted, a default quietly changes, an endpoint's method flips, a forbidden flag sneaks back in, a feature ships that no document ever mentioned. Review catches some of it, memory catches less, and the spec slowly becomes fiction — while still being the document new contributors, agents, and reviewers trust first.

spec-receipt prints the receipt instead. It extracts what the spec declares (flags, commands, endpoints, config keys, environment variables, requirement IDs — by deterministic patterns, from any Markdown spec), extracts what the code declares (the same five surface classes, by Python AST), and prints a per-item verdict with the evidence cited on both sides. No LLM. No score. No network. Nothing uploaded.

Static and read-only — it executes nothing. The target repo is read as Markdown text and Python AST only: no file is run, imported, compiled, or evaluated. Its own source imports no network or subprocess module, and the test suite proves the read-only claim: a fixture module that would write a marker file if it ran never runs, and every fixture tree comes out byte-identical after a check.

Verdicts are stingy by design. MISSING is issued only when a surface class was completely enumerated and the item is absent from it. CONTRADICTED needs hard evidence on both sides — a stated default against a different code default, a "there is no --legacy" against the flag's own AST node. A prose requirement is never judged implemented or absent: it is TRACED to an anchor in code or tests, or honestly UNTRACED. Natural language cannot prove a behaviour absent, and the receipt never pretends it can.

The receipt family

Three tools, one grammar — document = claims, code = evidence, every verdict cited:

  • skill-claims — is the package honest? Audits an agent skill's prose promises against its static behaviour.
  • spec-receipt — does the code still match the spec? Audits a repo's engineering spec against its declared implementation surfaces.
  • done-claim-auditor — did the agent do what it claimed? The same claim-vs-evidence logic pointed at session transcripts.

And before any of them: skill-vet — is it dangerous? Stealing, spying, hidden sending, each proven as a combination of evidence.

Quick start

pipx install spec-receipt              # or: pip install spec-receipt
spec-receipt check path/to/repo

(The PyPI name goes live when v1.0.0 is published; until then use a source checkout: python3 src/spec_receipt.py check <repo>, or pipx install git+https://github.com/jasveerai88/spec-receipt.)

One command, one receipt, an exit code you can gate on — 1 = drift found (at least one item MISSING or CONTRADICTED), 0 = no drift, 2 = input error. Add --json for a machine-readable receipt. Python 3.10+, standard library only, no dependencies.

Which file is the spec, in priority order:

  1. Explicit --spec arguments (repeatable).
  2. A .spec-receipt.toml manifest in the repo root listing specs = [...].
  3. Auto-discovery: SPEC.md / spec.md at root, docs/spec*.md, docs/SPEC.md, specs/**/spec.md, DESIGN.md, API.md. Nothing found → exit 2.

What it checks

Five surface classes, extracted from the spec and indexed from the code, judged in both directions:

Class Declared in the spec as Indexed in the code from
CLI flags --name in backticks, with a stated default ((default: fast)) or choice set (one of `low|high` ) argparse add_argument and click @click.option, with defaults and choices, via AST
CLI commands A ## Command: name heading, a backticked invocation line, an invocation line in a fenced block add_parser subcommands, click commands and groups, via AST
HTTP routes A backticked `GET /path` span, a row in an Endpoints/API table, an exclusivity statement ("only GET on /items") Flask @*.route, FastAPI method decorators and api_route, Django path() / re_path() in urls.py
Config keys A backticked dotted key (server.port) under a Configuration heading, or a key in a fenced TOML/INI example Keys of example/sample/config-named .toml / .ini / .json files, and config[...] / config.get(...) reads with constant keys
Environment variables A backticked SCREAMING_CASE name; negations ("never reads AWS_*") os.environ[...], os.getenv(...), os.environ.get(...) with constant names, incl. the from-import spellings

Plus requirement items: a line carrying an ID (FR-###, REQ-<word>, R-##) is traced to an anchor — the ID verbatim in code or tests (scoped to the ID's own spec), or two or more of its distinctive statement terms co-occurring in one in-scope file.

The verdicts:

Verdict Applies to Meaning
IMPLEMENTED Spec item The named surface exists in code as specified — AST node cited; any stated default/choices also match. For a negation item: the surface is absent from a completely enumerated class, so the negation holds
MISSING Spec item A structured surface item has no counterpart in code — issued only on complete enumeration of its class. Never for requirements, never on partial enumeration
CONTRADICTED Spec item Code proves the opposite of the spec statement, hard evidence on both sides: default vs different default, choices vs different choices, "there is no --x" vs the flag's AST node, a spec'd path registered with other methods only, an env negation vs a matching read
TRACED Requirement item The ID has an anchor in code or tests, scoped to its own spec. TRACED never means "correctly implemented" — the receipt says so every time
UNTRACED Requirement item (or partial class) No anchor, or the class could only be partially enumerated. The honest residue — not MISSING
DOCUMENTED Code surface A spec item names this surface (a surface named by a negation is DOCUMENTED — named, as forbidden; the item carries the CONTRADICTED)
UNDOCUMENTED Code surface The surface exists in code; no spec item names it

Quick start · What it checks · How it compares · Reference

How it compares

spec-receipt chenhz01/spec-drift lilabrooks/spec-drift spec-kit /speckit.analyze
The question Does the code still match the spec? Do spec-kit specs and their test anchors agree? What changed in this diff, per the governing docs? Do the spec, plan and tasks documents agree?
Scope Whole-repo snapshot, post-hoc Spec-kit layout only (specs/NNN/spec.md, FR-###) A diff range (base..HEAD) In-flow, during authoring
What is checked Five surface classes (flags, commands, routes, config, env) + requirement tracing, both directions FR↔test anchors and spec-vs-spec supersession The changed files, against their governing documents Spec ↔ plan ↔ tasks — documents only, never the implementation
Method Deterministic patterns + AST — same repo, same receipt, forever Lexical, zero-dependency, no LLM An LLM judges (Anthropic/OpenAI keys, per-file model calls, API cost) The authoring agent, in the loop
Output Per-item verdicts — MISSING / CONTRADICTED only on complete enumeration or direct conflict — with file + line on both sides "Candidates, never verdicts" (its own README's words) LLM drift judgements on the diff Consistency findings inside the authoring flow
Licence / state MIT, maintained Apache-2.0 Proprietary; its README marks the experiment as concluding / being retired Part of github/spec-kit

(Comparable descriptions from their own public pages and READMEs, checked 2026-11-06. chenhz01/spec-drift is the closest neighbour and honest prior art — the stopword + document-frequency filtering its README found mandatory for term tracing is used here too. The gap spec-receipt fills is the one /speckit.analyze leaves by design: post-hoc, against the code, in any Markdown spec format.)

Honest limits (the short version)

Static patterns are evidence, not proof. Surfaces built purely at runtime may not appear in the index; a class that cannot be fully enumerated is marked partial and its absence verdicts degrade to UNTRACED rather than being upgraded to MISSING; prose requirements are traced, never semantically judged. Extraction is deliberately scoped — a spec's examples of its own notation are not declarations, and some declaration shapes are missed as the price of that (each one is named in the full limits). Every receipt prints its limits in full; the complete list is below.


Reference

Contents: The drifted fixture · The clean fixture · The dogfood gate · For agents · Tests · Honest limits

The drifted fixture — the demo

tests/fixtures/drifted/ is a constructed repo whose code evolved against a frozen SPEC.md: one spec'd flag deleted, one default changed, a forbidden flag added, one endpoint's method changed from POST to GET, one undocumented flag grown, one requirement's tests renamed away. Verbatim output:

$ spec-receipt check tests/fixtures/drifted
spec-receipt 1.0.0 — spec drift receipt
Repo: drifted
Spec: SPEC.md
Classes: flags complete (4 surfaces), commands complete (1 surface), routes complete (1 surface), config complete (5 surfaces), env complete (2 surfaces)

SPEC ITEMS
CONTRADICTED flag          --mode                 spec SPEC.md:5 · code app.py:19
             spec states default `fast`; code default is `slow`
MISSING      flag          --level                spec SPEC.md:6 · code —
             the flags class was completely enumerated and --level is absent from it
IMPLEMENTED  flag          --verbose              spec SPEC.md:7 · code app.py:20
             surface declared in code
CONTRADICTED negation      --legacy               spec SPEC.md:8 · code app.py:21
             spec states there is no --legacy; the flag is declared in code
IMPLEMENTED  command       export                 spec SPEC.md:10 · code app.py:24
             command declared in code
TRACED       requirement   FR-001                 spec SPEC.md:16 · code tests/test_app.py:1
             ID FR-001 appears verbatim in code/tests
UNTRACED     requirement   FR-002                 spec SPEC.md:17 · code —
             no verbatim ID and no file carries two distinctive statement terms
IMPLEMENTED  endpoint      GET /items             spec SPEC.md:21 · code app.py:35
             route declared in code with this method
CONTRADICTED endpoint      POST /items            spec SPEC.md:22 · code app.py:35
             spec states POST /items; the path is registered in code with GET only
IMPLEMENTED  config        server                 spec SPEC.md:28 · code config.example.toml:2
             declared in the config index (example config or code read)
IMPLEMENTED  config        server.host            spec SPEC.md:29 · code config.example.toml:3
             declared in the config index (example config or code read)
IMPLEMENTED  config        server.port            spec SPEC.md:30 · code config.example.toml:4
             declared in the config index (example config or code read)
IMPLEMENTED  config        database               spec SPEC.md:31 · code config.example.toml:6
             declared in the config index (example config or code read)
IMPLEMENTED  config        database.url           spec SPEC.md:32 · code config.example.toml:7
             declared in the config index (example config or code read)
IMPLEMENTED  env           ITEMS_API_KEY          spec SPEC.md:48 · code app.py:48
             read in code
IMPLEMENTED  env           ITEMS_REGION           spec SPEC.md:49 · code app.py:49
             read in code
IMPLEMENTED  env-negation  AWS_*                  spec SPEC.md:50 · code —
             negation holds: no matching variable is read in the completely enumerated env class

CODE SURFACES
DOCUMENTED   flag          --mode                 code app.py:19 · spec SPEC.md:5
DOCUMENTED   flag          --verbose              code app.py:20 · spec SPEC.md:7
DOCUMENTED   flag          --legacy               code app.py:21 · spec SPEC.md:8
UNDOCUMENTED flag          --extra                code app.py:22 · spec —
DOCUMENTED   command       export                 code app.py:24 · spec SPEC.md:10
DOCUMENTED   endpoint      GET /items             code app.py:35 · spec SPEC.md:21
DOCUMENTED   config        server                 code config.example.toml:2 · spec SPEC.md:28
DOCUMENTED   config        server.host            code config.example.toml:3 · spec SPEC.md:29
DOCUMENTED   config        server.port            code config.example.toml:4 · spec SPEC.md:30
DOCUMENTED   config        database               code config.example.toml:6 · spec SPEC.md:31
DOCUMENTED   config        database.url           code config.example.toml:7 · spec SPEC.md:32
DOCUMENTED   env           ITEMS_API_KEY          code app.py:48 · spec SPEC.md:48
DOCUMENTED   env           ITEMS_REGION           code app.py:49 · spec SPEC.md:49

Summary: 17 items (IMPLEMENTED 11, MISSING 1, CONTRADICTED 3, TRACED 1, UNTRACED 1); 13 surfaces (DOCUMENTED 12, UNDOCUMENTED 1)
Verdict: DRIFT

Limits:
  - Stage 3 indexes five surface classes: CLI flags and commands (argparse + click, via AST), HTTP routes (Flask, FastAPI and Django URL declarations, via AST), config keys, and environment variables. Declaration forms outside those patterns are not indexed and never judged.
  - Routes: only declared methods are indexed - a Flask route declared without methods counts as GET (its framework default); the automatic HEAD/OPTIONS a framework adds at runtime are not declarations and are not indexed. Django URL routes declare no method, so they are indexed as ANY-method surfaces: they can satisfy, but never contradict, a method-specific spec item. Blueprint/APIRouter constructor prefixes, register_blueprint prefixes and include_router prefixes are applied only when constant and declared in the same module; add_url_rule registrations are not indexed, and their presence marks the routes class partial.
  - Config keys: presence only - values in example configs are never compared with anything. Indexed: keys of example/sample/config-named .toml, .ini and .json files (pyproject.toml and the manifest excluded) and config[...] / config.get(...) reads with constant-string keys, on a receiver named config. YAML configs cannot be parsed with the standard library and are not indexed; their presence marks the config class partial.
  - Environment variables: only os.environ[...], os.getenv(...) and os.environ.get(...) with constant-string names (including the from-import spellings) are indexed. Names built at runtime mark the env class partial.
  - Extraction scope: sections whose headings announce examples or definitions of the notation itself (Examples, Definitions, Glossary, Notation, 'spec item' forms) and lines that present an example (e.g. / for example) or describe the notation (a backticked token, of the form, written as, a placeholder) are illustrative, not declarative: no items are extracted from them. Text inside straight double quotes is quotation — the document talking about wording, not wording that declares — and is likewise never extracted. Template placeholders are not declarations either: an endpoint path whose every segment is a placeholder word (path, x, foo, ...) and a token naming a capitalisation style (SCREAMING_CASE) name the notation, not a surface. A name the document itself defines as one of its terms — in a definition table with a Meaning column, or in an enumerated list of terms with glosses — is vocabulary, not an environment-variable declaration.
  - Fenced blocks: an untagged block counts as config only when it contains at least one key = value assignment, and a [section] header declares a key only when it stands alone on its line — sample output quoted in a spec is not configuration. Invocation lines in fenced blocks do declare commands, but a line invoking a generic shell or package tool (pip, pipx, python, git, ...) names that tool's own operand, never a command of the tool being spec'd.
  - TRACED means a requirement ID has an anchor in code or tests; it never means the requirement is correctly implemented. Anchors are scoped to the requirement's own spec: an anchor must carry the spec's feature qualifier, sit in a file of the feature, or — when the ID is declared by only one spec in its tree — sit in that tree. A comment block that lists an ID as unanchored, or enumerates several IDs, is a denial or a listing, never an anchor — and its words are not term evidence either. An ID found only outside its scope is UNTRACED, not traced to the wrong place. UNTRACED is not MISSING: natural language cannot prove a behaviour absent.
  - MISSING is issued only when the item's surface class was completely enumerated and the item is absent from it; on partial enumeration an absent item is UNTRACED.
  - Static evidence only: nothing in the target repo is executed, imported or run. Surfaces built purely at runtime may not appear in the index.
  - Python code only. A repo with no Python surfaces reports its classes enumerated to empty.
(exit code 1)

Note the verdict that gives this tool its name in the author's own launch post: the spec says POST /items; the path is registered in code with GET only — CONTRADICTED, cited on both sides (SPEC.md:22 against app.py:35). And note what is not overclaimed: FR-002's tests were renamed away, so it is UNTRACED — reported, counted, and not upgraded to MISSING, because natural language cannot prove a behaviour absent.

The clean fixture — the other direction

tests/fixtures/clean/ declares every surface exactly — 3 flags (one with a stated default, one with choices), a command, 2 requirements, a Flask route pair, 5 config keys, 2 env vars and an env negation — and the code matches, surface for surface. Verbatim output (the Limits block, printed identically on every run, is elided here only):

$ spec-receipt check tests/fixtures/clean
spec-receipt 1.0.0 — spec drift receipt
Repo: clean
Spec: SPEC.md
Classes: flags complete (3 surfaces), commands complete (1 surface), routes complete (2 surfaces), config complete (5 surfaces), env complete (2 surfaces)

SPEC ITEMS
IMPLEMENTED  flag          --mode                 spec SPEC.md:5 · code app.py:16
             surface declared in code; stated default and choices match
IMPLEMENTED  flag          --level                spec SPEC.md:6 · code app.py:17
             surface declared in code; stated default and choices match
IMPLEMENTED  flag          --verbose              spec SPEC.md:7 · code app.py:18
             surface declared in code
IMPLEMENTED  negation      --legacy               spec SPEC.md:8 · code —
             negation holds: --legacy is absent from the completely enumerated flags class
IMPLEMENTED  command       export                 spec SPEC.md:10 · code app.py:20
             command declared in code
TRACED       requirement   FR-001                 spec SPEC.md:16 · code tests/test_app.py:1
             ID FR-001 appears verbatim in code/tests
TRACED       requirement   FR-002                 spec SPEC.md:17 · code tests/test_app.py:1
             ID FR-002 appears verbatim in code/tests
IMPLEMENTED  endpoint      GET /items             spec SPEC.md:21 · code app.py:31
             route declared in code with this method
IMPLEMENTED  endpoint      POST /items            spec SPEC.md:22 · code app.py:36
             route declared in code with this method
IMPLEMENTED  config        server                 spec SPEC.md:28 · code config.example.toml:2
             declared in the config index (example config or code read)
IMPLEMENTED  config        server.host            spec SPEC.md:29 · code config.example.toml:3
             declared in the config index (example config or code read)
IMPLEMENTED  config        server.port            spec SPEC.md:30 · code config.example.toml:4
             declared in the config index (example config or code read)
IMPLEMENTED  config        database               spec SPEC.md:31 · code config.example.toml:6
             declared in the config index (example config or code read)
IMPLEMENTED  config        database.url           spec SPEC.md:32 · code config.example.toml:7
             declared in the config index (example config or code read)
IMPLEMENTED  env           ITEMS_API_KEY          spec SPEC.md:48 · code app.py:42
             read in code
IMPLEMENTED  env           ITEMS_REGION           spec SPEC.md:49 · code app.py:43
             read in code
IMPLEMENTED  env-negation  AWS_*                  spec SPEC.md:50 · code —
             negation holds: no matching variable is read in the completely enumerated env class

CODE SURFACES
DOCUMENTED   flag          --mode                 code app.py:16 · spec SPEC.md:5
DOCUMENTED   flag          --level                code app.py:17 · spec SPEC.md:6
DOCUMENTED   flag          --verbose              code app.py:18 · spec SPEC.md:7
DOCUMENTED   command       export                 code app.py:20 · spec SPEC.md:10
DOCUMENTED   endpoint      GET /items             code app.py:31 · spec SPEC.md:21
DOCUMENTED   endpoint      POST /items            code app.py:36 · spec SPEC.md:22
DOCUMENTED   config        server                 code config.example.toml:2 · spec SPEC.md:28
DOCUMENTED   config        server.host            code config.example.toml:3 · spec SPEC.md:29
DOCUMENTED   config        server.port            code config.example.toml:4 · spec SPEC.md:30
DOCUMENTED   config        database               code config.example.toml:6 · spec SPEC.md:31
DOCUMENTED   config        database.url           code config.example.toml:7 · spec SPEC.md:32
DOCUMENTED   env           ITEMS_API_KEY          code app.py:42 · spec SPEC.md:48
DOCUMENTED   env           ITEMS_REGION           code app.py:43 · spec SPEC.md:49

Summary: 17 items (IMPLEMENTED 15, MISSING 0, CONTRADICTED 0, TRACED 2, UNTRACED 0); 13 surfaces (DOCUMENTED 13, UNDOCUMENTED 0)
Verdict: NO_DRIFT
(exit code 0)

Any CONTRADICTED on this fixture is a release blocker; the suite pins it.

The dogfood gate — this tool was caught lying first

Before v1.0.0, spec-receipt was run against three real spec'd repos — its own design document, skill-claims, and the public spec-kit-layout project chenhz01/spec-drift — with every CONTRADICTED and every TRACED citation hand-checked against the actual code. Run 1 failed the precision bar. Coverage passed (67.5% of items received a deterministic verdict, against a 50% kill line), but all 3 CONTRADICTED verdicts were false in intent: the extractor had read the design document's examples of spec-item forms — an illustrative --mode with a default, a quoted "no --legacy" — as declarations, and formed verdicts against pairs that should never have existed. Requirement anchors were also matched repo-wide, so one feature's FR-001 was "traced" to another feature's tests, and two IDs were traced to a comment block that explicitly lists them as having no anchor.

The fix was one class — extraction scoping, not the verdict engine: illustrative sections and lines, template placeholders, the document's own defined terms, quoted mentions, and sample output no longer form items; fenced invocation lines do form commands; anchors are scoped to the requirement's own spec, and a comment block that denies anchoring is never an anchor. Every failure shape became a regression test. The rerun, unchanged targets and commands: zero false CONTRADICTED on any target, every TRACED citation correctly scoped or honestly UNTRACED, mean coverage 92.6%. The full reports are in the project's working notes (DOGFOOD-SPEC-RECEIPT.md, DOGFOOD-RERUN.md); the disclosed trade-offs of the scoping rules are in Honest limits.

For agents

--json emits one object: the tool and repo record, the per-class enumeration state (complete / surfaces count, with index_notes explaining every partial marking), the items (each with kind, name, spec file + line, code file + line or null, verdict, and basis; flags may carry stated_default / stated_choices; requirements carry text; endpoints carry method and path), the surfaces (each with its verdict and both citations), summary counts that recount exactly against the items and surfaces, the overall verdict, the verdict rule, and the limits statement. Input errors — including argument errors — are a single JSON object with an error key and exit 2. SKILL.md states the full verdict rules and the JSON contract — and makes spec-receipt itself installable as a skill: point your agent at this repo's SKILL.md and it will receipt spec'd repos before trusting them.

Tests

python3 -m unittest discover -s tests

87 tests: both fixtures pinned to the exact verdict multisets above (clean: 17 items — 15 IMPLEMENTED, 2 TRACED — 13 surfaces all DOCUMENTED, exit 0; drifted: 17 items — 11 IMPLEMENTED, 1 MISSING, 3 CONTRADICTED, 1 TRACED, 1 UNTRACED — 13 surfaces, 1 UNDOCUMENTED, exit 1); unit tests for every extractor and verdict rule across all five classes (defaults, choices, method sets, negations, exclusivity, prefix resolution); partial-enumeration degradation (dynamic surfaces degrade absence to UNTRACED, never MISSING); the error contract (no spec, unreadable spec, unparseable files — clean {"error": ...}, exit 2, never a traceback, in text and --json modes); the stage-3 regression tests, one per dogfood failure shape; and the safety proofs — nothing in a target repo is ever executed, fixture trees are byte-identical after runs, output is byte-identical across runs, and the source's imports are pinned to a stdlib set with no network or subprocess module.

Honest limits

  • Static evidence only — runtime behaviour is out of scope. Receipt reads the repo's text and AST; it never runs anything. Surfaces built purely at runtime (routes registered from data, flags built in loops from non-constant names) may not appear in the index. Where the tool can see a class cannot be fully enumerated, it marks the class partial and absence verdicts for it degrade to UNTRACED.
  • CONTRADICTED is reserved for hard evidence. A spec statement flips only on a direct conflict cited on both sides. A stated parameter whose code value is not statically resolvable does not contradict — the surface is reported as matching, with the parameter unverifiable by construction (non-constant choices never contradict a stated set; an absent restriction does, because the code provably accepts more than the spec allows).
  • Requirements are traced, never judged. There is no verdict for "this prose requirement is correctly implemented" and none for "it is absent". TRACED means an anchor exists, scoped to the ID's own spec — never that the requirement is met.
  • Routes: only declared methods are indexed. A Flask route declared without methods counts as GET (its framework default); the automatic HEAD/OPTIONS a framework adds at runtime are not declarations. Django routes declare no method and are indexed as ANY-method surfaces: they can satisfy, but never contradict, a method-specific item. Blueprint/APIRouter constructor prefixes, register_blueprint and include_router prefixes are applied only when constant and in the same module; add_url_rule registrations are not indexed, and mark the routes class partial.
  • Config keys are presence only. Values are never compared with anything. Only example/sample/ config-named .toml, .ini and .json files and config[...] / config.get(...) reads with constant-string keys on a receiver named config are indexed; other receiver names are refused, and YAML configs cannot be parsed with the standard library — their presence marks the class partial.
  • Environment variables: constant names only. Only os.environ[...], os.getenv(...) and os.environ.get(...) with constant-string names (including the from-import spellings) are indexed. Names built at runtime mark the env class partial.
  • Extraction scope — the stage-3 trade-offs, disclosed. Sections whose headings announce examples or definitions of the notation itself (Examples, Definitions, Glossary, Notation, "spec item" forms) and lines that present an example ("e.g." / "for example") or describe the notation are illustrative, not declarative — no items are extracted from them; the price is that a genuine declaration sharing a line with "e.g." is missed too. Text inside straight double quotes is quotation and is never extracted. Template placeholders are not declarations: an endpoint path whose every segment is a placeholder word (/path, /x) names the notation, not a surface — so a literal route made only of placeholder words is template text to the tool. A name the document itself defines as one of its terms (a definition table with a Meaning column, an enumerated list of terms with glosses) is vocabulary, not an env-var declaration — and a term enumeration with parenthetical glosses is read the same way, so env vars declared in exactly that shape are missed. Sample output quoted in a spec is not configuration: an untagged fence counts as config only with at least one key = value assignment, and a [section] header declares a key only when it stands alone on its line.
  • Requirement anchors are scoped. An anchor must carry the spec's feature qualifier (e.g. FR-001 (001)), sit in a file of the feature, or — when the ID is declared by only one spec in its tree — sit in that tree. A comment block that lists an ID as unanchored, or enumerates several IDs, is a denial or a listing, never an anchor — and its words are not term evidence either. An ID found only outside its scope is UNTRACED, not traced to the wrong place.
  • Python implementations only. Other languages are not half-scanned; a repo with no Python surfaces reports its classes enumerated to empty, plainly.
  • No git/diff mode, no test running, no auto-fix, no spec rewriting, no PR comments, no score. A receipt, not a repair tool — and never a drift percentage or a grade.

Contributing

See CONTRIBUTING.md. The evidence discipline is the product: a verdict is issued only on hard evidence, cited on both sides, and contributions are held to that. Found a repo spec-receipt misjudges — a declaration it misses, a verdict the evidence doesn't support? Open an issue with the repo (or a minimal fixture); that is exactly the feedback that improves the extraction library.

Security

spec-receipt is itself an audit tool, so its own posture is simple: it has no network code, no subprocess code, no dependencies, and it never executes what it scans. See SECURITY.md for how to report a problem.

License

MIT — see LICENSE.

  • skill-claims — the honesty half of the receipt family: does a skill package do what its description claims?
  • done-claim-auditor — the same claim-vs-evidence logic pointed at agent sessions: audits a coding agent's completion claims against its own transcript.
  • skill-vet — the pre-install danger gate: stealing, spying, hidden sending, proven with file-and-line evidence.
  • permission-audit — audits a Claude Code permission allowlist for wildcard grants and dangerous modes before they hurt you.

Support

If spec-receipt catches your code drifting away from your spec, consider sponsoring.

Metadata

Release files for spec-receipt 1.0.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for spec-receipt 1.0.0
File Size Uploaded Provenance Actions
spec_receipt-1.0.0.tar.gz 66.2 kB With provenance Details for spec_receipt-1.0.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for spec-receipt 1.0.0
File Interpreter ABI Platform Actions
spec_receipt-1.0.0-py3-none-any.whl Python 3 none any Details for spec_receipt-1.0.0-py3-none-any.whl

Total release size: 106.0 kB

Release files / spec_receipt-1.0.0.tar.gz

Download URL spec_receipt-1.0.0.tar.gz
Size 66.2 kB
Tags Source
SHA-256 checksum
How to use checksums
629feb38967247d10dbd0a9a20c27d1318cac15beeb8a56ceea31149a830e16a
BLAKE2b-256 checksum
How to use checksums
d21e5cdfe328d10c1183a419d4fa3c5c91b12427c716d1f95fcbbdc0a0283928
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 7, 2026.

Transparency log

Release files / spec_receipt-1.0.0-py3-none-any.whl

Download URL spec_receipt-1.0.0-py3-none-any.whl
Size 39.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
ac8b6cd9bed9ac674d8b078469358bbab464f6b06569e5a5431e52121a326dc4
BLAKE2b-256 checksum
How to use checksums
b0c2c2cf6bbc7995641cca8eb48efb8796e66467ea6266711ba3671d6887addd
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 7, 2026.

Transparency log

Security for spec-receipt 1.0.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

Both files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for spec_receipt-1.0.0.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 7, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

1.0.0 This release

2 release files Full provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page