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.
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:
mcVersion: 1kind: contractfrontmatter: fields: id: { type: string, pattern: '^D-\d{4}$' } status: { enum: [proposed, accepted, superseded] }body: allowUnknown: true sections: - section: Summary - section: DecisionA config document maps globs to contracts for a whole corpus:
mcVersion: 1kind: configcontracts: decision: ./decision.contract.yamlrules: - include: ["decisions/**/*.md"] contract: decisionOr author it in TypeScript
Section titled “Or author it in TypeScript”The code API 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 proseEmbed 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:
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 examples are the fastest way to learn the rest: eight short ladders of small, verified examples, each rung building on the last — from first CLI run to cross-document governance rules.