MeDiVa
DocsUsing mediva

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 is syntax; whether its body says enough is content.
  • 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 Diagnostics

A 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 work

External 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 target

Diagnostics

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 rules

Migrating 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.

On this page