Skip to content

One contract, two doors

read-01 · TypeScript

The contract that validates a document also types it: validate() returns findings as data, read() returns the typed model or throws.

Builds on: nothing — start here in Read markdown as typed data.

A TypeScript program against the library API; inline comments show the resulting values and behavior.

import { contract, sections, section } from "markdown-contract";
import { z } from "zod";
const decision = contract({
frontmatter: z.object({ status: z.enum(["proposed", "accepted"]) }),
body: sections({ order: "strict", allowUnknown: true }, [
section("Summary"),
section("Decision"),
]),
});
// Door 1 — validate: findings as data, never throws. The shape CI wants.
const result = decision.validate(src, { path: "decisions/D-0001.md" });
result.findings; // Finding[] — every plane, sorted by source position
result.doc; // the typed model, present iff no error-level finding
// Door 2 — read: the typed model, or a thrown ContractError. The shape a consumer wants.
const doc = decision.read(src, { path: "decisions/D-0001.md" });
doc.frontmatter.status; // "proposed" | "accepted" — typed by the schema above
doc.body.Summary.text(); // the Summary section's prose, keyed by its heading
  • contract() with a Zod frontmatter schema
  • validate() vs read()
  • typed frontmatter and section text