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/e2e/plugin-compliance.

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

RuleDetail
Prefixevery rule name, diagnostic code and repairActions key starts compliance/; override: true may replace a core rule
apiVersionmust equal the loader's PLUGIN_API_VERSION (plugin-api-mismatch; 1 and 2 are retired)
Load errorsany breach throws PluginError at load; check with mdv debug plugin <module-or-path>
Other keysdocumentRules, repairActions, unsupportedAutofix, pluginLeaves
Leafexactly 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.

corpus/style-guide/report/templates/report.mdv.md
<!-- mdv: section required compliance/noWeasel -->
## Summary
corpus/style-guide/report/samples/pass/good.md
## Summary
Cut the uplink retry delay from 9s to 2s.
corpus/style-guide/report/samples/fail/weasel.md
<!-- 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.

On this page