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

Attestation 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 bind

Strict 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 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

Plugins

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.

On this page