YouBothAgent▾
You — Business rules and flows you own. Read these yourself.
Both — Know the idea; your agent follows the details.
Agent — Conventions and references your agent follows. Look up as needed.
Workspace▾
App & Library▾
Domain▾
Scalar▾
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 launchWords used on this page
TermDescription
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.



akan lint rewrites files. --fix is on by default, so run it only on the app or lib you touched (akan lint myapp). A repo-wide run in a dirty tree edits work nobody asked it to; use bunx biome check "<path>" when you only want a report.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-500has 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 instylegoes too (no-inline-color).
apps/myapp/ui/StatusBadge.tsx
A Raw Error
no-throw-raw-error- Why: A bare
Errorreaches the caller asInternal Server Error, with no message and no translation. - Fix: Throw an
Errthat 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-actionand publishes no agent tool for the field. - Fix: Pass the setter by reference. Normalize a value with the control's
transformprop; 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 typedvoid. The returned value reaches no call site. - Fix: Write the value into state with
this.set({ ... }). A barereturn;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 snapshotLoad.Unitsseeds 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, callst.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,serviceandstoreclasses by copying methods onto another class. A copied method that calls a#member throws. - Fix: Use a TypeScript
privatemethod with an underscore prefix. Everywhere else,srvkit/included,#privatestays 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.RuleDescription
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.daisyUI slotToken to use
base-100
backgroundbase-200
mutedbase-300
borderbase-content
foreground<colour>-content
<colour>-foregrounderror
destructiveErrors, logs and comments
RuleDescription
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 componentsno-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 testsno-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 testsno-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-clientalways points at line 1. It is a file-level diagnostic, so search the file for the marker.
Stores, forms and module files
RuleDescription
no-return-in-store-action
st.do.<action>() is typed void. Write the value into state with this.set({ ... }).Scope: *.store.tsno-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.tsno-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.tsxno-redeclare-predefined-endpoint
An endpoint that reuses a generated CRUD name, such as
create<Model> or view<Model>.Scope: *.signal.tsno-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 warningno-double-brace-placeholder
{{name}} in an .error() or .translate() entry is never filled in and shows as typed. Write {name}.Scope: *.dictionary.tsno-js-private-class-method
A
#private method. Use a TypeScript private _method() instead.Scope: *.constant.ts *.document.ts *.service.ts *.store.tsno-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>andmerge<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.RuleDescription
no-import-client-functions
A React hook or
st imported into a server component. Move the interaction out.Scope: page/** *.Unit.tsx *.View.tsxno-use-client-in-server
"use client" on a file that is always a server component. Split the interaction out.Scope: page/** *.Unit.tsx *.View.tsxnon-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.tsxno-async-component-in-ui
An async
ui/ component breaks under a client parent. Await in the page instead.Scope: ui/**/*.tsxImports
RuleDescription
no-deep-internal-import
An
@apps/@libs path past <name>/<entry>, ../../ in a module file, or ../ in its .tsx.Scope: module files, page/**, barrelsno-import-external-library
A third-party package. Re-export it through a lib's
common/, webkit/ or ui/ first.Scope: module files, page/**, barrelsno-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 .tsxno-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/ *.constantimport typeis always allowed. It is erased before bundling. A mixed value-and-type import is not exempt.common/and*.constant.tsobey both directions, so shared code reaches neither side.- Barrels count too. A client file may not import
db,srv,sig,dict,optionoruseServer; a server file may not importst,storeoruseClient. - Akan's own packages pass. Relative paths,
akanjs,@akanjs/*,@apps/*,@libs/*,@pkgs/*,react*,@playwright/*andbun:testare not third-party imports. - Cross-module constants are the exception. A
.tsmodule file may import../map/map.constant.
Biome's own rules
These apply to every file. The two that are off are off on purpose.
RuleDescription
nursery/useSortedClasses
Sorts Tailwind classes,
cn() string arguments too. Never hand-order or undo its output.Level: error, safe fixsuspicious/noConsole
console.log and console.debug. Only assert, error, info and warn are allowed.Level: errorcorrectness/noUnusedImports
akan lint deletes the unused import on its fix pass instead of reporting it.Level: error, safe fixsuspicious/noArrayIndexKey
Off on purpose:
key={idx} for an embedded scalar with no id of its own is intended.Level: offcorrectness/useExhaustiveDependencies
Off on purpose: the short dependency arrays in this workspace are deliberate.Level:
offSuppressing 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.
FormDescription
// 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.


Write
lint/plugin, not plugin. The bare // biome-ignore plugin: form that Biome's category name suggests is accepted and looks right, but it does nothing: the rule still fires.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 falsestill runs everyakan lintcheck. It skips only Biome's fixes, so the checks below still run;bunx biome checkruns Biome alone.
What akan lint checks after Biome
FileDescription
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
FileDescription
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.



biome.json is strict JSON, and one comment breaks it. akan lint reports the parse error on its line, but a bare biome check silently falls back to other configs and names a file you did not edit, or runs without your rules. Rename it to biome.jsonc to document a disabled rule.