Lint Is Not About Style

Most rules here catch code that compiles, runs and looks right, yet quietly does nothing. Only the linter notices.
Say you write bg-blue-500 on a badge. The page renders and the class is in the DOM, but the badge has no colour, and neither the build nor the browser complains.
A Class With No CSS
The raw palette is stripped from the stylesheet, so the badge renders with no colour.
className="bg-blue-500"
A Field No Agent Can Reach
An arrow around the setter hides it, so the field publishes no agent tool.
onChange={(v) => st.do.setTitleOnTicket(v)}
A Return Value Nobody Gets
Store actions are dispatched as void, so the caller never sees the value.
return ticket;
A Note Every Visitor Downloads
A bang comment survives minification and ships in the browser bundle.
//! remove before launch
Words used on this page
Biome
The formatter and linter this workspace uses. akan lint runs it for you.
grit plugin
A lint rule written in GritQL for Akan and run by Biome. There are 28.
diagnostic
One finding Biome prints: the file, the line, the rule name and a message.
safe fix
A fix Biome applies by itself, such as sorting classes or dropping an unused import.
colour vocabulary
The closed set of semantic colour tokens. The raw Tailwind palette is not in it.
scope
The paths a rule looks at. A file outside a rule's scope never trips it.

Six You Will Meet First

Each diagnostic prints the name of the rule that fired. Find that name below; the fix is mechanical once you know it.
A Colour Outside The Vocabulary
no-raw-palette-class
  • Why: The raw Tailwind palette is stripped from the compiled stylesheet, so bg-blue-500 has no CSS behind it. The badge renders unstyled while the DOM still shows the class.
  • Fix: Use a semantic token such as bg-primary. The hex colour in style goes too (no-inline-color).
apps/myapp/ui/StatusBadge.tsx
A Raw Error
no-throw-raw-error
  • Why: A bare Error reaches the caller as Internal Server Error, with no message and no translation.
  • Fix: Throw an Err that names a key, and register that key in the module dictionary as an [en, ko] pair.
apps/myapp/lib/ticket/ticket.service.ts
Then register the key in the same module's dictionary:
apps/myapp/lib/ticket/ticket.dictionary.ts
A Setter Wrapped In An Arrow
no-unpublished-form-setter
  • Why: Both lines run the same code, but the arrow is an anonymous closure. The control then emits no data-akan-action and publishes no agent tool for the field.
  • Fix: Pass the setter by reference. Normalize a value with the control's transform prop; do several writes in a _postSet<Field> store method.
apps/myapp/lib/ticket/Ticket.Template.tsx
A Value Returned From A Store Action
no-return-in-store-action
  • Why: Every store method is dispatched through st.do.<action>(), which is typed void. The returned value reaches no call site.
  • Fix: Write the value into state with this.set({ ... }). A bare return; guard stays legal.
apps/myapp/lib/ticket/ticket.store.ts
A Hydration Call Made From The Client
no-init-fetch-in-client
  • Why: fetch.init<Model><Suffix> builds the snapshot Load.Units seeds the store from. Called after hydration, it costs two extra round trips for a shell the browser already painted.
  • Fix: Start it in the route, where it resolves before the first byte, and hand the promise to the Zone as init. To reload from the client, call st.do.init<Model><Suffix>().
Before, the Zone loads the list on mount:
apps/myapp/lib/ticket/Ticket.Zone.tsx
After, the route starts the load and hands the promise down:
apps/myapp/page/project/[projectId]/_index.tsx
The Zone only renders what it is handed:
apps/myapp/lib/ticket/Ticket.Zone.tsx
#private In One Of Four Suffixes
no-js-private-class-method
  • Why: The framework merges constant, document, service and store classes by copying methods onto another class. A copied method that calls a # member throws.
  • Fix: Use a TypeScript private method with an underscore prefix. Everywhere else, srvkit/ included, #private stays the house style.
apps/myapp/lib/ticket/ticket.service.ts

Every Rule That Breaks The Build

