Coding standards
Last reviewed: 2026-10-06.
The rules code in this repository follows. Where a tool decides, the tool wins; CI runs
the tools on every pull request (.github/workflows/ci.yml).
Run them before pushing:
(cd server && mix format && mix credo --strict && mix compile --warnings-as-errors)npm run lint # npm run lint:fix applies the JavaScript layout for youCONTRIBUTING.md has the rules that protect users; they apply to every language below.
Elixir (server/)
Section titled “Elixir (server/)”- Layout is whatever
mix formatproduces (server/.formatter.exs). CI fails on unformatted code. - Credo in strict mode (
server/.credo.exs) decides the rest: its default checks, low-priority ones included (for example: a nested module called by its full name gets an alias at the top; aliases in alphabetical order). The config setsstrict: true, so a plainmix credoreports what CI'smix credo --strictreports, and CI fails on any issue. A check is switched off in code (# credo:disable-for-next-line) only with a comment saying why; there are none today. - No compiler warnings: CI compiles with
--warnings-as-errors, on the oldest and the newest Elixir it supports, and runs the tests withmix test --warnings-as-errors, so a warning in a test file fails too. - Where both are silent, follow the community Elixir Style Guide.
- Project rules:
- Settings are read only through
Kotiko.Config; a new setting is added there, toconfig/runtime.exsand to docs/reference/configuration.md (a test checks all three). - A new route is added to docs/reference/http-api.md in the same pull request (a test checks it).
- No user text in logs above debug level: no words, page text, keys or tokens.
- Code that makes HTTP requests takes
req_options, so tests can stub it withReq.Test(Kotiko.LLM.ClientandKotiko.Telegramdo). Tests never reach the network. - A secret is never echoed in an error message.
- Settings are read only through
JavaScript (extension/, test/, scripts/, spec/tools/)
Section titled “JavaScript (extension/, test/, scripts/, spec/tools/)”- ESLint (
eslint.config.js) decides, andnpm run lintmust pass (CI runs it on every pull request; any finding is an error, there are no warnings). It checks four things:- Correctness: ESLint's recommended rules, plus stricter ones that catch real
mistakes:
===everywhere exceptx == null(null or undefined), no shadowed names, no unused variables or arguments (an argument a callback must take but doesn't use starts with_), no assignments to parameters, noeval,new Functionor string timers, no expression statements such asa && b()or(a(), b()), no closures over a loop's changing variables. The full list isSTRICTERin the config. One exception: Playwright tests hand values to callbacks that run in the browser (page.evaluate((id) => ..., id)) under the same name, so shadowing is allowed there. - One idiom:
constunless reassigned, nevervar; object shorthand;?.,??=,||=and spread instead of their long forms; dot access where possible. - Layout:
LAYOUTin the config, with@stylistic/eslint-plugin(the formatting rules ESLint itself moved out of its core): two-space indent, double quotes (a template literal or single quotes only to avoid escaping), semicolons, trailing commas on multi-line lists, a multi-line ternary's?and:at the start of the line,{ a }and[a]spacing, quotes on object keys only where needed, one blank line at most, LF and a final newline.npm run lint:fix(eslint . --fix) applies it. There is no line-length limit: a line is as long as reads well, and short lines are not forced because the popup has a hard size cap (slice 20 §8).extension/spec/spec.jsis generated and skips the layout rules. - Safety:
eslint-plugin-no-unsanitizedand the HTML-string ban below.
- Correctness: ESLint's recommended rules, plus stricter ones that catch real
mistakes:
web-ext lintchecks the extension package (also part ofnpm run lint).- Why not Prettier: it was measured and not adopted (slice 53, open question 4). At print widths from 100 to 200 it rewrote 137 to 188 files (9,085 to 31,918 added lines) and grew what the popup loads at open by 1.6 to 6.5 KB, past its 128 KB cap. The ESLint layout rules describe the code as it was written: adopting them changed 12 files by 22 lines, all whitespace or quotes.
.editorconfigsets two spaces, LF and UTF-8 for every editor.- This section is the JavaScript style guide for the extension:
- Classic scripts, no modules and no bundler. Each file attaches one namespace to
globalThis(for exampleKotikoI18n), and pure modules also export themselves whenmoduleexists, so Node tests can load them. - No runtime npm code in
extension/: what is in the folder is what ships, so store reviewers can read it. - Build DOM with
textContentand DOM methods only, never HTML strings (innerHTML,outerHTML,insertAdjacentHTMLare banned by ESLint). - Every user-facing string goes through the i18n helper
t()(extension/lib/i18n.js) and lives inextension/_locales/en/messages.json; other locales are optional. - Errors travel as codes (slice 25) and become words only in the interface.
- Classic scripts, no modules and no bundler. Each file attaches one namespace to
- Node scripts (
*.mjs) are ES modules with no dependencies unless they are dev tools.
Shell (server/*.sh)
Section titled “Shell (server/*.sh)”- ShellCheck clean; CI runs it.
- Start with
set -euo pipefail; quote every expansion. - Where ShellCheck is silent, follow the Google Shell Style Guide.
HTML and CSS (extension/)
Section titled “HTML and CSS (extension/)”- Colours, spacing and type come from the design system's tokens
(
extension/ui/tokens.css, slice 06), not literal values.node extension/ui/tools/contrast.mjschecks every text and control colour pair against WCAG 2.2 AA in both themes. - No inline scripts and no event-handler attributes (
onclick="..."); scripts are files, wired up withaddEventListener. - Layouts use logical properties (
margin-inline-start), so right-to-left text works.
Markdown
Section titled “Markdown”- Plain language, short sentences, no emojis. Wrap prose at about 90 characters.
- Specs follow
slices/TEMPLATE.md. - New files carry an SPDX header, or are covered by
REUSE.toml(Markdown is);pipx run reuse lintchecks it.
Every language
Section titled “Every language”- Slice 50 section 7's
cross-cutting rules: no English-named base concepts in code (
gloss,base_lang, neverenglish;scripts/check-base-neutral.mjschecks it), language names fromIntl, never assuming spaces between words, per-language rules as data inspec/lang/, locale-aware text handling. Its rule 13 (public text in English and Spanish) is replaced by DECISIONS 2026-10-05: English is required, other languages are optional. - The project's old names are not used outside history (
scripts/check-old-name.mjs).
Commits and pull requests
Section titled “Commits and pull requests”Conventional Commits, as described in CONTRIBUTING.md.
