Skip to content
lawlint
Esc
navigateopen⌘Jpreview
On this page

Authoring rules

Rule packages, the Markdown rule format, every frontmatter field, and testing what you write.

Rules are Markdown files with YAML frontmatter — one file per rule, grouped into a package.

house-style
style.yaml
rules
no-per-our-discussion.md
empty-hedge.md

Only .md files under rules/ are discovered. Point lawlint at the package with --rule-dir ./house-style, or list it in ruleDirs in configuration. Package rules merge over the built-ins: same id, replaced rule.

The manifest

# style.yaml
name: house-style
version: 1.0.0
description: Our firm's writing conventions.

The package name namespaces every rule in it — a rule with id: no-doublets becomes house-style/no-doublets.

A rule file

---
id: no-per-our-discussion
engine: phrase
severity: warning
intent: style
description: Flags correspondence filler that adds no information.
patterns:
  - pattern: "per our discussion"
    suggestion: "Name the discussion, or drop the phrase."
    fix: "as we discussed"
---
"Per our discussion" tells the reader nothing they do not already know. Either
identify the conversation being referred to, or delete the clause.

Frontmatter carries the structured fields; the body is prose. What the body means depends on the kind of rule — explanatory text for a hard rule, the judge’s rubric for a soft one.

fix vs fixTemplate

fix is a literal replacement for the whole match. $ has no special meaning in it, so fix: "$500 fee" inserts exactly that.

fixTemplate is a replacement template that interpolates the pattern’s own capture groups — $1/${1} numbered (0 is the whole match), $name/${name} named, and $$ for a literal $. Use it when the repair is an insertion or a reorder rather than a swap, which a whole-match replacement cannot express:

patterns:
  - pattern: '(\w+, \w+) (and|or) (\w+)'
    fixTemplate: '${1}, ${2} ${3}'   # Alice, Bob and Carol -> Alice, Bob, and Carol

A pattern may set one or the other, never both. Referencing a group the pattern cannot define is a load error, not a silent empty expansion.

Two behavioural differences worth knowing. fix adapts its casing to the matched text; fixTemplate does not, because the captured groups already carry the source’s own casing and re-casing the assembled string would corrupt it. And the choice of field — never the content of the string — decides how a replacement is treated, so a literal $1 can never be silently reinterpreted as a capture reference.

Frontmatter fields

Common to every rule

PropType
idstring

Bare name; namespaced to <package>/<id>.

Typestring
engine"phrase" | "leading" | "density" | "statistical" | "inferential"

How the rule is evaluated. Decides whether it is a hard or soft rule.

Type"phrase" | "leading" | "density" | "statistical" | "inferential"
severity?"error" | "warning" | "suggestion"

Legacy "info" is accepted as an alias for "suggestion".

Type"error" | "warning" | "suggestion"
Defaultwarning
intent?"detection" | "style"

Style rules lint and fix but never move the human-likeness score.

Type"detection" | "style"
Default"detection"
scope?"prose" | "text" | "all"

Which part of the document the rule sees.

Type"prose" | "text" | "all"
Default"text"
description?string

One line, shown in listings and the rule reference.

Typestring
rationale?string

Why the pattern is worth flagging.

Typestring
message?string

Default diagnostic message.

Typestring
docs?string

Documentation URL carried on every diagnostic.

Typestring
Defaulthttps://lawlint.com/rules/<id>
examples?{ bad, good } | { bad, good }[]

Before/after pairs shown with the rule.

Type{ bad, good } | { bad, good }[]

Hard rules

PropType
patterns?string[] | object[]

phrase, leading, and density engines. A bare regex fragment, or an object with pattern, message, suggestion, and fix.

Typestring[] | object[]
allow_context?{ pattern, window }

phrase only. Expands the match by `window` bytes each side; if `pattern` matches the expanded slice, the match is suppressed.

Type{ pattern, window }
threshold?number

density: matches per 1,000 words. statistical: the flag boundary for `direction`.

Typenumber
metric?string

statistical only. An unknown metric is a load error, not a skipped rule.

Typestring
params?Record<string, number>

statistical only, e.g. { max_words: 45 } or { run_length: 3 }.

TypeRecord<string, number>
direction?"above" | "below"

statistical document-level metrics only. Which side of `threshold` flags.

Type"above" | "below"

A pattern’s fix string makes the fix machine-applicable — it will be applied by --fix. Omit it and the rule reports a suggestion the author has to act on. See Fixes.

patterns:
  - pattern: "\\bnull and void\\b"
    message: A doublet where one word does the work.
    suggestion: Use "void".
    fix: "void"

Soft rules

PropType
granularity?"sentence" | "paragraph" | "document"

inferential only. What the judge is shown at a time.

Type"sentence" | "paragraph" | "document"
Default"sentence"
rubric?string

inferential only. The judging criteria — normally written as the Markdown body instead.

Typestring
flag_examples?string[]

inferential only. At least three examples that should flag.

Typestring[]
pass_examples?string[]

inferential only. At least three examples that should not.

Typestring[]
---
id: empty-hedge
engine: inferential
severity: warning
description: Flags hedges that carry no information about actual uncertainty.
---
Flag a sentence when it hedges a claim without saying what is uncertain or why.
A hedge earns its place only when it tells the reader what the uncertainty is.

## Flag examples

- It could potentially be argued that the clause may be unenforceable.
- This arguably suggests that there might possibly be an issue.
- Some might say the provision is perhaps somewhat ambiguous.

## Pass examples

- The clause is likely unenforceable in California, though no court has ruled.
- We estimate a 60% chance of prevailing on the third claim.
- Whether the notice was timely depends on when it was received.

Either supply the examples as ## Flag examples and ## Pass examples sections, or as flag_examples / pass_examples frontmatter arrays. Each needs at least three. The body outside those two sections is the rubric.

Testing a package

lawlint rules test ./house-style
lawlint rules test ./house-style --offline           # skip soft rules entirely
lawlint rules test ./house-style --judge=anthropic:<model>

Runs each rule against its own examples and reports pass or fail per example — so a rule whose pattern is too broad fails on its own pass examples before it ever reaches a document.

Inferential rules need a model to evaluate their examples. Without --judge they are skipped; --offline skips them explicitly and never loads a model.

Listing what is loaded

lawlint rules --json
lawlint --rule-dir ./house-style rules --json

Prints metadata for every loaded rule, including each rule’s tier and intent. This is what generates the rule reference.

Last updated on July 24, 2026