CLI

Map scoring

RulesCI
How evlog map turns rule results into a score — per-entry weights, the weighted project average, grade thresholds, coverage classification, and how routes are flagged as money, auth, or PII.

The score exists to be compared against itself. It is a number you can watch move as you fix things, and gate a pull request on — not a benchmark against other projects.

One entry point

Every entry point starts at 100 and loses the weight of each requirement it fails, floored at 0.

RequirementWeight
wide-event40
audit25
structured-errors20
page-error-handling20
context15
error-handling15

A rule that passes costs nothing. A rule that reports n/a costs nothing either — it never applied, so it is not held against the entry point. Opportunities carry no weight at all and cannot appear here.

A check you disabled with a comment is also n/a, so it costs nothing — and the report counts how many, because a score that is partly the result of disabled checks has to say so.

Take the login handler from the playground:

evlog map server/api/auth/login.post.ts
CHECKS
✓ useLogger  wide event emitted per request
✓ log.set    context attached with log.set()
✗ log.audit  sensitive action with no audit trail

structured-errors and error-handling were n/a — the handler throws nothing and catches nothing. One requirement failed, so the score is 100 − 25 = 75.

Weights add up to more than 100 on purpose. A handler that fails everything lands at 0, and the ordering of what to fix first stays meaningful: wide-event at 40 outweighs any pair of the smaller rules.

Which makes the arithmetic legible in both directions. Here is the worst entry point in the playground going from 20 to 100, one line at a time, each fix worth exactly the weight of the rule it satisfies:

api/auth/[...all].ts·20/100
export default defineEventHandler(async (event) => {
const log = useLogger()
log.set({ provider, flow: 'oauth' })
const session = await auth.handler(event)
log.audit('auth.login', { userId: session.user.id })
return session
})
20/100at risk
log useLogger()−40
ctx log.set()−15
audit log.audit()−25
err createError()n/a
catch catch loggingn/a
fetch fetch handlingn/a
every rule costs exactly its weight ▲ 20 → 100 in three lines

The project score

The global score is the average of every entry point, weighted by how much a blind spot there would cost you:

Entry pointWeight
Flagged money or auth×2
Page×0.5
Everything else×1

Pages weigh less because a page that swallows a fetch error is a worse user experience than an observability hole, and they are usually the most numerous files in an app. Sensitive handlers weigh double because that is where you will need the event.

Three entry points — a sensitive handler at 75, a plain handler at 100, and a page at 50:

(75 × 2) + (100 × 1) + (50 × 0.5)   275
───────────────────────────────── = ───── = 79
        2 + 1 + 0.5                  3.5

A project with no entry points at all scores 100.

Grades

ScoreGrade
90–100excellent
70–89good
50–69needs work
0–49at risk

Coverage classification

Alongside the score, each entry point is classified — this is what the summary block in the JSON counts, and what the coverage rows in the report are built from.

ClassWhen
instrumentedA handler that passes both wide-event and context, or a page that handles its fetch errors
partialA handler that passes one of the two
darkA handler that passes neither, or a page that swallows fetch errors
exemptevlog infrastructure — every rule is n/a, so it sits at 100 and is counted apart from your own entry points

dark is the number to watch. A dark entry point produces nothing you can query: when it misbehaves, the only evidence you will have is the user's description of it.

Sensitivity

Sensitivity is what promotes an entry point to double weight and adds the audit requirement, so it is worth knowing exactly how it is decided.

Money — $

  • Imports stripe, @stripe/stripe-js, paddle-sdk, or @lemonsqueezy/lemonsqueezy.js
  • Path contains checkout, payment, billing, invoice, refund, subscription, charge, or payout

Auth — A

  • Imports better-auth, next-auth, lucia, @auth/core, or @auth/nextjs
  • Path contains auth, oauth, login, logout, signin, signup, register, password, token, session, mfa, or otp

PII — @

Fields matching email, phone, address, ssn, or iban and a write call (create, update, insert, upsert) in the same handler.

Money and auth are high sensitivity: double weight, and an audit trail is required. PII is medium: no extra requirement, but it is marked in the report.

How the matching works

Two decisions keep this from producing noise.

Imports are read from the AST. A package counts only when it is genuinely imported, so a // TODO: drop stripe comment does not make a route handle money.

Path terms match whole words, allowing a plural. /api/authors is not an auth route, and /api/refunds is a money route. This matters more than it looks: a wrongly flagged route is handed a 25-point requirement it has no reason to satisfy, and counts double in the average — the fastest way to make the whole number untrustworthy.

Every reason is shown in full when you inspect an entry point, so a wrong call is one line to spot:

evlog map server/api/auth/login.post.ts
FLAGGED SENSITIVE BECAUSE
▍ auth: path says "auth"

Reading the score honestly

  • A high score is not proof of good observability. It says the shape is there: an event per entry point, context attached, errors explainable, sensitive actions audited. Whether the context you attach is the context you will need is a judgement no static analyser can make. Best practices →
  • Compare runs, not projects. A 200-route app that scores 70 is in better shape than a 6-route app that scores 70.
  • Watch dark and Money & auth rather than the headline number. Those are the two places where the next incident is hiding.

Next

  • CI — turn the score into a pass/fail check
  • Rules — what each requirement actually looks for