# harn-rules-hostlib
The host capability that exposes the [`harn-rules`](../harn-rules) declarative
rule engine to the Harn language. Part of the
[Rule Engine program](https://github.com/burin-labs/harn/issues/2826)
(Epic A, [harn#2838](https://github.com/burin-labs/harn/issues/2838)).
## Why a separate crate?
`harn-rules` depends on `harn-hostlib` (for the tree-sitter grammars), so the
`rules` builtins **cannot** live inside `harn-hostlib` — that would be a
dependency cycle. This crate sits one level up
(`harn-cli` → `harn-rules-hostlib` → `harn-rules` → `harn-hostlib`) and an
embedder calls [`install`] next to `harn_hostlib::install_default`.
## Builtins (`std/rules` wraps these)
| `rules.search` | read-only | run a rule, return matches with capture bindings |
| `rules.report` | read-only | run report-only, return a `DataTable` (counts + rows) |
| `rules.diagnostics` | read-only | run a declarative rule, return its diagnostics |
| `rules.visit` | read-only (async) | imperative `on_match($node, ctx)` visitor |
| `rules.apply` | write (deterministic-tools) | apply a codemod `fix`; dry-run by default, safety-gated |
A rule is passed as its **TOML source** (`rule`), with either inline `source`
(+ `language`) or a list of `paths`. So an agent can author and run a rule
entirely from `.harn` without recompiling the binary.
## The imperative visitor (`rules.visit`)
[harn#2878](https://github.com/burin-labs/harn/issues/2878) — the native
escape hatch. `rules.visit` runs a rule's matcher and calls a `.harn` visitor
`on_match($node, $ctx)` once per match. The visitor **returns** its report(s)
— `nil`/`false` to skip, `true` to flag with rule defaults, a
`{message, fix, safety, severity}` dict, or a list of them — which become
diagnostics in the same shape `rules.diagnostics` emits. It can compute a
message or fix from the captured metavars, which the declarative form cannot.
Two design points worth knowing:
- **Async, registered directly on the VM.** A *synchronous* hostlib builtin
cannot call a `.harn` closure (`Vm::call_closure_pub` is async-only), so
`rules.visit` is an **async** builtin installed via
`Vm::register_async_builtin` in [`install`] rather than through the sync
`HostlibRegistry`. It obtains a child VM from its `AsyncBuiltinCtx` and calls
back per match.
- **Returns rather than mutates.** Harn closures capture by value and
`VmValue` has no callable variant carrying captured Rust state, so a
mutating `ctx.report(...)` could not accumulate soundly. Returning the
report is both the correct option and the simpler one.