TypeScript API
Compile a contract and validate Markdown programmatically, split into syntax (shape) and state (attestation/content) so LLM autofix is safe by construction.
Cooper: "TARS, what's your honesty setting?" TARS: "Ninety percent." Cooper: "Ninety?" TARS: "Absolute honesty isn't always the most diplomatic… but I'll never tick a box I didn't earn."
mediva splits validation the way TARS splits his settings: shape is one dial, honesty is another. The form of a document can be corrected by a machine; whether the work behind it actually happened cannot. The API makes that split explicit so an autofixer can never be asked to lie.
mediva's programmatic surface is Zod-shaped: compile a contract, then validate a document. Validation is split by concern so you can ask exactly what you mean.
import { compile } from "mediva";
const schema = compile(`
<!-- mdv: block required minWords=20 noPlaceholder -->
## Summary
<!-- mdv: endblock -->
`);
const result = schema.validate(markdown);
if (!result.success) console.error(result.error.flatten());Concerns: syntax vs state
Every rule has a concern, surfaced on each diagnostic:
syntax— FORM / shape (heading level, exact checklist labels, fences, ordering, enums, and a required section being present and non-empty). Meaning-preserving; safe to auto-fix.content— real substance must exist in a present section (minWords, no placeholders). The section's mere presence issyntax; whether its body says enough iscontent.attestation— a checkbox or selection asserts real work was done (allChecked,oneChecked).external— depends on an external system (issueState).
content, attestation, and external are state — obligations a human owns. They can't be honestly satisfied by editing text, so they are never handed to an autofixer. This is TARS's honesty dial: you can reformat the report all day, but you cannot type your way into having actually run the tests.
The methods
schema.validateSyntax(md) // FORM only — the safe surface for LLM autofix
schema.validateState(md, { include }) // content + attestation + external (configurable)
schema.validate(md, { include }) // everything; include: "syntax" | "state" | "all" | concern[]Each returns the Zod-shaped result plus byConcern buckets:
const r = schema.validate(md);
// { success: true, data, warnings, byConcern }
// { success: false, error, warnings, byConcern } // error.issues are DiagnosticsA document can pass syntax while still failing state — that's the point:
schema.validateSyntax(md).success // true — shape is correct
schema.validateState(md).success // false — a checkbox is still unchecked; a human must finish the workExternal rules use the same context channel as the CLI's --context file. For example, issueState=open checks a Fixes #N reference against context.issues; without context it reports missing-context:
schema.validateState(md, {
context: { issues: [{ number: 123, state: "open" }] },
include: ["external"],
});You can also pass context.title for the title* rules. There is no first-#-heading fallback: a title rule with no context.title reports missing-title, so populate context.title yourself (e.g. from the document's H1, or from a PR/issue title) when you want those rules to run.
Inspecting document state
schema.inspect(md) returns a structured DocumentState, the form-like, per-field counterpart to the flat Diagnostic[] from validate(). Use it to drive a UI or inspector; the playground follows this shape.
const state = schema.inspect(md);
// {
// valid: boolean, // no error-severity diagnostics anywhere
// shapeValid: boolean, // no syntax/shape errors
// stateValid: boolean, // no content/attestation/external errors
// fields: FieldState[], // one entry per declared field, in contract order
// document: Diagnostic[], // document-scope diagnostics (frontmatter, structure)
// }Each FieldState is one form input's status — enough to render a per-section verdict without parsing the flat diagnostics yourself:
// FieldState = {
// label: string; // the section's heading text
// kind: FieldKind; // section, table, list, code, media, …
// present: boolean; // the section exists in the document
// empty: boolean; // present but unfilled (the kind's own empty-state opinion)
// shape: Verdict; // skeleton well-formedness; "skipped" when absent
// state: Verdict; // content/attestation/external; "skipped" when absent or empty
// diagnostics: Diagnostic[];// this field's own diagnostics, in emission order
// }A Verdict is one of "valid" | "invalid" | "skipped" — skipped meaning the check couldn't run (the section is absent, or empty so there's nothing to inspect). So shape/state are the two concern axes as a per-field verdict, and a UI can show a section as shape-clean but state-pending — exactly the split autofix acts on versus what a human still owns.
LLM autofix: use validateSyntax
When repairing a document with an LLM, validate with validateSyntax and feed only those errors to the model. Attestation/content/external failures are never in the prompt, so the model cannot be asked to fake state (tick a box it didn't earn, invent filler) — safe by construction, not by prompt wording. The robot literally cannot lie, because it's never handed the question.
let r = schema.validateSyntax(md);
if (!r.success) {
md = await llmFix(md, r.error.message); // only FORM errors reach the model
r = schema.validateSyntax(md);
}
// Then surface state for a human:
const state = schema.validateState(md); // not an autofix targetDiagnostics
error.flatten() returns a Zod-shaped { formErrors, fieldErrors }. Each diagnostic carries a stable code, message, line, severity, and (under provenance, the default) its concern and fix applicability — the same codes the CLI emits.
Rendering: renderMarkdown
renderMarkdown(template) strips the <!-- mdv: … --> directives out of a contract and returns plain
Markdown — the human-readable view of a .mdv.md file (the same thing mediva render emits on the CLI):
import { renderMarkdown } from "mediva";
const plain = renderMarkdown(contract); // contract minus its hidden rulesMigrating from safeParse / parse
schema.safeParse(md) and schema.parse(md) remain available and run full validation (all concerns), but are deprecated — prefer validate(md) for the same behavior, and validateSyntax / validateState to be explicit.
When to use the API over the CLI
Reach for the API when validation is part of a build step, a test, or a server — e.g. checking LLM output before you persist or render it (the docking computer that won't let a malformed payload aboard). See Validate LLM output before persisting. Use the CLI to gate files in CI.