A doctor is a single JS file. It exports a meta object and a
doctor(ctx) function. Companion fixtures describe the cases used to verify it.
Here’s what the metadata for a doctor looks like:
export const meta = {
id: "refresh-session",
description: "Direct discarded session refresh promises.",
severity: "warning",
blindSpots: [
"Only direct named imports from @acme/session are recognized.",
"Stored promises and wrapper calls are not followed.",
"Without call analysis this check does not run.",
],
checks: [{
id: "discarded-refresh",
description: "The session refresh call's result is discarded.",
severity: "warning",
revision: 1,
needs: ["calls"],
onUnknown: "skip",
reportingUnit: "occurrence",
claim: "A direct named refreshSession import from @acme/session is called as a discarded expression.",
lookalikes: ["Awaited, returned, passed, or stored results", "Shadowed local functions"],
impact: "Discarding this promise can leave refresh failures unhandled.",
why: "The identified package call is a standalone discarded expression.",
fix: "Await or return this promise where appropriate for the caller's contract.",
}],
};This metadata accompanies the complete implementation and fixtures in Writing your first doctor. That example assumes the named package export returns a promise; metadata alone does not establish it.
The confinement
The runtime gives the doctor a read-only context and nothing more.
Doctors run under Node’s permission model: file writes, subprocesses, and native addons are denied by Node itself, and an import guard refuses every module a doctor tries to reach. Network globals are deleted before doctor code runs, and a checker scans the doctor file for network calls. Nothing phones home.
Check fields
| Field | What it is |
|---|---|
id |
Stable check name; appears as doctor/check-id |
description |
The heading shown for each finding |
severity |
error, warning, or info; severity does not establish certainty |
revision |
Bump when the check’s meaning changes; cosmetic edits keep it |
claim |
The observable condition the check states. Required to certify |
lookalikes |
Shapes that look like the claim but are fine |
needs |
Required analysis capabilities, such as ["calls"]; declare these when using optional analysis |
onUnknown |
"narrow" or "skip" when required analysis is unavailable; implement and test this behavior |
reportingUnit |
occurrence, file, or project — what one finding covers |
impact / why / fix |
The explanation carried into every report and JSON scan |
The claim is the load-bearing field. A check without a claim cannot be
certified, because eloquent impact prose is not a testable statement.
A nonempty claim is not proof that the implementation establishes it. Verification
also requires lookalikes, and an onUnknown policy when needs is declared.
Declare a reporting unit on new checks and inspect skipped coverage. More on this
in honest checks.
Report an inspectable finding
evidence belongs to an emitted finding, not the check metadata:
ctx.report.finding({
rule: "discarded-refresh",
file,
line: call.line,
column: call.column,
evidence: { endLine: call.endLine, endColumn: call.endColumn },
});Lines are one-based and columns are zero-based. Supply the expression’s end position for multiline evidence. Distinct occurrences on one line need distinct columns; location and evidence help reviewers inspect the claimed operation.
Doctors receive shared structural and binding analysis through ctx. Use these
facts to establish identity and scope; keep framework semantics in the doctor.
Do not import a parser or create a private text-based substitute for semantic
facts. See testing and evaluating doctors for
location witnesses, unavailable-analysis fixtures, and independent evaluation.
Where doctors live
Three locations, in priority order:
./doctors/in the repo being scanned~/.any-doctor/doctors/(global,--globalwhen generating)- the bundled pack (lowest priority)
Fixture files (*.fixtures.mjs) beside a doctor are its test data, not
target source — scans skip them.