navree
Documentation

Conformity checks

navree check verifies what the compiler can't: every route documented, every entity classified, workspace data reached only through WorkspaceRepo, translations and migrations. navree gate runs it with fmt, clippy, the tests and the front end in one command.

On this page

The compiler checks types and the container checks the module graph, but some rules of a well-built app live between modules: every route documented in the API, every table either owned by a workspace or shared, no query on workspace data without the workspace filter. navree check verifies those rules in one command, and navree gate runs it together with formatting, lints, tests and the front end's checks before you push.

Running navree check

Run it in your app:

Shell

It does two things, in order:

  1. It validates the module graph with the binary's built-in check, which constructs nothing. A broken graph stops here.
  2. It runs conformity:check, which boots the app once without serving traffic and runs every conformity check the app's modules contribute. ApiModule and WorkspaceModule contribute theirs; the checks never run on the request path.
Text

Errors fail the command (exit status 1); warnings are reported only. Because conformity:check boots the app, it needs the same configuration and database as any other command (navree migrate first, so the tables exist).

FlagDoes
--only api.docs,workspaceRuns only the named checks. A name without a dot selects its whole section: workspace runs workspace.entities, workspace.repo and workspace.tables.
--jsonPrints the report as JSON, for CI annotations and agents.
--graphPrints the module tree (every module, provider, export and command) and stops.

The JSON report lists each check with its status (ok, warn, fail or skip), how many items it looked at, and its findings:

JSON

You can run the app command directly too, which is what a deployment pipeline does: cargo run -- conformity:check --root . --json. --root is where the source checks read your code; navree check passes the app's directory.

The checks

CheckContributed byVerifies
api.docsApiModuleEvery route has #[api] documentation
workspace.entitiesWorkspaceModuleEvery entity in your source is classified as a workspace or a global entity
workspace.repoWorkspaceModuleCode reaches workspace entities only through WorkspaceRepo
workspace.tablesWorkspaceModuleEach workspace table has its workspace column, an index that starts with it and, on PostgreSQL, row-level security
i18n.translationsBuilt inRuns the app's i18n:check command when a module registers it
db.migrationsBuilt inRuns the app's migrate:lint command when a module registers it

Routes documented

api.docs lists every route without #[api] metadata, except the API module's own openapi.json and docs. The framework's routes (auth, access, audit, workspaces, notifications and the rest) are documented, so the findings are yours. A route without docs appears in the OpenAPI document with no summary, no tags and no schemas, which is what typed clients and your colleagues read.

Rust

Two keys of the api section go with it:

