| name | harn-rules |
| short | Structural search, lint rules, and codemods with the Harn rule engine. |
| description | Use for authoring and running declarative structural rules — patterns, lint diagnostics, and codemods. |
| when_to_use | Use when writing or running a structural rule, a `harn scan` pattern, or a `harn codemod` fix. |
Harn rule engine
Use this skill when matching or rewriting code structurally — a search pattern,
a lint rule, or a codemod — instead of with regex/glob. The engine is the
harn-rules crate; it is exposed to Harn as std/rules and to the CLI as
harn scan (read-only) and harn codemod (apply).
Pair it with [[harn-language]] for .harn syntax and [[harn-testing]] for
conformance fixtures.
Start here
crates/harn-rules/README.md is the rule-model reference (matcher forms,
relational/composite algebra, where/transform/fix, safety tiers).
docs/src/cookbooks/rules-engine.md is the worked-example cookbook
(harn scan / harn codemod, authoring a rule).
- A rule is TOML; an agent can author and run one entirely from
.harn
(via std/rules) without recompiling the binary.
A rule, end to end
- A rule has an identity (
id, language, severity, message), a [rule]
matcher block, and an optional fix.
- Scalar fields come before
[rule] — [rule] opens a TOML table, so any
later top-level scalar would bind to it.
- The rule's kind is derived: a
fix makes it a codemod; a message with
no fix is a lint; a bare matcher is a search.
Matcher forms (the [rule] block)
pattern — a code snippet with $VAR metavariable holes (ast-grep style),
compiled to a tree-sitter query. Operators/keywords match literally
(?? ≠ ||); a repeated $VAR unifies; a metavar-free pattern is a
literal pattern (foo() matches calls to foo, not any call).
$VAR:kind — a typed placeholder: bind only nodes of a syntactic class.
expr/stmt/type/ident aliases resolve to the grammar's supertype
(TypeScript/JS/Python), else use an exact tree-sitter kind. Unknown kind =
compile error.
kind — a bare tree-sitter node kind. regex — a regex over node text.
- Relational (each a sub-rule, tuned by
stopBy/field): inside
(ancestor), has (descendant), follows/precedes (siblings).
- Composite:
all/any (lists), not (a sub-rule), matches (a
[utils.NAME] utility rule by id). Every key on a node is ANDed.
Predicates, rewrite, and safety
- A
where predicate narrows matches: regex, comparison = { op, value },
a recursive pattern, or Harn-only semantic filters
resolvesTo = { name, kind, line, column, id } and type = "int".
Harn match dictionaries keep text in captures and add
capture_metadata with resolved / type where available.
[transform.NAME] synthesizes a metavar before fixing (convert = "snake",
replace = { regex, by }, substring = { start, end }).
fix interpolates $VAR / ${VAR} ($$ → $) and splices
format-preserving edits.
safety (least→most dangerous): format-only → behavior-preserving →
scope-local (default) → surface-changing → capability-changing →
needs-human. The two safest are machine-applicable; the rest are
suggestions (opt-in). auto_apply refuses anything riskier;
apply_checked also fails a non-idempotent fix.
From the CLI
harn scan '<pattern>' <paths> --lang <lang> — structural search; also
--rule <file> / --rule-pack <pack>, --report-only (per-file counts),
--json.
harn codemod --rule <file> <paths> — dry-run by default (a unified diff
per file with safety + idempotency); --apply writes (capability-gated),
--allow-unsafe applies above machine-applicable safety, --json.
harn lint <paths> — runs project lint rules. Declarative language = "harn"
rules and imperative *.lint.harn modules in [rules] ruleDirs merge into
the normal output. A script rule exports lint(source) and returns a
{message, severity?, line?, column?} finding, a list of findings, or the
rules_diagnostics(...) result. .harn rules run read-only and fail safe.
Trusted native Rust rule libraries can also be loaded from
[rules] nativeRuleDirs; they export harn_native_lint_register_v1 and emit
the same diagnostic/fix shape as built-ins.
From .harn (std/rules)
rules_search({rule, source|paths, language}) → matches with capture
bindings. rules_report(...) → a report-only data table (counts + rows).
rules_diagnostics(...) → declarative diagnostics. rules_apply({..., dry_run, allow_unsafe}) → codemod (gated; dry_run by default).
rules_visit({rule, ..., on_match: fn(node, ctx) { ... }}) — the imperative
escape hatch: the visitor returns its report(s) (nil/false to skip,
true to flag, a {message, fix, safety, severity} dict, or a list).
Gotchas
- Harn source is supported: pass
--lang harn, set language = "harn", or let
.harn paths resolve from their extension.
rules_apply is a gated deterministic tool: call
hostlib_enable("tools:deterministic") before it (even for a dry run).
- Format every rule run / fixture with both
cargo fmt and harn fmt.