Reading what the doctor says
When to use
aru doctor reported something, a build passes and something still feels
unenforced, or a change is about to be called finished -- the doctor is the
last gate. Fixing what a finding points at is the family skill of that code;
this skill says what the finding means.
Before you start
aru doctor is this framework's architecture rules run as static analysis over
the parsed tree. Without it, mandatory architecture is documentation nobody
reads. Run the aru the Dockerfile pins -- an older one checks fewer rules, and
its clean report says less than it seems to.
Contracts and imports
Every finding carries a file, a line, the rule name, what is wrong, and a
Why that says what breaks. An error fails the run; a warning fails it only
under --strict. There is no ignore comment and no allow-list. The one marker
the doctor reads is //arandu:system-grant <reason>, on or above an
auth.SystemGrant call outside a seeder, a job and a command: it excuses
system-grant-outside-scope on that line and nothing else, and a marker with
no reason excuses nothing.
Every rule
The whole set the doctor checks, with the severity it reports, in the order
aru doctor --list prints them. An error fails the run; a warning fails it
only under --strict. tests/Unit/DoctorSkill_test.go fails when a row here
differs from the list it carries, and, when the aru on PATH is the release the
Dockerfile pins, when that list differs from what aru doctor --list prints.
Procedure
- Read the Why, not the rule name. The rule name says which check fired;
the Why says what a user of the application would experience. Fix the
second one.
- Fix the cause at the line it names, by moving the code to the row of
"Where each kind of code lives" in
AGENTS.md that names its kind -- not by
reshaping it until the rule stops matching.
- Never suppress. A finding you cannot fix is a design question, and the
answer goes in the report.
- Run it again until it is clean, then the other gates.
The findings you will meet most
grant-not-received — a repository method takes no auth.Grant. Every
caller gets the row, whoever asked. Add the Grant as the parameter before the id,
start the method with if err := g.Check(Action...); err != nil { return err }
— a Grant that is taken and never checked is grant-not-checked — and take it
from the Policy.
tenant-from-request — the tenant is read from a path segment, a form
field or a query; tenant-from-header is the same finding for a header. A
tenant that arrives with the request is a tenant the caller chose. Read it with
auth.Tenant(g).
repository-without-policy — a repository is reachable with no Policy
deciding. Write the Policy; the generator writes one that denies everything, and
you open it action by action.
resource-not-reauthorized — a method authorized the action and then read
one row without authorizing the row. The first call answers "may this caller
look at all"; the second answers "may this caller look at this". Skipping the
second means any user of the same tenant sees the row.
view-data-is-a-map — a view was handed a map. A typo in a key is then a
blank space on a page that answered 200. Declare a struct that embeds
view.Page.
raw-output-is-not-a-component — {!! x !!} was given a value rather than a
call. The raw form escapes nothing, so a value that ever comes from a person is
stored cross-site scripting. Write {{ x }}, which escapes, or return it from a
component function.
policy-never-opened — a policy denies every action. On a new module this is
correct and expected; it is a warning so that a fresh project is not red on day
zero.
retired-module — an import names a module that no longer exists. The line
says what replaced it.
import-not-canonical — a file names a symbol through a framework bridge
package rather than through the path the symbol lives at: security.Grant for
auth.Grant, data.DB for database.DB, fhttp.Context for
hhttp.Context. The bridges are old import paths whose symbols are aliases of
a component's, kept so code written against them goes on compiling; each is
removed in v1.0.0, and until then one type is spelled two ways, file by file.
It reads every Go file, tests included, and every .kyse.go source -- its
import lines and its local.Name uses, as text, so a sentence in the markup
that spells security.Grant counts. The Go aru view:build writes is
skipped: the finding goes to the import line of the source it was compiled
from. Which path is canonical is read, symbol by symbol, from the framework
source at the version go.mod requires; a type the framework declares, such as
Router or SessionStore, is canonical where it is. That catalog is read from
disk, so on a machine that has not downloaded the version the rule is silent.
Import the path the finding names, and keep the framework import only for what
the framework declares; aru imports:catalog --fix shows the rewrite for every
file and --apply writes it. It is a warning because nothing is wrong yet.
sensitive-field-not-redacted — a struct under app/ has a field whose
name carries a sensitive word (password, passwd, secret, token, apikey,
creditcard, document, cpf, cnpj; a whole word, singular or plural) and whose
type can hold one, and a value of it reaches a log or JSON sink in this
project's code -- the message names the sink. slog, fmt and
observability.Dump print every field, json.Marshal writes every exported
one, and only the type can promise it never leaks. A type nothing logs or
encodes is not reported, nor one with LogValue or MarshalJSON, nor one only
encoded to JSON whose sensitive fields are all json:"-"; a count such as
MaxTokens int is not a secret. It follows a value only inside one function:
typed by the signature, by a composite literal, or by a name that is the type's
in lower case -- not through another type's field, a call's result or a helper
that logs. Add LogValue() slog.Value and MarshalJSON to the type, or tag
the field json:"-" when JSON is the only sink.
generated-not-wired — an exported New... constructor under
app/Http/Controllers or app/Services is named by no file outside the tests:
code nothing constructs is code the router never reaches, its tests pass, and a
change to it changes nothing anybody sees. A call from bootstrap/app.go,
routes/web.go or another constructor wires it. A test double -- Fake, Stub,
Mock, Spy or Dummy as a word of the constructor, of the type it returns or of
its file -- that a test constructs is not reported; a service named like
production code whose only caller is its own test still is, which is the case
the rule exists for. It compares names across the project, not types, so a
function of the same name elsewhere hides a finding rather than inventing one.
Paste the wiring aru make:module printed into bootstrap/app.go and
routes/web.go, or delete what nothing reaches.
model-query-stale — a <Entity>Query.go, or a factory aru model:build
renders, is missing, behind its entity, or left over from one that is gone. A
build compiles what is on disk, so the application would run against the query
of an entity that is not the one in the source. Run aru model:build; in a
pipeline that calls go build directly, aru model:build --check asks the same
question.
model-core-outside-models — the model core is used outside a package that
declares entities, which here is app/Models: a model.NewTable, or a method
called on a *model.Table, a *model.Builder or what Base() returns. The
core hands back untyped rows, and a query written on it is a second way to reach
the table, one the generated query does not describe. Call the generated
constructor, models.Notes(db), or write the query as a method on *NoteQuery
in the custom block of the entity's file.
Three more checks run only under --profile=performance:
profile-not-declared, join-across-aggregates and
transaction-across-aggregates. What they report is correct code on the
conventional profile, and each says so in its own first lines. The ones above
run on every profile.