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
rules
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
idstring
Bare name; namespaced to <package>/<id>.
stringengine"phrase" | "leading" | "density" | "statistical" | "inferential"
How the rule is evaluated. Decides whether it is a hard or soft rule.
"phrase" | "leading" | "density" | "statistical" | "inferential"severity?"error" | "warning" | "suggestion"
Legacy "info" is accepted as an alias for "suggestion".
"error" | "warning" | "suggestion"warningintent?"detection" | "style"
Style rules lint and fix but never move the human-likeness score.
"detection" | "style""detection"scope?"prose" | "text" | "all"
Which part of the document the rule sees.
"prose" | "text" | "all""text"description?string
One line, shown in listings and the rule reference.
stringrationale?string
Why the pattern is worth flagging.
stringmessage?string
Default diagnostic message.
stringdocs?string
Documentation URL carried on every diagnostic.
stringhttps://lawlint.com/rules/<id>examples?{ bad, good } | { bad, good }[]
Before/after pairs shown with the rule.
{ bad, good } | { bad, good }[]Hard rules
patterns?string[] | object[]
phrase, leading, and density engines. A bare regex fragment, or an object with pattern, message, suggestion, and fix.
string[] | 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.
{ pattern, window }threshold?number
density: matches per 1,000 words. statistical: the flag boundary for `direction`.
numbermetric?string
statistical only. An unknown metric is a load error, not a skipped rule.
stringparams?Record<string, number>
statistical only, e.g. { max_words: 45 } or { run_length: 3 }.
Record<string, number>direction?"above" | "below"
statistical document-level metrics only. Which side of `threshold` flags.
"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
granularity?"sentence" | "paragraph" | "document"
inferential only. What the judge is shown at a time.
"sentence" | "paragraph" | "document""sentence"rubric?string
inferential only. The judging criteria — normally written as the Markdown body instead.
stringflag_examples?string[]
inferential only. At least three examples that should flag.
string[]pass_examples?string[]
inferential only. At least three examples that should not.
string[]---
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.