An LLM code grader
The repository's own checks plus an LLM for the judgement calls
The half the scripts cannot do
Many rules in AGENTS.md have a mechanical half and a judgement half.
A script can count the lines in a comment. It cannot tell whether the
comment says anything. A script can list every ?? in a file. It
cannot tell which ones paper over a missing value and which guard a
genuinely optional one.
deno task grade:code covers both halves for one file, a folder, or
every module under src/, and prints a scored report. It is advice,
not a gate.
Mechanical checks, reused
The mechanical half does not re-implement the rules. It imports the
repository's own checkers
(scripts/grade-code/checks.ts):
import { findIssues as findAliasExportIssues } from "#scripts/check-alias-exports/rules.ts";
import { findCommentIssues } from "#scripts/check-comments/rules.ts";
import { findIssues as findEmptyCatchIssues } from "#scripts/check-empty-catch/rules.ts";
import { LINE_LIMIT } from "#scripts/check-file-lengths/rules.ts";
import { findImportIssues } from "#scripts/check-imports/rules.ts";
So a mechanical grade always agrees with what deno task precommit
would say. That is only possible because every checker keeps its
rules in a pure rules.ts, separate from the IO that reads files.
Judgement questions, with evidence
The judgement half asks an LLM a fixed set of questions, one call per
file
(questions.ts).
Each question is a 0–3 score with four written criteria, worst to
best, and an instruction that names the AGENTS.md section it judges.
Some of the questions:
- Do comments earn their place, and describe current code?
- Is the copy plain language, and does naming match the site?
- Is the code functional and composed, or hand-rolled loops?
- Is it schema over organic structure, with shared dispatch?
- One path for one-or-many?
- Offensive, not defensive? Do lookups throw instead of returning stand-ins? Are there guards for impossible states?
- Does SQL select only needed columns, with the right transaction shape?
- Is pure logic split from IO?
The model is not asked to find things on its own. The mechanical pass extracts line-addressed evidence first: every fallback operator, every catch clause, every cast, every SQL string, every link. The question then asks the model to judge those lines. From the "offensive errors" question:
fallback_operatorslists every??,||, and?.with its line;catch_clauseslists catches. Score each down when it papers over a value the code treats as expected [...] and score??/||/?.on genuinely optional values as fine.
Grounding the model in extracted evidence keeps its answers about the file in front of it, rather than about code in general.
Uncertainty becomes a warning
Every answer comes back with a confidence. A pass or a fail that the
model itself is unsure about (confidence under 0.3) becomes a WARN
for a human to read, not a verdict. A grader that is confidently wrong
is worse than no grader.
How it gets used
- On a file before a refactor, to see which rules it breaks.
- Across
src/with--csv, to rank files worst-first and pick the next clean-up target. - With
--no-jevfor a fast, free, mechanical-only pass. - By an agent, on its own change, before asking for review.