Skip to content
kerfjsGitHub

Tooling

AGENTS.md

<!-- Generated by kerf agents-md from kerf@0.1.0. Do not edit: change kerf.config.ts and regenerate. -->

This is a kerf app. It is not React: components run once, state is a box, and React hooks don't exist. Run kerf check --json after every change; each diagnostic gives code, location, why and fix.

Start with .kerf/map.md (kerf map): what this app has and what to call.

Rules

tsx
import { derived } from '@kerf-js/core'export function Cart({ items, taxRate }: {  items: { id: string; name: string; price: number }[]  taxRate: number}) {  const subtotal = derived(() =>    items.reduce((s, i) => s + i.price, 0),  )  const total = derived(() => subtotal.value * (1 + taxRate))  return (    <section>      <ul>        {items.map(item => <li key={item.id}>{item.name}</li>)}      </ul>      <p>{total.value.toFixed(2)}</p>    </section>  )}
  1. State is a box. Read and write .value, including in JSX (W015).
  2. Compute from other values with derived, not watch or the component body (W001).
  3. JSX is live. Each {expr} and attribute re-evaluates when its inputs change.
  4. Props are current. Destructure one level (W006). Read in derived, watch, JSX or handlers. const x = props.y is a live read-only alias; let x = props.y and props.y = write locally; only bind:y={box} writes out.
  5. The body runs once. No state-based early returns (W040) or let counters (W041). Branch in JSX.
  6. Pass a box only to give write access (value={coupon}, child prop typed State<string | null>), or so list rows can read one entry: cart.get(p.id) re-runs one row, cart[p.id] or cart.value[p.id] re-runs them all (W017), in the list and in the row component alike.
  7. Lists and conditionals: .map with key (W010), &&, ternaries. Keep items stable: replace only the changed item (p.id === id ? { ...p, price } : p), never ({ ...p }) every item (W014).
  8. Copy a prop into state with state.linked(() => prop), not state(prop) (W005). untrack(() => …) is a snapshot that never updates: never render it (W018).
  9. Side effects go in watch(() => source, value => cleanup?); DOM work in onMount. flush() applies pending DOM updates now.
  10. Async data is resource(signal => fetch(…)) (.value .pending .error .refresh()), never derived(async …) (W012). Loading and errors: <Boundary pending={…} error={(e, retry) => …}>.

Leave props untyped: kerf docs props.

API in kerf@0.1.0

  • @kerf-js/core: Boundary, batch, context, defineState, derived, flush, onCleanup, onMount, resource, state, untrack, watch, state.linked
  • @kerf-js/core/dom: render, hydrate.
  • @kerf-js/core/ssr: renderToString, renderToStringAsync. Never import the rest.
  • @kerf-js/core/browser: online, visible, lifecycle, media, on, interval. Create them in setup (W051); server-rendered media needs ssr (W054). More: kerf docs browser; hover and press as state: kerf docs ui-state.
  • @kerf-js/core/surface: canvas and WebGL, never raw <canvas>: kerf docs surface.
  • @kerf-js/core/test: assertNoLeaks, clock, flush, memoryStorage, net, page, render, scope, screen, tabs, touches, user, and expect(fn).toTouchAtMost(n, { runs }). Query by role/label/text; user actions and clock.advance end in flush(), so never sleep or waitFor.

Diagnostics

kerf docs <code> explains one. Also flagged: an early return in a .map callback (W013), raw addEventListener, setInterval or sockets with no cleanup (W050), and a watch that folds a stream (W052) or copies a live value into state (W056).

Test checks (kerf check on test files): T001–T009, T013. No assertion, tautologies, sleeps, container/selectors/test ids, mocking kerf, snapshots, conditional expect, .only/.skip. To allow one: // kerf-allow T00x: <reason>.

Before a change is done, see it work (kerf browser snapshot|act|assert, or __kerf__.agent in your own browser) and run kerf test --gate: new tests must fail on the old code and kill a mutant (T010, T011, T020). kerf build audits its pages at phone and desktop width (L-codes) and fails on an error. More: kerf docs verify.

Before publishing, set build.audit.indexing (public or private) and audit the deployed URL: kerf audit --url <url> --indexing public. Never drop a private noindex for a score.

Full stack

