Code style & lint¶
The pre-commit hook runs ESLint + Biome + Prettier + the architecture validator + canaries + guideline-coverage on every commit. Skipping any of them is not an option.
Quality caps (per-function LoC, file size, complexity, parameter count) live in CLEAN_CODE.md. That file is the single source of truth — do NOT restate the numbers here. The
lint:guideline-coveragegate assertseslint.config.mjsactually enforces those caps for every Pipeline cluster.
Project-specific rules (beyond defaults)¶
SOLID + OCP¶
Open-closed pattern enforced via eslint.config.mjs max-depth: 1 and complexity rules.
| Banned | Allowed |
|---|---|
if (bank === 'amex') {} else if (...) {} ladders | Lookup table — MAP[bank]?.(...) |
| Functions over the cluster cap (see CLEAN_CODE.md) | Extract helpers |
class extension chains | Composition + declarative config |
No CSS selectors in interaction code¶
| Banned in interaction code | Allowed |
|---|---|
page.$$('div#login'), $eval, querySelector | getByText, getByRole, getByPlaceholder (Playwright text-first) |
Hardcoded #login-form, .btn-submit strings | The 7-strategy SelectorResolver |
waitForSelector('#id') | waitForText / waitForRole |
CSS selectors are allowed in parsing / extraction code (table walks, date-picker grids, etc.) — the rule targets user-facing flow only.
No null / undefined returns¶
| Banned | Allowed |
|---|---|
function foo(): T | null | function foo(): Procedure<T> |
return null;, return;, return undefined; | return succeed(value); / return fail(type, msg); |
value! non-null assertion | Optional chaining + explicit guard |
No raw PII in logs¶
| Banned | Allowed |
|---|---|
LOG.info(\account ${accountId}`)|LOG.info({ account: maskTail4(accountId) })` | |
LOG.debug({ result }) (whole payload) | LOG.debug({ stage: 'final', resolvedCount }) |
console.log(...) (any) | Use the typed logger |
No throw new Error across module boundaries¶
| Banned | Allowed |
|---|---|
throw new Error('...') | throw new ScraperError('...') |
return fail(ScraperErrorTypes.Generic, '...') (preferred in pipeline code) |
No nested call expressions¶
The no-restricted-syntax rule blocks foo(bar(x)) patterns (where bar(x) is the argument to foo). Extract intermediate variables for debuggability:
// ❌ banned
expect(isOk(result)).toBe(true);
// ✅ allowed
const isSuccess = isOk(result);
expect(isSuccess).toBe(true);
Test files have the assertOk helper for this exact pattern.
Auto-fix vs hand-fix¶
| Tool | Auto-fix command | What it handles |
|---|---|---|
| Prettier | npm run format | Whitespace, quotes, trailing commas, line wraps |
| ESLint | npm run lint:fix | Import order, unused imports, trivial style |
| Biome | npx biome check src --write | Some safe semantic fixes (organise imports, simplify expressions) |
Architecture violations + canaries + dead code must be hand-fixed. They flag invariant breaks, not style.
Common errors and their fixes¶
| Error | Fix |
|---|---|
File has too many lines (N). Maximum allowed is 600 | Split the file — extract a *Helpers.ts or *Branches.test.ts |
🚫 FORBIDDEN NESTED CALL | Extract the nested call to a variable |
🚫 ARCHITECTURE: Functions cannot return 'null' or 'undefined' | Switch to Procedure<T> |
Do not use 'throw new Error()' | Use ScraperError (or fail() if you're in pipeline code) |
🚫 TYPE SKIP: Do not declare variables as 'unknown' | Cast to the concrete type immediately at the boundary |
🚫 LINT SKIP: Do not disable ESLint rules | Don't use // eslint-disable — fix the underlying issue |
When in doubt¶
Read the Architecture → Layer separation page first. Most surprise-violations come from accidentally importing across a layer boundary; the fix is usually moving a helper, not silencing the rule.