Conventions actually followed by the codebase. When this document and the code disagree, the code wins — open a PR to fix the doc, not the code (unless the code is the deviation, in which case fix the code).
Sections marked (deviation) point at known inconsistencies that are being tracked, not patterns to follow.
| Surface | Language |
|---|---|
| Code comments | English |
| User-facing strings (UI labels, errors shown to users) | French |
Documentation (*.md) |
English |
| Commit messages | French (matches the existing log) |
| Variable names | English |
Existing code has French comments in places (e.g. older LoginPage.tsx).
Don't bulk-translate; convert opportunistically when you're already touching
the file. Don't introduce new French comments.
| Layer | Path | Naming | Example |
|---|---|---|---|
| React component | components/<area>/ |
PascalCase.tsx |
LoginPage.tsx, GraphRenderer3D.tsx |
| Component test (colocated) | same dir | PascalCase.test.tsx |
LoginPage.test.tsx |
| Component CSS (colocated) | same dir | PascalCase.css |
AdminPanel.css |
| React Context | context/ |
XxxContext.tsx |
AuthContext.tsx, GraphContext.tsx |
| Service | services/ |
camelCase.ts |
api.ts, errorHandler.ts, websocket.ts |
| Type definitions | types/index.ts |
one file, named exports | types/index.ts |
| Ambient module declarations | @types/ |
*.d.ts |
3d-force-graph.d.ts |
| Test stubs | test-stubs/ |
camelCase.ts |
empty.ts |
Components are organized by feature area (auth/, graphs/, admin/,
dashboard/, user/, common/, layout/), not by component type. Don't
introduce a components/buttons/ or components/forms/ directory.
(deviation) — services/adminService.ts uses a Service suffix; the
others (api.ts, errorHandler.ts, websocket.ts) don't. New services
should follow the non-suffixed form: feature.ts, not featureService.ts.
There is currently no hooks/ directory. Custom hooks live inside contexts
(AuthContext exports useAuth, etc.). If you write a reusable hook that
isn't context-bound, create hooks/useXxx.ts.
| Layer | Path | Naming | Example |
|---|---|---|---|
| Express route module | routes/ |
kebab-case.js (lowercase, hyphens for compounds) |
auth.js, import-export.js |
| Sequelize model | models/ |
PascalCase.js (matches the model name) |
User.js, GraphShare.js |
| Models barrel | models/index.js |
the only place that wires associations | |
| Middleware | middleware/ |
camelCase.js |
asyncHandler.js, errorHandler.js, auth.js |
| Utility | utils/ |
camelCase.js |
dotValidator.js, fileUpload.js, logger.js |
| Service | services/ |
camelCase.js |
emailService.js |
| Sequelize config | config/ |
one file per concern | database.js |
Tests live outside src/, mirrored by layer:
backend/tests/
setup.js (Jest setupFilesAfterEnv)
unit/
models/<Name>.test.js
middleware/<name>.test.js
services/<name>.test.js
utils/<name>.test.js
integration/
routes/<route>.test.js
| Kind | Style | Example |
|---|---|---|
| Variables, function parameters | camelCase |
userId, dotContent |
| Functions | camelCase |
validateSession, setupAdminUser |
| React components, Sequelize models, classes, TS interfaces | PascalCase |
GraphRenderer3D, User, ApiService |
| Module-scope constants | SCREAMING_SNAKE_CASE |
API_BASE_URL, NO_NAV_PATHS, DEFAULT_GRAPH_OPTIONS |
| Database columns | snake_case |
user_id, dot_code, permission_level, start_time |
| URL path segments | kebab-case |
/api/import-export, /validate-dot, /parse-dot |
| Environment variables | SCREAMING_SNAKE_CASE |
SESSION_SECRET, VITE_API_URL, REDIS_URL |
| Vite-exposed env vars | VITE_* prefix (mandatory) |
VITE_API_URL, VITE_WS_URL |
Don't mirror DB column names into JS variables. Sequelize already maps
user_id ↔ userId via model attributes; use the camelCase form
everywhere in JS.
Order, without blank lines between groups (current style):
// 1. React + react-router
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
// 2. Third-party (MUI, axios, …)
import { Box, Button } from '@mui/material';
// 3. Internal — absolute-style (services, contexts, types)
import apiService from '../../services/api';
import { useAuth } from '../../context/AuthContext';
import type { User } from '../../types';
// 4. Internal — same-folder relative
import { LoginForm } from './LoginForm';type imports use the import type form when only types are needed.
Order, no blank lines between groups:
const express = require('express');
const { body, validationResult } = require('express-validator');
const { User } = require('../models');
const { authRateLimit, validateSession } = require('../middleware/auth');
const { asyncHandler } = require('../middleware/errorHandler');
const logger = require('../utils/logger');
const router = express.Router();router declaration goes last in the import block, blank-line-separated
from the requires.
- Functional components only.
React.FC<Props>(orFC<Props>after named import) is the existing style. Don't introduce class components. - Local state →
useState. Multiple linked fields with reducer logic →useReducer. Cross-route state → an existing Context. - Side effects →
useEffectwith an explicit dependency array. If you intentionally pin a partial deps list (e.g. load-on-mount), add:See// eslint-disable-next-line react-hooks/exhaustive-deps // <one-line reason>
AdminPanel.tsxload effects for prior art. - Provider tree is fixed and ordered:
ErrorBoundary → ThemeProvider → Router → AuthProvider → GraphProvider → SimulationProvider → NotificationProvider. Don't add a 5th application-wide provider without a justification documented in an ADR (planned indoc/adr/). - Lazy load heavy routes with
React.lazy+<Suspense>. The current lazy split isGraphEditor,GraphViewer,AdminPanel— keep these lazy, and lazy-load any new heavy page. - Auth guard uses
<ProtectedRoute>fromApp.tsx. Don't reimplement it per route.
- All async route handlers wrapped in
asyncHandler(middleware/asyncHandler.js) to forward thrown errors to the central error middleware. Nakedasync (req, res) =>in routes is a lint-pass smell — wrap it. - Validation →
express-validator'sbody() / query() / param()chain inside the route definition. The result ofvalidationResult(req)is formatted by the errorHandler.joiis also installed and used at config boundaries (env-var validation, where it fits better). - Errors →
throw(ornext(err)); neverres.status(500).json(...)inline. Letmiddleware/errorHandler.jsshape the response. - Logging →
loggerfromutils/logger.js(Winston). Noconsole.log/console.errorinsrc/.morganis piped into the same logger. - Sessions → never read or write the session map directly. Use
validateSessionmiddleware to populatereq.user, then readreq.user.id,req.user.role. - Sequelize associations are wired in
models/index.js. When you add a model, register its associations there, not inside the model file. - DB columns are
snake_case, but Sequelize attributes are exposed ascamelCaseto JS — use the camelCase form in code (user.firstName, notuser.first_name).
- English only in new code.
- WHY, not WHAT. Don't restate what the code does. Document:
- hidden constraints (e.g. "must run before X because Y")
- invariants the code relies on
- non-obvious workarounds with a link or commit ref
- load-bearing behaviors (
GraphRenderer3D.tsxhas many — preserve them)
- JSDoc for public route handlers and exported helpers, with at least
the path/method (for routes) and a one-line description. Existing
routes/auth.jsis a good template. - No emojis in code or commit messages. Emojis in user-facing strings (UI text, notifications) are fine.
- No AI attribution lines in any artifact (commits, PRs, comments).
- MUI
sxprop is the default for component-scoped styling. Use it for layout, spacing, simple conditional styles. .cssfile colocated with the component only whensxisn't enough: global selectors, complex animations, third-party widget overrides. SeeAdminPanel.cssas the reference case.- Theme tokens (colors, typography, spacing scale, scrollbar) live in
App.tsx'screateTheme. Don't hardcode#4caf50/#ff6b35in components — read from theme. - The dark theme is the only theme. Don't add a light-theme path without a product decision documented in an ADR.
- File naming:
*.test.jsonly. No*.spec.js. - Location:
tests/unit/<layer>/<Name>.test.jsortests/integration/routes/<route>.test.js. tests/setup.jsis auto-loaded viasetupFilesAfterEnv(configured inbackend/package.json) and silences the Winston logger globally — put cross-suite mocks there.- For modules that install module-level timers (e.g.
utils/fileUpload.js's 1hsetInterval), calljest.useFakeTimers()beforerequireto avoid keeping the event loop alive after tests finish.
- File naming:
*.test.ts/*.test.tsx, colocated with the source. - Use
vi.fn()/vi.mock()— notjest.*.vitest/globalsexposesdescribe,test,expect,beforeEach,afterEach,viwithout imports (configured invite.config.tstest.globals: true). - Form submit assertions: prefer
fireEvent.submit(form)overuserEvent.click(submitButton)— under jsdom + Vitest, the click fires but the form's nativesubmitevent doesn't. - Tests that mount
GraphRenderer3D(or anything usingResizeObserver/three-spritetext) must stub these at the top of the file: jsdom doesn't shipResizeObserver, andthree-spritetextpulls Three.js modules.3d-force-graphis best mocked as a chainable spy that returnsthis— seeGraphRenderer3D.test.tsxfor the pattern. - The renderer's parser calls
fetch(${VITE_API_URL}/public/parse-dot), so stubglobalThis.fetchto bypass the backend. - Don't render
Appfrom a test without first mockingservices/api— the auth probe runs on mount and will hang the test.
| Tool | Status |
|---|---|
Frontend ESLint (eslint.config.js, ESLint 9 flat config) |
Wired, repo lint-clean. Keep it that way — CI will fail otherwise. |
Backend ESLint (.eslintrc.json, eslint:recommended only) |
Minimal. Migration to a stricter preset is planned. |
| Prettier | Wired at the repo root (.prettierrc.json). Runs on commit via lint-staged for *.{json,md,yml,yaml}. Source files are not Prettier-reformatted in bulk (see .prettierignore) — ESLint handles them on save / on commit. |
.editorconfig |
Present at the repo root. 2-space indent, LF line endings, final newline, trim trailing whitespace (except in Markdown). |
| Husky + lint-staged | Pre-commit hook (.husky/pre-commit) runs lint-staged: Prettier on config/docs, npm run lint -- --fix on the affected sub-package for changed JS/TS files. To bypass in a true emergency: git commit --no-verify (don't make a habit of it). |
unused-vars (frontend) |
@typescript-eslint/no-unused-vars ignores caught errors named _, err, error — keep error bindings even when unused, for stack traces. |
Before committing, both packages must lint-clean:
( cd backend && npm run lint )
( cd frontend && npm run lint )- Commit messages: French, present tense, no body unless useful.
<scope>: <subject>for scoped changes (e.g.GraphRenderer3D: …,CI: …,Doc: …).<subject>for repo-wide changes (e.g.Ignore les artefacts runtime).- First line ≤ ~70 chars. Body wrapped at ~72 if added.
- No AI attribution anywhere: no
Co-Authored-By, noGenerated with Claude Code, noAnthropicmention in commits, PR descriptions, comments, or code. - Branches: working directly on
mainis current practice for this solo repo. If branches are introduced, usefeature/<slug>,fix/<slug>,chore/<slug>. - Daily changelog under
doc/changelog/YYYY-MM-DD.md. One line per file modified, listing the actual change. If a change is reverted within the day, delete the line rather than logging "added then removed". - Never commit secrets.
.envfiles are gitignored and must stay so.
- Architecture overview →
/ARCHITECTURE.md - Backend topic guides →
/backend/doc/ - DOT 3D specification →
/doc/dot-3d/ - Architecture decisions →
doc/adr/ - Contribution workflow →
/CONTRIBUTING.md