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: section required minWords=20 noPlaceholder -->
## Summary
<!-- mdv: endsection -->
`);
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 state still has unfinished work — that's the point:
schema.validateSyntax(md).success // true — shape is correct
schema.validateState(md).success // true — but not finished:
schema.validateState(md).warnings // [unchecked-items] — a box is still unchecked; a human owns that workAttestation tripwires like allChecked sit at warn by default (signal, not verdict): success
stays true and the finding lands in warnings. A contract that promotes the check
(allChecked error=unchecked-items) moves the diagnostic into error.issues and flips success
to false. Grounded membership checks (a name outside a declared source) error by default.
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.
Grounding: schema.bind()
A contract whose rules reference logical source names (<issue|in: openIssues?>,
covers.in=changedFiles?) is bound programmatically with schema.bind() — the embedder hands
values, files, or async loaders in memory, keyed by the names the template spells:
const bound = await schema.bind({
sources: {
openIssues: ["123", "456"], // literal data
changedFiles: async ({ signal }) => fetchFiles(signal), // loader; AbortSignal threaded
},
context: { breaking: false },
});
const report = bound.validate(md); // sync — loaders resolved once at bindStrict by default: a misspelled binding key (binding-unknown) or an unbound required ref
(binding-missing) throws a GroundingError collecting every wiring issue — at bind time,
never as a silently shape-only validation. { partial: true } reports issues instead of
throwing (editors/previews). Loader entries take { kind: "loader", load, timeoutMs, onUnavailable }; onUnavailable: "skip" degrades optional refs on acquisition failure but
can never satisfy a required one. See the grounded templates guide
for the full model — spellings, requiredness, precedence, record/replay.
The older resolveInjectedSources export is a compatibility adapter over the same machinery;
new integrations should bind through the schema.
mediva/grounding: declaration shapes and source status
The mediva/grounding subpath exports the lower-level machinery schema.bind() and the CLI's
sources.declare are both built on. A DeclaredSourceEntry is more than a bare path — it can
project a source to a typed slice and set its value-identity policy:
import type { DeclaredSourceEntry, SourceCompareMode } from "mediva/grounding";
// members: object-catalogue projection; records: ordered tuple projection.
// compare ("exact" | "folded" | "mention") is settable on either, or on a bare
// path via { path, compare } — omitted always means "mention" (today's behavior).
const reviewers: DeclaredSourceEntry = { path: "./team.json", members: ["login"], compare: "exact" };
const metrics: DeclaredSourceEntry = { path: "./metrics.json", records: ["metric", "period", "unit", "value"] };computeSourceStatus(declaredNames, referenced, resolvable) derives each declared name's
structured status — the data layer mirroring mdv sources list/doctor — without any CLI
formatting:
import { computeSourceStatus, type SourceStatusEntry } from "mediva/grounding";
const statuses: SourceStatusEntry[] = computeSourceStatus(
["openIssues", "changedFiles"],
new Map([["openIssues", { optional: false }]]), // discovered contract references
new Set(["openIssues"]), // names that currently resolve
);
// [{ name: "openIssues", state: "active" }, { name: "changedFiles", state: "dormant" }]Each entry's state is one of "active" (referenced and resolvable), "required-absent"
(referenced without ?, not resolvable — should fail a run), "optional-absent" (referenced,
every reference marked ?, not resolvable — honest-null, never a failure), or "dormant"
(declared but referenced by no discovered contract). See the
grounded templates guide for the full projection and compare-mode model.
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 rulesPlugins
compile(template, { plugins }) and validate(md, schema, ctx, { plugins }) both accept a plugins array of PluginManifest — resolved into a plugin-extended registry before the template compiles, so plugin rules, diagnostics, and severity overrides behave exactly like core ones:
import { compile } from "mediva";
import plugin from "mediva-plugin-compliance";
const schema = compile(template, { plugins: [plugin] });See Authoring plugins for how to build a manifest with definePlugin, and the mediva/testing subpath (expectDiagnostics, runCorpus) for testing one.
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.