Lint a contract for internal-consistency defects — leftover placeholders, broken cross-references, undefined/unused defined terms, numbering gaps, party-name and date inconsistencies — with CI-gateable exit codes. Deterministic, stdlib-only. Also composes with the contract-ops suite.
Project description
contract-lint
Lint a contract for internal-consistency defects — before anyone signs it. Point it at a draft and it flags leftover placeholders, cross-references that go nowhere, defined terms that are never defined (or never used), duplicate definitions, gaps in section numbering, party names spelled three different ways, and impossible dates — each as a finding with a stable rule id, a severity, and a line. It exits non-zero when something is wrong, so a CI pipeline or an agent can gate on it.
Deterministic and offline: no model, no network, no telemetry. The same input always produces the same byte-for-byte report, so you can diff it in CI.
- Stdlib only. Zero runtime dependencies; the core is fully functional with nothing installed.
- Single file.
contract_lint_cli.py— no DB, no daemon, no SaaS. - CI-gateable.
--check(exit-code only),--fail-on error|warning|none,--json, and--sariffor code-scanning. - Reads the text.
.md/.txt/.htmlnatively;.docx/.pdfvia optional extras. It lints the original numbering, cross-references, and defined-term casing — not a normalized model.
Part of the contract-ops suite — optional. contract-lint stands on its own, but it also composes with the contract-ops CLI suite: it is the pre-signature quality gate the suite was missing — where
compare-cligates drift between versions, contract-lint gates defects within one document. It shares the suite's agent conventions (--catalog json,--json, exit codes) and can lint a draft or extract-cli's source text. Seedocs/INTEROP.md.
Run this
pipx run contract-lint demo # zero-config: lint a deliberately-flawed sample contract
# or, installed: pip install contract-lint && contract-lint demo
That lints a bundled, deliberately-broken contract (no file, no network, no model) and prints every kind of finding. Then point it at your own draft:
contract-lint your-contract.md
contract-lint your-contract.md --check && echo "ready to sign" # exit-code gate
Where to go next
- New here? Keep reading — Quick start and The rules.
- Driving it from an agent? See
AGENTS.md. Callcontract-lint --catalog jsonat startup to discover commands/flags, andcontract-lint rules --jsonto discover rule ids (don't hardcode them). The--jsonreport followsdocs/spec/lint-output.schema.json. There's also an MCP server (lint_contract/list_rules/lint_demotools). - Wiring it into CI?
docs/recipes/— a GitHub Action (with code-scanning), a pre-commit hook, and gating any CI/shell. - Wiring it into the pipeline?
docs/INTEROP.md— it sits as a pre-signature gate (after draft-cli, alongside compare-cli, before sign-cli) and emits SARIF. - Contributing?
CONTRIBUTING.mdandARCHITECTURE.md.
Install
pip install contract-lint # zero dependencies, fully functional on .md/.txt/.html
pip install "contract-lint[docx]" # + .docx reading (pulls in extract-cli's Word backend)
pip install "contract-lint[pdf]" # + .pdf reading (pulls in extract-cli's PDF backend)
Requires Python 3.9+. .docx/.pdf reading is delegated to
extract-cli's document backends; the core
works without them on .md / .txt / .html (or text piped on stdin). You can also
convert first: extract contract.pdf | contract-lint -.
Quick start
contract-lint demo # lint the bundled flawed sample
contract-lint draft.md # human table report
contract-lint draft.md --json | jq .summary
contract-lint draft.md --sarif > lint.sarif
cat draft.md | contract-lint - --format md # read from stdin
# Lint several at once (merged report, worst exit code wins):
contract-lint contracts/*.md --check
# CI gate: fail the build only on errors (default), or on warnings too:
contract-lint draft.md --check # exit 0 clean / 1 if errors / 2 unreadable
contract-lint draft.md --check --fail-on warning
The rules
Each finding is { rule, severity, message, line, column?, excerpt }. Discover them
live with contract-lint rules --json.
| Rule | Severity | Default | What it catches |
|---|---|---|---|
placeholder |
error | on | Leftover unfilled placeholders: [Bracketed], {{mustache}}, <ANGLE>, ____ blanks, TBD, [•]. |
broken-xref |
error | on | A cross-reference (Section 7.2, Article IV, Exhibit B, Schedule 2.1, clause 9.3, Annex 1) that points to something not present in the document. |
undefined-term |
warning | off | A capitalized defined-style term used but never defined. Off by default (proper-noun-prone); enable it for documents with a formal definitions section. |
unused-definition |
warning | on | A term defined but never used afterward. |
double-definition |
warning | on | A term defined more than once. |
numbering |
warning | on | Gaps or duplicates in a heading-number sequence. |
duplicate-heading |
warning | on | Two headings with the same title (often a copy-paste left unedited). |
party-consistency |
warning | on | Defined party names used with variant spellings (Acme Corporation vs Acme Corp. vs ACME Corporation). |
date-sanity |
warning | on | Impossible or inconsistent dates (malformed, or an expiration before the effective date). |
number-consistency |
warning | on | A written-out amount that disagrees with its parenthetical figure, e.g. thirty (45) days. |
signature-block |
warning | off | A complete-looking contract with no signature/execution block. Off by default (most useful as a final pre-signature check). |
undefined-term and signature-block ship off — the former is the most
false-positive-prone, the latter is opinionated and noisy on fragments. Enable either per
run (--enable undefined-term) or in config.
Output & exit codes
- Default: a human-readable table on stdout. Errors/warnings about the run (unreadable
file, bad usage),
--whyrationale, and the demo banner go to stderr. --json: the locked, schema'd report on stdout (docs/spec/lint-output.schema.json). No timestamp — the report is byte-stable and diffable.--sarif: SARIF 2.1.0 on stdout for GitHub code-scanning / CI annotations (docs/spec/lint-sarif.schema.json). Mutually exclusive with--json.--check: print nothing; communicate purely via the exit code.
| Code | Meaning |
|---|---|
0 |
Clean — no findings at or above --fail-on. |
1 |
Gate tripped — findings at or above --fail-on (default: error). |
2 |
Bad usage / unreadable input (no such file, bad flag, --json + --sarif, a .pdf without the extra). |
--fail-on error (default) trips on errors only; --fail-on warning trips on any
finding; --fail-on none never trips (report-only). Branch on the exit code, not on the
human-readable message.
Configuration
Optional, in precedence order (later wins): suite-wide
~/.config/contract-ops/contract-lint.json → project .contract-lint.json (found by
walking up from the linted file, like git) → --config PATH → --enable/--disable
flags. See config/contract-lint.json.example:
{
"rules": {
"undefined-term": { "enabled": true }, // turn the opt-in rule on
"placeholder": { "enabled": true, "severity": "error" },
"party-consistency": false // disable a rule
},
"ignore": ["Force Majeure", "Annex [0-9]"] // drop findings whose message matches these regexes
}
contract-lint draft.md --enable undefined-term --disable numbering
Inline suppression
A comment on (or above) a line suppresses findings there — eslint-style:
Fee is {{rate}} per hour. <!-- contract-lint: disable-line placeholder -->
<!-- contract-lint: disable-next-line broken-xref -->
See Schedule 9 for details.
<!-- contract-lint: disable-file undefined-term -->
disable / disable-line (this line), disable-next-line (the next line),
disable-file (the whole document). Name one or more rule ids, or omit them to suppress
all rules. Only known rule ids are honored, so trailing comment syntax (-->, */) is ignored.
Composability
# Lint a draft straight out of draft-cli, before rendering/signing:
draft fill nda --vars vars.json | contract-lint - --format md --check
# Lint the source text of any document extract-cli can read:
extract counterparty.pdf | contract-lint - --format md
# Gate a whole folder of drafts in CI (fail on the first defective one):
for f in contracts/*.md; do contract-lint "$f" --check || exit 1; done
# Emit SARIF for GitHub code-scanning:
contract-lint draft.md --sarif > contract-lint.sarif
In a GitHub workflow, the bundled action lints, uploads SARIF to code-scanning, and gates:
- uses: DrBaher/contract-lint-cli@v0.2.0
with: { paths: contracts/, fail-on: error } # needs permissions: security-events: write
It's also a pre-commit hook (id: contract-lint). See docs/recipes/.
Shell completion
eval "$(contract-lint completion bash)" # bash
eval "$(contract-lint completion zsh)" # zsh
(There is also a hidden contract-lint __complete … helper that the scripts call.)
Interop
The cross-CLI contracts live under docs/spec/ as JSON Schema 2020-12 and
are registered in docs/INTEROP.md:
--jsonreport:lint-output.schema.json- rule catalog:
rules.schema.json(rules --json) - SARIF:
lint-sarif.schema.json(--sarif)
contract-lint never calls a model. A future opt-in LLM rule (off by default, never on a hot
path) would reuse the suite's shared ~/.config/contract-ops/llm.json.
Development
make install # editable install with dev extras
make test # full test suite
make typecheck # mypy --strict
make coverage # tests under coverage + report
make build # wheel + sdist
make smoke # build, install the wheel in a clean venv, run it
make spec-check # validate --json/SARIF/rules output against docs/spec schemas (offline)
See ARCHITECTURE.md and CONTRIBUTING.md.
License
MIT © DrBaher
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file contract_lint-0.2.0.tar.gz.
File metadata
- Download URL: contract_lint-0.2.0.tar.gz
- Upload date:
- Size: 58.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9bddb086e73229bf04bb705c0c7f7f42ff371bcd8ef223fe97572f99d5bcdba1
|
|
| MD5 |
f8f5ec4bdfab88b00b0a6f67bbc27a1d
|
|
| BLAKE2b-256 |
56f385fda43aebdd283fa384c85368e2ad640271993a60335a13d4da9b8f2d0e
|
File details
Details for the file contract_lint-0.2.0-py3-none-any.whl.
File metadata
- Download URL: contract_lint-0.2.0-py3-none-any.whl
- Upload date:
- Size: 28.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f2a9060d71025260d2be353ab3588236efc68322186bc70af6bf1e53663fa2bf
|
|
| MD5 |
1ad64fcd94c9ba169ddec8961e6e50b2
|
|
| BLAKE2b-256 |
ca584e88769be85bfaf5c76e5c9dfa8b56ed1da06673c422785f3323438dcf4d
|