Authoring plugins
Add namespaced rules, diagnostics, and leaf tags to mediva without forking it — package them as a plugin.
Request permission to dock a third-party module, Captain. MOTHER doesn't need to know every ship in the fleet by name — she just needs the module to declare its callsign, pass inspection, and speak her protocol. A mediva plugin is that module: your own rules, your own diagnostics, loaded into the same registry MOTHER validates against.
A plugin adds rules, diagnostics and leaf tags that behave exactly like core ones. Start from tests/.
A minimal plugin
// src/index.mjs
import { definePlugin, defineFieldRule } from "mediva";
import { defineLeaf } from "mediva/authoring";
const noWeasel = defineFieldRule({
name: "compliance/noWeasel",
scope: "field",
syntax: "flag",
category: "content",
status: "stable",
order: 10,
doc: {
summary: "Reject vague marketing weasel words (leverage, synergy, world-class) from the body.",
example: "<!-- mdv: section compliance/noWeasel -->",
},
fix: "none",
diagnostics: ["compliance/weasel-words"],
run(ctx) {
const prose = ctx.target.proseText();
for (const match of prose.matchAll(/\b(leverage|synerg\w+|world-class)\b/gi)) {
ctx.report(
"compliance/weasel-words",
`The "${ctx.target.label}" section uses the weasel word "${match[0]}" — say the specific, verifiable thing instead.`,
);
}
},
});
const signoff = defineLeaf("compliance/signoff", {
locator: { kind: "linePrefix", label: "Signed-off-by:" },
rules: { "content.none=@placeholders": true },
docs: {
summary: "A compliance attestation line.",
example: "<!-- mdv: compliance/signoff required -->",
},
});
export default definePlugin({
name: "compliance",
apiVersion: 3,
fieldRules: [noWeasel],
pluginLeaves: [signoff],
diagnostics: {
"compliance/weasel-words": {
severity: "error",
title: "Weasel word",
suggestion: "Replace the vague marketing word with the specific, verifiable claim it's standing in for.",
gate: "state",
},
},
});Use both like core atoms, and tune severity like core codes (overrides):
<!-- mdv: section required compliance/noWeasel warn=compliance/weasel-words -->
## Summary
<!-- mdv: section required -->
## Attestation
<!-- mdv: compliance/signoff required -->
Signed-off-by: Priya Raman, release manager
<!-- mdv: end compliance/signoff -->Manifest rules
| Rule | Detail |
|---|---|
| Prefix | every rule name, diagnostic code and repairActions key starts compliance/; override: true may replace a core rule |
apiVersion | must equal the loader's PLUGIN_API_VERSION (plugin-api-mismatch; 1 and 2 are retired) |
| Load errors | any breach throws PluginError at load; check with mdv debug plugin <module-or-path> |
| Other keys | documentRules, repairActions, unsupportedAutofix, pluginLeaves |
| Leaf | exactly one of family ("list", "table", "code": positional) or locator (by content); never owns a heading |
Diagnostics: gate
gate is "shape", "state" or "contract", never "lint": the lint gate is markdownlint's rules and mediva's fence check. If the honest fix changes a claim (a tick, a status, evidence, a value picked from a list) or needs work or facts from the world, the code is state; if it only changes form, shape. A shape code also needs a repairActions entry (or an unsupportedAutofix listing).
A rule's codes must all be one gate. A rule that checks form and then a claim puts the claim in a state half, state: { diagnostics, run }, which runs only once the document passed shape.
A rule reports only the codes it declares, in its diagnostics or its state half's. Any other code becomes the contract error undeclared-code, and the check could not run.
diagnostics: {
"compliance/too-many-acronyms": {
severity: "error",
title: "Too many acronyms",
suggestion: "Spell out the least-common acronyms on first use until the section stays under the limit.",
gate: "shape",
},
},
repairActions: {
"compliance/too-many-acronyms":
"Spell out the least-common acronyms in place until at most the allowed number remain.",
},Testing with mediva/testing
expectDiagnostics(template, document, codes, { plugins, context }) asserts that each code fires. runCorpus walks a directory; a first-line <!-- expect: code-a, code-b --> on a fail sample names its codes.
<!-- mdv: section required compliance/noWeasel -->
## Summary## Summary
Cut the uplink retry delay from 9s to 2s.<!-- expect: compliance/weasel-words -->
## Summary
We leverage a world-class retry.import { test } from "bun:test"; // or vitest, node:test
import { runCorpus } from "mediva/testing";
import plugin from "../src/index.mjs";
runCorpus(new URL("../corpus", import.meta.url).pathname, { plugins: [plugin], test });Wiring into a project
// mediva.config.mjs
import { defineConfig } from "mediva";
export default defineConfig({
plugins: ["mediva-plugin-compliance"], // package name, relative path, or an inline manifest
});The nearest config up to the git root loads automatically. --config FILE picks one; --no-config skips it (a config is executable code: use it on checkouts you don't trust).
Next: compile's plugins option, Tags & rules.