config/app.toml
KeyDefaultDoes
api.require_docsfalseRefuses to boot while a route has no #[api], listing each one: api.require_docs: 2 routes have no #[api] documentation: GET /api/v1/deals/board (module DealsModule), .... Turn it on in development so a new route can't ship undocumented.
api.undocumented[]Routes allowed without docs, by mounted path: "METHOD /path" or "/path" for every method, and a trailing * matches the rest of the path. From the environment, comma-separated: APP_API__UNDOCUMENTED=GET /internal/*.

Apps made with navree new list the Lighthouse and Logbook dashboards there (undocumented = ["/_lighthouse*", "/_logbook*"]): they are pages, not API routes. Other modules serve pages or speak their own protocol too; list them when you add them to the app:

ModuleRoutesPattern
Helmthe admin panel, its assets and the JSON calls behind it"/admin*" (or your helm.path)
Fleetthe dashboard and the JSON calls behind it"/_fleet*" (or your fleet.path)
GraphQLthe endpoint, GraphiQL and the subscriptions socket"/graphql*" (or your graphql.path)
UIthe component assets and the gallery"/_navree/ui*" (or your ui.mount)

navree check names the module of every undocumented route, so a module missing from the list shows up the first time it runs.

Entities classified

Under row isolation, every table either belongs to one workspace or is shared by all of them, and the difference decides how code may read it. workspace.entities reads your source for SeaORM entities (#[sea_orm(table_name = "..")]) and requires each one to be classified in its module:

src/modules/deals/mod.rs
  • workspace_scoped::<E>() registers an access subject for the record checks and classifies it.
  • workspace_entity::<E>() classifies an entity that isn't an access subject. Its entity implements WorkspaceScoped.
  • global_entity::<E>() marks a table shared by every workspace: currencies, plans, the app's own settings.

Entities defined by navree crates (users, workspaces, jobs) aren't in your source and aren't checked. An entity classified both ways is an error too.

Workspace data through WorkspaceRepo

workspace.repo finds the places where your code reaches a workspace entity directly, where the workspace filter is easy to forget:

  • Entity::find, find_by_id, insert, insert_many, update, update_many, delete, delete_many and delete_by_id on a workspace entity;
  • Repo::<Entity>, navree-db's repository, which doesn't know about workspaces.

WorkspaceRepo::<Entity> and CurrentWorkspace::scope(..) are the scoped ways in. A deliberate cross-workspace access, such as an admin report or a maintenance command, takes a marker comment on its line or the line above:

Rust

The scan reads your code as text, not as a compiled program. It follows use declarations (trees, self, super, crate, as renames and globs) and type aliases, so use super::entity::{self, Entity as Deal} and then Deal::find() is found, while another module's entity::Entity is not confused with this one. It skips tests/, benches/, examples/ and #[cfg(test)] modules. It can't see through a generic function that takes the entity as a type parameter, or a macro that writes the query; the isolation tests catch what reaches the routes either way.

Workspace tables

workspace.tables reads the migrated database. For each workspace entity's table:

  • the workspace column (WorkspaceScoped::WORKSPACE_COLUMN, workspace_id by default) must exist, or it is an error;
  • an index should start with that column, since every workspace query filters by it (a warning);
  • on PostgreSQL, row-level security should be enabled and forced (a warning).

A table that doesn't exist yet is a warning: run your migrations first. The check reads SQLite and PostgreSQL.

Under schema and database isolation each workspace's tables are apart already, so the three workspace checks are skipped.

Translations and migrations

Two checks run commands other modules register, so navree check covers them without knowing their modules:

  • i18n.translations runs i18n:check, which the localization module registers to catch syntax errors and messages missing from a locale.
  • db.migrations runs migrate:lint, the migration lint, when a module registers it.

Each is skipped, with the reason, when the app has no such command.

Adding your own check

A module contributes a check with conformity_check. The closure gets the booted app and the source root, and returns what it looked at and what it found:

Rust
  • Name checks <section>.<what>, so --only billing selects them.
  • Finding::error fails the check, Finding::warning reports only; .hint(..) says how to fix it.
  • CheckOutcome::skipped(reason) reports a check that doesn't apply. An Err from the closure is reported as an error finding: "the check could not run: ...".
  • cx.root() is the app's source directory, for checks that read files.

conformity_check also imports ConformityModule, which registers the conformity:check command once, however many modules contribute checks. ConformityReport::run(ctx, &ConformityOptions::new(root)) runs the checks in a test:

Rust

The gate

navree gate runs every check a change must pass before you push, one step after another, and stops at the first failure:

StepRunsIn
fmtcargo fmt --all -- --checkthe app
clippycargo clippy --workspace --all-targets -- -D warningsthe app
testcargo test --workspacethe app
checknavree checkthe app
web:typecheckthe typecheck script, or tsc --noEmit when the front end has a tsconfig.json and no such scriptweb/
web:testthe test scriptweb/
web:buildthe build scriptweb/

--workspace is passed when your Cargo.toml has a [workspace] table (a navree new app does). The web steps run when web/package.json exists, with pnpm, yarn or npm chosen by the lockfile, and only for the scripts it has. Each step's output streams to your terminal, then the gate prints its times:

Text
FlagDoes
--only test,checkRuns only these steps. web selects every web: step.
--skip webLeaves steps out.
--keep-goingRuns every step even after one fails, to see all the failures at once.
--web frontendThe front end's directory, relative to the app (default web/).
--no-lockDoesn't wait for other gates.

One gate at a time

A gate compiles and tests the whole app, and two at once on one machine slow each other down and can exhaust its memory. So the gate takes a lock, navree-gate.lock in the system's temporary directory, shared by every app of the same user on the machine. A second gate waits and says whose gate it waits for:

Text

The lock is released when the gate ends, including when it fails or you stop it. In CI, where each job has its own machine, the lock costs nothing.

In CI

The gate is one command, so a pipeline step is one line:

.github/workflows/ci.yml

With Dock, the gate's cargo steps get the app's service settings, as navree dev does.