Twenty-eight are grit plugins written for Akan, each an error unless its row names a warning, which prints without failing the build. The rest are Biome's own. Each plugin looks only at its scope, so a plain package under pkgs/ never trips the module rules.
Colour vocabulary
All five look at every .ts and .tsx file in apps/ and libs/, except tests.
no-raw-palette-class
Raw palette classes such as bg-blue-500 compile to no CSS. Use a token such as bg-primary.
no-arbitrary-color
Colour values such as bg-[#3b82f6] ignore data-theme. A var() reference is fine.
no-daisyui-legacy-class
Removed daisyUI classes such as btn-primary, card-body and bg-base-100 render unstyled.
no-inline-color
A colour literal in style={{ ... }} or a <style> body skips tokens and theme switching. The same literal in an SVG colour attribute (fill, stroke, stopColor, …) or an el.style write is a warning.
no-interpolated-arbitrary-class
A runtime-built arbitrary value like min-h-[${n}px] has no CSS. Use style or literal classes.
daisyUI's dropped colour slots map onto tokens like this. black and white stay in the vocabulary.
base-100
background
base-200
muted
base-300
border
base-content
foreground
<colour>-content
<colour>-foreground
error
destructive
Errors, logs and comments
no-throw-raw-error
A thrown Error. Throw new Err("<module>.error.<key>") and register the key. An Error built anywhere else, to reject, return or store, is a warning.Scope: apps/** libs/**, except tests, *.constant.ts, common/**, env/**
no-deprecated-log-level
logger.log() reads like its own level but emits at info. Write .info().Scope: apps/** libs/**
no-bang-comment-in-client
A //! or /*! comment survives minification and ships. Use // FIXME: instead.Scope: ui/ webkit/ common/ page/, *.constant.ts *.store.ts, module components
no-document-cookie
An app:// page on iOS, macOS and Linux keeps no cookies, so document.cookie reads empty in the app. Use getCookie / setCookie / removeCookie from akanjs/client.Scope: apps/** libs/**, except tests
no-web-storage
localStorage / sessionStorage bypass the store akanjs picks per platform and throw during SSR. Use storage from akanjs/client, or secretStorage for credentials.Scope: apps/** libs/**, except tests
no-web-only-api-outside-webkit
navigator.share, navigator.serviceWorker, Notification.*, navigator.geolocation and navigator.vibrate are missing in some app WebViews. Keep them in a webkit/ hook that branches on isNativeApp().Scope: apps/** libs/** outside webkit/, except tests — a warning
  • //! stays legal on the server. Server files, srvkit/ and CLI code never reach a browser.
  • no-bang-comment-in-client always points at line 1. It is a file-level diagnostic, so search the file for the marker.
Stores, forms and module files
no-return-in-store-action
st.do.<action>() is typed void. Write the value into state with this.set({ ... }).Scope: *.store.ts
no-unpublished-form-setter
An arrow that only forwards to a form setter. Pass st.do.setXOnY by reference.Scope: every .tsx in apps/ libs/
no-init-fetch-in-client
fetch.init<Model><Suffix> or fetch.get<Model>Init<Suffix> on the client. Load it in the route.Scope: "use client" files and *.store.ts
no-model-type-in-util-zone
A cnst model as a prop type of an always-client file. Take an id instead.Scope: *.Util.tsx *.Zone.tsx
no-redeclare-predefined-endpoint
An endpoint that reuses a generated CRUD name, such as create<Model> or view<Model>.Scope: *.signal.ts
no-unguarded-endpoint
An endpoint or slice init() with no guards or guards: [], and a slice with no guards or no root. Each is open to every caller; write guards: [Public] when that is the intent.Scope: *.signal.ts — a warning
no-double-brace-placeholder
{{name}} in an .error() or .translate() entry is never filled in and shows as typed. Write {name}.Scope: *.dictionary.ts
no-js-private-class-method
A #private method. Use a TypeScript private _method() instead.Scope: *.constant.ts *.document.ts *.service.ts *.store.ts
no-static-in-object-light-model
A static on XObject or LightX, which never reaches the full model. Declare it on XInput, X or XInsight.Scope: *.constant.ts
  • Generated CRUD names are taken. <model>, light<Model>, create<Model>, update<Model>, remove<Model>, view<Model>, edit<Model> and merge<Model> already exist, so give a custom endpoint another name.
Server components
Pages, Unit and View are always server components. These rules keep client code out of them.
no-import-client-functions
A React hook or st imported into a server component. Move the interaction out.Scope: page/** *.Unit.tsx *.View.tsx
no-use-client-in-server
"use client" on a file that is always a server component. Split the interaction out.Scope: page/** *.Unit.tsx *.View.tsx
non-scalar-props-restricted
A function passed as a prop from a server component. Only loader, render and of take one.Scope: page/** *.Unit.tsx *.View.tsx
no-async-component-in-ui
An async ui/ component breaks under a client parent. Await in the page instead.Scope: ui/**/*.tsx
Imports
no-deep-internal-import
An @apps/@libs path past <name>/<entry>, ../../ in a module file, or ../ in its .tsx.Scope: module files, page/**, barrels
no-import-external-library
A third-party package. Re-export it through a lib's common/, webkit/ or ui/ first.Scope: module files, page/**, barrels
no-import-server-in-client
Imports a *.document/*.dictionary/*.service/*.signal file, srvkit/ or a server entry.Scope: ui/ webkit/ page/ common/, *.store.ts *.constant.ts, every .tsx
no-import-client-in-server
Imports a *.store, a module component, ui/, webkit/, a client entry or a client barrel.Scope: *.document *.dictionary *.service *.signal srvkit/ common/ *.constant
  • import type is always allowed. It is erased before bundling. A mixed value-and-type import is not exempt.
  • common/ and *.constant.ts obey both directions, so shared code reaches neither side.
  • Barrels count too. A client file may not import db, srv, sig, dict, option or useServer; a server file may not import st, store or useClient.
  • Akan's own packages pass. Relative paths, akanjs, @akanjs/*, @apps/*, @libs/*, @pkgs/*, react*, @playwright/* and bun:test are not third-party imports.
  • Cross-module constants are the exception. A .ts module file may import ../map/map.constant.
Biome's own rules
These apply to every file. The two that are off are off on purpose.
nursery/useSortedClasses
Sorts Tailwind classes, cn() string arguments too. Never hand-order or undo its output.Level: error, safe fix
suspicious/noConsole
console.log and console.debug. Only assert, error, info and warn are allowed.Level: error
correctness/noUnusedImports
akan lint deletes the unused import on its fix pass instead of reporting it.Level: error, safe fix
suspicious/noArrayIndexKey
Off on purpose: key={idx} for an embedded scalar with no id of its own is intended.Level: off
correctness/useExhaustiveDependencies
Off on purpose: the short dependency arrays in this workspace are deliberate.Level: off

Suppressing A Rule

Sometimes a fixed colour is right: an OS-chrome mockup, a data-visualization scale, a vendor's brand. Suppress that one spot, and always write the reason.
// biome-ignore lint/plugin: <reason>
Turns plugin rules off for the next line or statement.
{/* biome-ignore lint/plugin: <reason> */}
The same inside JSX, placed above one element.
// biome-ignore-all lint/plugin: <reason>
At the top of a file, turns plugin rules off for the whole file.
// biome-ignore lint/suspicious/noConsole: <reason>
Biome's own rules are named by their group and rule name instead.
The JSX form looks like this in a real component:
apps/myapp/ui/BrowserChrome.tsx
  • Every suppression carries a reason. There is no bare disable block anywhere in this workspace.
  • Keep the scope small. Use the file-level form only when every hit in the file has the same reason, like a data-viz palette file.

Commands And Configuration

akan lint formats and fixes one app, lib or package. bunx biome check only reports:
Terminal
  • Up to 200 diagnostics are printed. Biome's own default is 20 with no count, which reads as progress when only the mix of findings changed.
  • --fix false still runs every akan lint check. It skips only Biome's fixes, so the checks below still run; bunx biome check runs Biome alone.
What akan lint checks after Biome
page/styles.css
Each theme's text and background token pair must meet WCAG contrast.
ui/Recipe/*
A recipe with no variant to choose fails. Make it a component or a class constant.
AGENTS.md
A stale ## Recipes In Scope block fails. Run akan sync <name> to regenerate it.
Where the configuration lives
biome.json
Sits at the workspace root and extends @akanjs/devkit/biome.base.json.
@akanjs/devkit/biome.base.json
Sets every rule's level and scopes each grit plugin to the paths it applies to.
@akanjs/devkit/lint/*.grit
The plugin sources, one file per rule. Most open with a comment on what breaks without it.

Released under the MIT License

Connect your AI to these docs

MCPhttps://akanjs.com/mcp
Copyright © 2026 Akan.js All rights reserved.System managed bybassman