Skip to content

Why markdown-contract

Teams keep their durable knowledge in markdown — decision records, runbooks, planning docs, changelogs. It is the cheapest format people will actually keep writing: plain text, diffs well, works in every editor.

The trouble starts when you need to rely on those documents:

  • Does every decision record actually have a Decision section?
  • Is status one of the values your tooling expects?
  • Can a script read the Files to touch table without hand-rolled parsing?

Today the answers come from ad-hoc regex, a bespoke linter per repo, or a heavyweight CMS that takes the documents away from plain markdown. Existing markdown tooling validates frontmatter and then transforms the body — none of it lets you declare what a document’s body must look like and check it. markdown-contract fills that gap.

You declare a contract per document type: the frontmatter fields, the section structure, the shape of tables and lists, and any cross-cutting rules. From that single declaration you get both:

  • Validation — findings with path:line positions, ready for a terminal, JSON pipeline, or SARIF upload to code scanning.
  • A typed model — the same contract that checks a document also types it, so doc.frontmatter.status and doc.body.summary.text() are ordinary typed reads, not string spelunking.

Check and consume are never two definitions that drift apart; they are one.

A few forces shaped how the library works:

  • Findings you can act on. Every mechanism reports the same finding shape, positioned to the source line. Severity (error / warn / report) is declared in the contract, not chosen at the call site, so a rule can’t be strict in one place and lax in another by accident.
  • The right tool per axis. A schema language can’t express “these sections, in this order, with optional gaps” — and a tree grammar is poor at datatypes. So markdown-contract uses both where each is strong: a small section grammar for structure, Zod for content, and named rules for everything cross-cutting. See How it works.
  • Read-only by design. The validator never rewrites your documents. Formatting, repair, and normalization are deliberately someone else’s job.
  • Generic engine, declarative corpus. The engine carries no knowledge of any particular repository. A markdown-contract.yaml maps directories and globs to contracts, so validating a whole docs tree is configuration, not code — and simple contracts need no TypeScript at all.
  • Not a formatter. Line length, wrapping, whitespace, and table padding belong to a formatter. markdown-contract asserts presence and shape, never placement and whitespace.
  • Not a CMS or site generator. Your files stay plain markdown in your repo; markdown-contract only validates and reads them.
  • Not a template engine. Contracts validate documents (including your scaffolding templates); they never generate content.

Next: How it works for the technical approach, or Getting started to run it.