Authoring rules
Repo-local rules live at .sextant/rules/<name>.md. The file is
markdown with YAML frontmatter — same shape as the built-in rules
embedded in crates/sextant-rules/rules/builtin/.
The body of the file becomes user-facing documentation: it’s what
explain_rule and
sextant rules explain print, and it’s what
the LLM evaluator (if any) uses as its prompt.
Frontmatter schema
Section titled “Frontmatter schema”---id: project.no-todo # required, dotted, globally uniquename: "No TODO comments" # required, human-readabledescription: "One-liner." # required, shown in `rules list`severity: warn # info | warn | errorcategory: style # complexity | size | duplication | # tests | reliability | style | # security | docs | { custom: "<name>" }scope: file # diff | file | repolanguages: [rust, python] # omit = all languagesevaluator: # see below type: regex pattern: '\bTODO\b'enabled: true # default trueoverrides: [] # rule ids this rule disablestags: [style, todo]---| Field | Required | Notes |
|---|---|---|
id | yes | Dotted, globally unique. Convention: <source>.<category>.<short-name>. |
name | yes | Human-readable. |
description | yes | One-liner shown in rules list. |
severity | yes | info, warn, or error. |
category | yes | Built-in enum or { custom: "<name>" }. |
scope | yes | diff, file, or repo. |
languages | no | Whitelist. Empty = all languages. |
evaluator | yes | regex, ast, or llm for repo rules. See below. |
enabled | no | Default true. Set false for a stub overriding a built-in. |
overrides | no | Rule ids this rule replaces. |
tags | no | Free-form labels. |
Evaluator types
Section titled “Evaluator types”regex — line-by-line pattern match
Section titled “regex — line-by-line pattern match”evaluator: type: regex pattern: '\.unwrap\('| Field | Required | Notes |
|---|---|---|
pattern | yes | Standard Rust regex. Matched per line. |
replacement | no | Regex-crate replacement template. When set, each match emits a unified-diff patch rewriting the line. |
Cheapest authoring option. The regex runs against each line of each file in scope; every match is one finding pointing at that line.
ast — tree-sitter query
Section titled “ast — tree-sitter query”Runs a tree-sitter query against the file’s parse tree. Pick this when you need to distinguish a keyword in a type position from the same keyword in a comment or string, or when you need to scope a match by ancestor node kind.
evaluator: type: ast query: '((predefined_type) @t (#eq? @t "any"))' capture: t message: "no `any` allowed" not_under: [catch_clause]| Field | Required | Notes |
|---|---|---|
query | yes | Tree-sitter query S-expression, compiled per language listed in languages:. |
capture | no | Capture name to anchor the finding line. Defaults to the first capture. |
message | no | Override message. Defaults to <rule.name>: matched <snippet>. |
not_under | no | Drop a match if any ancestor’s node kind is in this list. |
ast rules require at least one language in languages:. The
same query string is compiled once per listed language, so you can
target both typescript and tsx (which share grammar) with one
file.
llm — LLM-as-judge
Section titled “llm — LLM-as-judge”evaluator: type: llm model: claude-sonnet-4-6 # optional; falls back to [judge].model max_tokens: 1024 # optional temperature: 0.0 # optionalThe rule body is the prompt. Placeholders {{path}}, {{code}}, and
{{rule.id}} get substituted at evaluation time. Output is constrained
via tool-use to well-typed Findings —
no JSON parsing failures.
Requires [judge] in .sextant/config.toml and the corresponding API
key in env. See Configuration → judge.
builtin — Rust evaluator
Section titled “builtin — Rust evaluator”Reserved for built-in rules. Don’t use this in repo-local rules.
After authoring
Section titled “After authoring”- Validate:
sextant rules check .sextant/rules/<name>.md— catches YAML errors and missing fields without fully loading the rule. - Confirm load:
sextant rules list | grep <id>— should show the rule withsource: repo. - Try it:
sextant gradeand look for findings. - Read it back:
sextant rules explain <id>— verify the body formats well.
- The body is shown verbatim by
explain_rule. Treat it as user- facing documentation: explain why the rule exists and how to fix a finding. - Calibrate severity.
errorshould block real bugs;warnis “fix when convenient”;infois informational. - For LLM rules, write the prompt to ask for concrete findings tied to specific lines. Vague prompts produce vague output.
Override a built-in
Section titled “Override a built-in”Repo-local rules with the same id as a built-in replace it. To turn
one off entirely, ship a stub:
---id: builtin.size.fn-lengthname: "(disabled)"description: "x"severity: infocategory: sizeenabled: falseevaluator: { type: regex, pattern: "(?!)" }---The (?!) regex never matches, the stub is enabled: false, and the
built-in is replaced by an inert rule.
Examples
Section titled “Examples””No unwrap()”
Section titled “”No unwrap()””---id: project.no-unwrapname: "No unwrap()"description: "Forbid .unwrap() in production code."severity: warncategory: reliabilityscope: filelanguages: [rust]evaluator: type: regex pattern: '\.unwrap\('tags: [rust, panics]---
# No unwrap()
`.unwrap()` panics on `None` / `Err`. In production code that'salmost always a bug — the program crashes instead of handling theerror path.
## Fixing a finding
- Use `?` to propagate errors up.- Use `.expect("reason")` if the panic is genuinely unreachable, with a comment explaining why.- Use pattern matching to handle both arms explicitly.LLM rule: “API surface comments”
Section titled “LLM rule: “API surface comments””---id: project.api-surface-commentsname: "Public API needs a comment"description: "Public functions / types should have a doc-comment explaining intent."severity: infocategory: docsscope: filelanguages: [rust]evaluator: type: llm model: claude-sonnet-4-6---
# Public API needs a comment
You are reviewing `{{path}}` for missing documentation on the publicAPI surface.
Look for `pub fn`, `pub struct`, `pub enum`, and `pub trait` itemsthat lack a `///` or `//!` doc-comment immediately above them.Internal items (`pub(crate)`, `pub(super)`) are out of scope.
For each violation, return a finding pointing at the line of thepublic item with severity `info` and a message naming the item andsuggesting a one-line description.
```code{{code}}Bundling rules into a pack
Section titled “Bundling rules into a pack”If you want to distribute a rule set — to other repos, other
teams, or the internet — bundle the markdown files into a rule
pack. A pack adds a pack.toml manifest, lives
under .sextant/rules/vendor/<name>/ once installed, and ships
hash-locked so consumers can’t silently disable individual rules.
See also
Section titled “See also”- Rule concept — the data model.
- Evaluator concept —
regex,ast, andllmin detail. - Rule packs — package and ship a rule set.
- Configuration → judge — LLM provider config.