09. Bailiff
An ESLint plugin plus a CLI wrapper. Violations appear as editor squiggles, not as a code review comment somebody forgets to leave.
This is the feature that separates Avelon from a boilerplate. A template is advice. A linter is a decision.
Rules
Every rule ships with a documented reason and an autofix or codemod where one is possible. A rule you cannot explain in one sentence does not belong in the set.
| Rule | Enforces | Level |
|---|---|---|
no-vendor-import | No vendor SDK imported outside a driver package or avelon.config.ts | error |
no-cross-layer | Views cannot import Models. Controllers cannot import Controllers. Models cannot import anything under Http/. | error |
no-orphan-query | Database access only in Models, Actions, Errands, and seeds. Never in a component or a page. | error |
no-unwarded | Scrivener.unwarded() only in app/Errands/ and database/seeds/ | error |
require-ward | Every model has a ward | error |
no-raw-outside-drivers | driver.raw() only in app/Drivers/ or behind an explicit disable comment | error |
no-hand-edited-generated | Any diff to generated paths that did not come from a generator | error |
no-bun-in-core | @avelon/core and @avelon/orm may not import bun:* | error |
no-model-across-boundary | A model instance passed to view() instead of a serializer | error |
require-request | A write action validates through a Request | error |
no-fake-transaction | Sequential writes wrapped in try/catch and named like a transaction | error |
relation-depth | .with() chains exceeding the configured driver's maxRelationDepth | error |
no-lib-dumping | There is no lib/. A file has a home or it does not exist. | error |
no-not-supported-error | Throwing on an unsupported capability instead of typing it away | error |
attribute-schema-match | Model attribute declarations match database/types.ts | error |
controller-thinness | A controller action past N statements suggests an Action | warn |
no-any-public | any in an exported signature | error |
Why each error rule exists
no-vendor-import is the whole driver system. One import in a controller and the abstraction is decorative.
no-cross-layer is what keeps the structure legible after the person who set it up leaves. A view importing a model is how "just this once" becomes an architecture.
no-orphan-query stops the pattern where a component fetches its own data, which is how you get forty queries on a page and no way to see them.
no-unwarded and require-ward are data leak prevention, not style. A model without a ward is a table anybody can read.
no-hand-edited-generated protects the upgrade path. A hand-edit to a generated router silently disappears at the next sync, and the person who made it will not connect the two events.
no-model-across-boundary turns a confusing serialization error at runtime into a clear one at authoring time.
no-fake-transaction is subtle and worth the rule. Code that reads as atomic and is not produces partial writes under failure, and reviewers do not catch it because it looks correct.
attribute-schema-match makes a column rename a type error rather than an incident.
Escape hatches
Every rule can be disabled inline, and every disable requires a reason:
// bailiff-disable-next-line no-raw-outside-drivers -- pgvector similarity, no IR support yet
const rows = await DB.raw().rpc('match_documents', { embedding })
A bare // bailiff-disable-next-line with no reason is itself an error.
This matters more than the rules do. A linter with no escape hatch gets disabled wholesale, and then you lose the entire thesis. A disable with a reason is a searchable list of everywhere the framework was not good enough, which is a roadmap.
Running
reeve bailiff # all rules
reeve bailiff --fix # autofixable only
reeve bailiff --explain no-orphan-query
A default scan skips node_modules, .next, dist, archive/, and spikes/. Historical archive trees and throwaway spikes are not the product corpus; application and library code still has to obey the rules. Tests under @avelon/core and @avelon/orm may import bun:test because they are not the runtime graph. bun:* in those packages' src/ remains an error.
Ships in reeve doctor and in the default CI workflow. Editor integration is the ESLint plugin, so no separate extension is required.
Configuration
Rules are on by default. A project may downgrade a rule to warn or disable it in avelon.config.ts, and reeve doctor prints which rules are not at their default so the drift is visible rather than forgotten.
Adding a project-specific rule uses the same plugin API the built-in rules use.