@kerf-js/start app: file routes in src/routes (_layout.tsx, [id].tsx, load, meta), server functions in src/server/** or *.server.ts. Import your own code as #/server/todos (src/server/todos); generated files are kerf:slot/<id> and kerf:tokens/<name>.

  • Reads: query({ tags: ['todos'], auth: 'user', run: ({ ctx }) => ctx.db.select().from(todos) }). In a component todos().value ?? []; in load and handlers await todos.fetch().
  • Writes: mutation({ input, auth: 'user', invalidates: ['todos'], optimistic, run }).
  • Content an owner edits (menu, settings, pages): read with cache: { life: 'hours', warm: true }; edits go to a draft (preview.start(id) shows the owner), publishing applies it, slow follow-ups go in after(). kerf docs start § Publishing.
  • Database: Drizzle; await every statement. Schema change: kerf db generate (Q015). Inputs: schemas.todos.insert.pick({ title: true }).
  • Auth: ctx.user is the user or null; auth: 'user' refuses anyone else (A002), { role: 'admin' } or { can: (ctx) => … } the wrong user (A006). With auth on, mutations need a signed-in user unless auth: 'public' (sign-in, sign-up, sign-out). Guard pages with redirect('/login') in a layout's load; run still checks who owns a row. Sessions live in the database; pages cache them 5 minutes in a signed cookie (session.cookieCache). JWTs are API-only (api: { functions }).
  • Never copy query results into state, import db from a component (Q012), fetch /_kerf/*, or poll (todos.live(); rooms: channel).
  • Navigation: chrome shared by sections (sidebar, tabs) goes in the one layout above them, following currentPath(): a per-section _layout.tsx is replaced on each move. Route changes show a skeleton from the page's markup; export const skeleton = Named (plain markup, class and data-* only) overrides it.
  • Every page sets meta as you build it, from data and params: kerf docs meta.

Styling

Run kerf style find <what it is> first, then reuse a group or add an axis value; never class lists. New: const card = group('card', { base: { pad: 'group' }, axes: { status: { late: { tone: 'critical' } } } }) from @kerf-js/core/style, <li {...card({ status })}>. Tokens only. D001–D007, D011, D012, D020–D025, D030–D041, W060: kerf docs style, kerf docs design; kerf fix applies the safe fixes.

Commands

CommandDoesSide effects
kerf check --jsonCompile every component in check mode and report W- and P-diagnostics with fixesnone
kerf docs <topic>Print a docs topic or a diagnostic code (kerf docs W014)none
kerf addInstall a plugin and wire it into the config (--dry-run shows the changes)fs:kerf.config.*, fs:package.json, network:install
kerf devDev servernetwork:listen
kerf buildProduction build and contractfs:.output/**
kerf dbgenerate: new migrationsfs:drizzle/, fs:migrations/, fs:prisma/**
kerf testThe test gate on the diff (--gate)fs:.kerf/gate
kerf browserDrive the running app in any open browser by role/label/text (kerf browser help)network:dev-server, fs:.kerf/browser
kerf runRun a project task by nametask
kerf run contentGenerate docs, search indexes, llms.txt and numbers from the repofs

kerf tasks --json lists the rest.

Plugins

Fonts (@kerf-js/text)

  • Declare fonts once in text({ fonts }) in kerf.config.ts: { body: { family, src, fallback } }. Never hand-write @font-face (X020).
  • The build subsets each face by unicode-range, adds a metric-matched "<Family> Fallback" face and preloads body and display. @kerf-js/core/vite loads them: no import. kerf docs fonts.
  • A build trims faces on its own: custom axes the source never varies are pinned, and uncommon latin characters load on demand. An axis set at runtime needs declaring in axes; trim: false turns it all off.
  • text.measure is lazy: it costs nothing until something measures, a canvas s.text(), measure(), lines() or fit() from @kerf-js/core/text, or a veneer list given text (measure: 'off' removes it).
  • Font stacks name the fallback second: font-family: "Inter", "Inter Fallback", sans-serif. font-display is swap or optional, never block (X021).

veneer (UI kit)

Import components from @kerf-js/veneer. Layout: Stack Row Grid Split Center Cover Frame Spacer Inset (gap: hair tight related group section page). No raw values (D001–D007); intent props only: tone (neutral accent positive caution critical info), emphasis (solid soft outline ghost frame link), size (sm md lg). The look comes from dials: data-expression|theme|density|motion|flair on any element (scopes nest). Fields take label (required), hint, error. App-wide: veneer({ seed: { accent }, dials, radius, icons, fonts, tokens }) (icons pins the icon tier); D010 fails a pair that can't pass. Theming: colors names colours (tone="brand"), system retunes scales, parts restyles an element per state (parts.button.hover.bg; kerf docs theme lists parts), themes are named looks switched by data-palette or setPalette. Own CSS: never a bare [data-part]; hang it from its [data-scope]. A tall sticky header sets --veneer-scroll-offset.

Icons

Import icons from @kerf-js/veneer-icons: CheckIcon is hollow, the default, CheckIconSolid the other form (<Icon name="check" solid />); <Icons.Check /> and veneer's <Icon name="check" /> build to the same bytes. Tiers are a prop, tier, or the page's data-expression. Decorative by default; label gives one a name. An unknown literal name is I001 at the build and in kerf check. A name only known at runtime is legal and loads the pack lazily (I002 reports it), or single files with icons({ source }) and kerf icons publish <dir>. trash, bell, check, refresh and copy animate with action. kerf docs icons has the rest.

More

kerf docs <topic> or kerf mcp: rules, props, lists, async, testing, browser, verify, start, style, surface, config, diagnostics, commands.