Skip to content
kerfjsGitHub

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:

tsx
function Price(props: { value: number }) {  const draft = state(props.value)  return <b>{draft.value}</b>}

kerf check says (wrapped here to fit):

text
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:

PartHereWhat it tells you
Placesrc/components/Price.tsx:2:23The file, line and column. Editors and terminals make it a link
CodeW005Which mistake. Look it up any time with kerf docs W005
Severityerrorerror, warning or info. Only errors fail the command
Why`state(props.value) copies…``What goes wrong, in terms of this code
FixUse 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:

tsx
// 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.

FamilyAboutFound by
WComponents and reactivity: reads that won't update, lists, bind:, React habits, prop typesThe compiler, as you save and in kerf check
DDesign and accessibility: raw colours and spacing, labels, alt text, focus, heading orderkerf check
TTests that don't test: no assertion, sleeps, snapshots, .only; and the test gate's T010, T011, T020kerf check on test files, and kerf test --gate
PThe project: kerf.config.ts, tasks, scripts, plugins and their permissionskerf check, and kerf dev and kerf build for P109 and P110
HPage metadata and share cards: titles, descriptions, imageskerf check and kerf routes --meta
QServer functions and the database@kerf-js/start as it serves, and kerf check
A, C, FAuth, the server contract, forms@kerf-js/start as it serves, and kerf check
RRealtime (R001–R024) and Rust server functions (R101–R109)kerf check, kerf doctor and the server
S, UState that crosses to the server; shared hover and focuskerf check, and the runtime in dev
X, I, MText, fonts and surfaces; icons; mediakerf check, the runtime in dev, and plugins
EDeploys: which hosts fit, the CDN in front, uploads (E001–E014)kerf deploy hosts, kerf deploy and kerf doctor
LBuilt pages: axe's WCAG rules, console errors, the head, sideways scroll, layout shiftkerf 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

bash
npx kerf check

kerf 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:

ExitMeans
0No errors. There may be warnings
1At least one error
2A 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:

bash
npx kerf check src/routes/index.tsx src/components/Price.tsx

The flags:

FlagWhat it does
--jsonThe whole result as JSON on stdout, for tools and agents. The text still goes to stderr
--verboseAdds 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:

json
{  "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

bash
npx kerf fix

kerf 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:

text
src/routes/index.tsx:8:12  W015src/components/Card.tsx:4:7  D037

Like 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):

bash
npx kerf docs W005
text
# 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 offWrite
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-codecheck: { 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 filescheck: { exclude: ['src/legacy/**'] }
Plugin permission errors P109 and P110trust: '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

tsx
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

tsx
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:

tsx
// 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

tsx
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

tsx
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

tsx
<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:

tsx
// 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

tsx
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 dev shows compiler diagnostics as you save. kerf check checks everything, and exits 1 on errors.
  • kerf fix applies 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-allow with a reason, and check in 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.

tsx
function Cart() {  const total = state(0)  return <p>Total: {total}</p>}
Show the solution
tsx
// 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.

tsx
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:

tsx
// 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:

tsx
// 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.