Getting started
markdown-contract is a Node ESM package (Node ≥ 20) with a library API and a
markdown-contract CLI. It is not yet published to npm — for now, build it
from a checkout of the
source repository:
git clone https://github.com/sksizer/markdown-contractcd markdown-contractbun install # resolve the workspacebunx moon run core:build # tsc → packages/core/dist (library + CLI bin)The publishable package lives in packages/core; link it into your project
with npm link / bun link from there, or run the CLI directly via
packages/core/dist/cli/index.js.
Validate from the terminal
Section titled “Validate from the terminal”Point validate at a folder. Config is auto-discovered
(markdown-contract.yaml in the working directory), or passed explicitly:
markdown-contract validate ./decisions # uses markdown-contract.yamlmarkdown-contract validate ./decisions --contract decision.contract.yamlmarkdown-contract validate ./docs --format sarif > results.sarif # or --format jsonFindings print as path:line level id — message. Exit codes are CI-ready:
0 clean, 1 error-level findings, 2 usage or config error.
The CLI reference lists every command, flag, and exit code, and the validate examples walk each step with real transcripts.
Or let init write the config for you
Section titled “Or let init write the config for you”init reads an existing folder of markdown and infers a tight-but-accepting
config — then immediately re-validates the folder against what it wrote:
markdown-contract init ./docs # writes markdown-contract.yaml, self-checksmarkdown-contract init ./docs --dry-run # print what would be writtenmarkdown-contract init ./docs --check # CI drift guard: fail if docs outgrew the configDeclare a contract in YAML — no code
Section titled “Declare a contract in YAML — no code”Simple contracts need no TypeScript. A contract document declares frontmatter fields and body sections — the YAML reference is the full field catalog, and the authoring examples cover tables, text constraints, and repeatable sections:
mcVersion: 2kind: contractfrontmatter: type: object required: [id, status] properties: id: { type: string, pattern: '^D-\d{4}$' } status: { enum: [proposed, accepted, superseded] }body: additionalSections: true sections: - section: Summary - section: DecisionA config document maps globs to contracts for a whole corpus:
mcVersion: 2kind: configcontracts: decision: ./decision.contract.yamlrules: - include: ["decisions/**/*.md"] contract: decisionOr author it in TypeScript
Section titled “Or author it in TypeScript”The code API (full surface in the API reference) adds what YAML can’t express: arbitrary Zod schemas, nested grammars, and custom rules — and it types the document for reading:
import { contract, sections, section, optional, maxWords } from "markdown-contract";import { z } from "zod";
const decision = contract({ frontmatter: z.object({ id: z.string().regex(/^D-\d{4}$/), status: z.enum(["proposed", "accepted", "superseded"]), }), body: sections({ order: "strict", allowUnknown: true }, [ section("Summary", { content: maxWords(120) }), section("Decision"), optional(section("Consequences")), ]),});
// Validate: findings with positions, never throws.const result = decision.validate(src, { path: "decisions/D-0001.md" });
// Read: the same contract returns the typed model (or throws ContractError).const doc = decision.read(src, { path: "decisions/D-0001.md" });doc.frontmatter.status; // "proposed" | "accepted" | "superseded"doc.body.Summary.text(); // the Summary section's prose, keyed by its headingThe typed model goes much further — iterable typed table rows, anchor lookups, nested section navigation. The model reference is the full surface; the read examples show it in use.
Embed it
Section titled “Embed it”The CLI is a thin shell — everything it does is a library call away. Validate
a corpus from your own tooling with runCorpus (see the API
reference):
import { defineConfig, runCorpus } from "markdown-contract";
const config = defineConfig({ rules: [{ include: ["decisions/**/*.md"], contract: decision }],});const { findings, exitCode, stats } = runCorpus(config, { cwd: repoRoot });Where next
Section titled “Where next”The appendix examples are the fastest way to learn the rest: four small groups of worked, verified examples — validate from the terminal, author contracts in YAML, read markdown as typed data, and automate and embed.
When you need exhaustive detail, the reference section is the spec: CLI, YAML config & contracts, library API, typed model, findings, dialect, and the glossary.