LearnGet started
Diagnostics and kerf check
Kerf's compiler reads every component before it runs, and it knows the mistakes that are easy to make in a framework where components run once. When it sees one, it doesn't only fail: it tells you which mistake, where, why it's wrong, and what to write instead. That message is a diagnostic. This page teaches you to read one, run the commands that find them, and fix the ones you'll meet in your first week.
Anatomy of a diagnostic
Here's a component with a habit from React:
function Price(props: { value: number }) { const draft = state(props.value) return <b>{draft.value}</b>}kerf check says (wrapped here to fit):
src/components/Price.tsx:2:23 W005 error `state(props.value)` copies `props.value` once; later changes are ignored. fix: Use `state.linked(() => …)` to reset when it changes, or `untrack()` if a snapshot is intended. (autofix)kerf check: 14 files, 1 errors, 0 warnings (812 ms)Every diagnostic, from every part of Kerf, has the same parts:
| Part | Here | What it tells you |
|---|---|---|
| Place | src/components/Price.tsx:2:23 | The file, line and column. Editors and terminals make it a link |
| Code | W005 | Which mistake. Look it up any time with kerf docs W005 |
| Severity | error | error, warning or info. Only errors fail the command |
| Why | `state(props.value) copies…`` | What goes wrong, in terms of this code |
| Fix | Use state.linked(…)… | What to write instead |
| Edit | (autofix) | The diagnostic carries the exact edit, so a tool can apply it |
The code is the stable part. The words can improve between versions; W005 will always mean "state of a prop". That makes a code something you can search for, put in a commit message, or tell an agent.
Here's the fix it asks for:
// src/components/Price.tsximport { state } from '@kerf-js/core'import { Button } from '@kerf-js/veneer'export function Price(props: { value: number }) { const draft = state.linked(() => props.value) return <Button onClick={() => draft.value++}>{draft.value}</Button>}draft starts at the prop's value, can be changed locally, and resets when the parent passes a new value. The original was a box that never heard about the prop again.
Families of codes
The letter says what part of Kerf found the problem, and so roughly what it's about. You don't need to learn these: the code and its message are enough to act on. This is a map for when you wonder where one came from.
| Family | About | Found by |
|---|---|---|
| W | Components and reactivity: reads that won't update, lists, bind:, React habits, prop types | The compiler, as you save and in kerf check |
| D | Design and accessibility: raw colours and spacing, labels, alt text, focus, heading order | kerf check |
| T | Tests that don't test: no assertion, sleeps, snapshots, .only; and the test gate's T010, T011, T020 | kerf check on test files, and kerf test --gate |
| P | The project: kerf.config.ts, tasks, scripts, plugins and their permissions | kerf check, and kerf dev and kerf build for P109 and P110 |
| H | Page metadata and share cards: titles, descriptions, images | kerf check and kerf routes --meta |
| Q | Server functions and the database | @kerf-js/start as it serves, and kerf check |
| A, C, F | Auth, the server contract, forms | @kerf-js/start as it serves, and kerf check |
| R | Realtime (R001–R024) and Rust server functions (R101–R109) | kerf check, kerf doctor and the server |
| S, U | State that crosses to the server; shared hover and focus | kerf check, and the runtime in dev |
| X, I, M | Text, fonts and surfaces; icons; media | kerf check, the runtime in dev, and plugins |
| E | Deploys: which hosts fit, the CDN in front, uploads (E001–E014) | kerf deploy hosts, kerf deploy and kerf doctor |
| L | Built pages: axe's WCAG rules, console errors, the head, sideways scroll, layout shift | kerf build and kerf audit |
A plugin can add codes of its own, under its own name, such as <short-name>/<CODE>. They read the same way.
Most of what you'll see as a beginner is W, with some D if your app uses Veneer. The diagnostics reference lists every code, and each one has its own page at /reference/codes/<CODE>, such as W005.
Where you see them
While you work: kerf dev
kerf dev compiles each file when you save it. A compiler error stops that file: the page shows the error over the app, and the terminal prints it. A warning prints in the terminal and the page carries on. Fix the error and save, and the page comes back.
The editor shows TypeScript's errors and the prop-inference codes (W080–W085), but not the compiler's others. Editor and TypeScript setup explains why, and how to set the editor up.
Before you commit: kerf check
npx kerf checkkerf check compiles every .tsx and .jsx file in check mode, then adds what needs the whole project: the D-codes, your tests, the config, your routes' metadata, and anything that spans files. A W014 that starts in one file and lands in another's list, for example, only shows here.
It ends with a summary line, and its exit code says how it went:
| Exit | Means |
|---|---|
0 | No errors. There may be warnings |
1 | At least one error |
2 | A usage error, such as an unknown flag, or Kerf itself failed |
So kerf check in CI fails the build on errors and lets warnings through.
Give it files to check only those:
npx kerf check src/routes/index.tsx src/components/Price.tsxThe flags:
| Flag | What it does |
|---|---|
--json | The whole result as JSON on stdout, for tools and agents. The text still goes to stderr |
--verbose | Adds which compiler build ran (kerf doctor explains the two) |
--root <dir> | Checks the app in <dir> instead of the current folder |
--preset <p> | Checks against a deploy preset, for codes that depend on the host |
In JSON
With --json, each diagnostic is an object, and the same shape comes from every command and the MCP server. The text values below are shortened:
{ "code": "W005", "severity": "error", "owner": "core", "file": "src/components/Price.tsx", "span": [2, 23, 2, 34], "offset": [65, 76], "why": "`state(props.value)` copies `props.value` once; …", "fix": { "text": "Use `state.linked(() => …)` to reset …", "description": "Make it a linked state …", "safe": false, "edits": [ { "file": "src/components/Price.tsx", "span": [2, 17, 2, 35], "offset": [59, 77], "text": "state.linked(() => props.value)" } ] }, "docs": "kerf docs W005"}span is start line, start column, end line, end column, counting from 1. offset is the same range as character positions in the file. You'll rarely read this yourself; it's what lets an agent fix the code without guessing.
Fixing: kerf fix
npx kerf fixkerf fix applies every edit marked safe: an edit that can't change what the app does, only make it correct. It runs kerf check, applies the safe edits, and checks again, up to five passes, until nothing safe is left. Then it lists what it changed:
src/routes/index.tsx:8:12 W015src/components/Card.tsx:4:7 D037Like kerf check, it takes files to limit it to those, and --json.
Not every edit is safe. W005's edit turns state(x) into state.linked(() => x), which changes behaviour: maybe you wanted a snapshot, and untrack was the fix. So kerf fix leaves it, and in --json it's "safe": false. The text output marks both kinds (autofix), because both carry an edit; the difference is who applies it. A safe one, kerf fix does. Any other, you or your agent decide.
Some codes with safe fixes: W015 (add .value), W006 (read a nested prop at the use site), W043 (data-x={true}), W081 and W082 (write the inferred prop type), D037 (a positive tabindex), and D002 when a spacing value is exactly a step of the scale.
Looking up a code: kerf docs
Every code has a page, in the terminal (wrapped below to fit):
npx kerf docs W005# W005 (error)`state(prop)`: React's `useState(prop)` bug,the box never follows the prop.**Fix:** `state.linked(() => prop)`, or `untrack` for a one-time copy.**Status in kerf@0.1.4:** reported, suggested edit.Triggers it:…More: `kerf docs props`.It says what the code catches, the fix, whether the Kerf you have installed reports it yet, a short example that triggers it, and which topic to read next. The same text is on the site at /reference/codes/W005.
kerf docs with no topic lists every topic, and kerf docs diagnostics prints the table of every code with its status in your install.
Severities, and turning things off
Errors are code that doesn't do what it says: a value that never updates, a list that rebuilds every row, a button nobody can reach with a keyboard. They fail kerf check, and a compiler error stops that file in kerf dev.
Warnings are likely mistakes that can be right: a watch that could be a derived (W004), two solid buttons side by side (D038). They're reported, and nothing fails.
Info is something you might want to know, such as an icon name only known when the app runs (I002).
Kerf has no comment that turns off a W-code. Each one marks code that behaves differently from how it reads, and there is always a way to write what you meant, often the one in the fix (untrack when you really want a snapshot). For the other families, there are switches where a real exception exists:
| To turn off | Write |
|---|---|
| One D-code at one place | // kerf-allow D001: <reason> on the line above, or at the end of the line |
| One T-code in a test | // kerf-allow T004: <reason> on the line above, or in the file's header for the whole file |
| Every D-code | check: { design: false } in kerf.config.ts |
| The route metadata pass (H-codes) | check: { routes: false } |
| Prop inference (W080–W085) | check: { props: false }, or 'manual' to leave writing types to kerf fix |
| Some files | check: { exclude: ['src/legacy/**'] } |
| Plugin permission errors P109 and P110 | trust: 'warn' makes them warnings |
Write a reason after the colon. It's for the next person, and for the agent that would otherwise "fix" it.
The ones you'll meet first
These are the codes beginners hit most, roughly in the order you'll hit them. Each is an error.
W015: a box rendered without .value
const count = state(0)return <p>{count}</p>count is the box; JSX needs what's in it: {count.value}. It's a safe fix, so kerf fix adds .value for you.
W001: computing in the body
function Total(props: { count: number }) { const total = props.count * 5 return <b>{total}</b>}The body runs once, so total is worked out once and never again. Wrap it in derived and read .value, or do the sum in the JSX:
// src/components/Total.tsximport { derived } from '@kerf-js/core'export function Total(props: { count: number }) { const total = derived(() => props.count * 5) return <b>{total.value}</b>}const n = props.count on its own is fine: it only names the prop, and stays live. It's doing more with it, such as arithmetic, a call or ??, that needs derived. Derived values goes deeper.
W005: a box made from a prop
The one at the top of this page. state(props.value) never follows the prop. Use state.linked(() => props.value) for an editable copy that resets, or untrack(() => props.value) for a value that should stay as it was.
W010: a list without keys
function TodoList(props: { todos: { id: string; title: string }[]}) { const { todos } = props return ( <ul> {todos.map((t) => ( <li>{t.title}</li> ))} </ul> )}A row needs key, from a stable id in the data: <li key={t.id}>. With it, Kerf moves and reuses rows instead of rebuilding them. Rendering lists has more.
W020–W029: React hooks
const [open, setOpen] = useState(false)There are no hooks. useState is state, useMemo is derived, useEffect is watch or onMount, useRef is ref={el}, and useCallback is a plain function. kerf check reports a hook, a hook import or a dependency array as W020, and an empty dependency array on derived has a safe autofix.
W061: bind: in the wrong place
<input bind:value={name} />bind: gives a component's prop write access to a box in its parent, so it only goes on a component, and only takes a box. A form element takes value and onChange:
// src/components/NameField.tsximport { state } from '@kerf-js/core'export function NameField() { const name = state('') return ( <label> Name{' '} <input value={name.value} onChange={(e) => (name.value = e.currentTarget.value)} /> <span>Hello, {name.value}</span> </label> )}A component of your own can take bind:value, because there value is a prop. Writing to a parent with bind: covers when to use it.
W041: a let that should be state
let clicks = 0return <button onClick={() => clicks++}>{clicks}</button>A plain variable changes, but nothing re-reads it, so the button shows 0 forever. Make it state(0) and read clicks.value.
Deep diveDiagnostics written for agents
Kerf was built with AI agents writing much of its code, and the diagnostics are designed for them as much as for you. Every one has a stable code an agent can look up with kerf docs, a reason written about the code in front of it, and a fix it can apply: the exact edit, marked safe or not. kerf check --json gives the whole list in one shape, so an agent can run it after every change, read what's wrong, fix it and run it again, without a person translating a stack trace. The same messages are what you read, which is why they're plain and specific. Working with AI agents shows that loop.
Recap
- A diagnostic has a place, a code, a severity, a reason and a fix, often with the exact edit.
- The letter is the family: W for components and reactivity, D for design and accessibility, T for tests, P for the project, and so on.
kerf devshows compiler diagnostics as you save.kerf checkchecks everything, and exits 1 on errors.kerf fixapplies the safe edits; the rest need you to decide.kerf docs <CODE>explains any code, as does/reference/codes/<CODE>.- W-codes have no off switch, because each has a correct way to write it. D- and T-codes take
// kerf-allowwith a reason, andcheckin the config turns whole passes off.
Next, Working with AI agents: how an agent uses all of this.
Try it
Challenge 1
Fix a W015
kerf check reports W015 error \total\ is a box; JSX needs its value. on this component. Fix it.
function Cart() { const total = state(0) return <p>Total: {total}</p>}Show the solution
// src/components/Cart.tsximport { state } from '@kerf-js/core'export function Cart() { const total = state(0) return <p>Total: {total.value}</p>}{total} would render the box itself. {total.value} reads what's in it, and the compiler wires that read to the text, so it updates when total does. kerf fix makes this exact edit.
Challenge 2
A code you haven't met
This component shows a 0 on the page when there are no messages. Run kerf docs W011 (or open W011), then fix it.
function Inbox(props) { const { messages } = props return ( <section> {messages.length && ( <ul> {messages.map((m) => ( <li key={m.id}>{m.subject}</li> ))} </ul> )} </section> )}Show the solution
W011 is a number on the left of &&. When the length is 0, 0 && … is 0, and 0 renders as text. Compare it so the left side is a boolean:
// src/components/Inbox.tsxexport function Inbox( props: { messages: { id: string; subject: string }[] },) { const { messages } = props return ( <section> {messages.length > 0 && ( <ul> {messages.map((m) => ( <li key={m.id}>{m.subject}</li> ))} </ul> )} </section> )}Challenge 3
Decide on a suggested edit
kerf check reports W001 on const label = props.name.toUpperCase(), and its edit would make it derived(() => props.name.toUpperCase()). kerf fix won't apply it. Why not, and when would untrack be the right fix instead?
Show the solution
The edit isn't safe because it changes what the code does: today label is computed once, and after the edit it follows name. That's almost always what you meant, so apply it and read label.value in the JSX:
// src/components/Shout.tsximport { derived } from '@kerf-js/core'export function Shout(props: { name: string }) { const label = derived(() => props.name.toUpperCase()) return <strong>{label.value}</strong>}untrack(() => props.name.toUpperCase()) is right only when you want the first name and never a later one, such as the name a form started with. Then the code says so on purpose, and a reader knows it isn't a mistake.