Adds .planning/codebase/ with parallel-mapper outputs covering stack, integrations, architecture, structure, conventions, testing, and concerns.
16 KiB
Coding Conventions
Analysis Date: 2026-05-11
This document is the canonical short-form reference for code style, structure, and feature-boundary rules in the Reactive Resume monorepo. The authoritative long-form reference lives in AGENTS.md at the repo root — when in doubt, defer to it.
Tooling Stack
- Formatter + linter: Biome 2.x (
biome.json). Pre-commit hook (lefthook.yml) runsbiome check --write --unsafeon staged JS/TS/JSON files. - Type checker:
tsgo --noEmit(the@typescript/native-previewTS implementation) in every workspace package andapps/web. Usepnpm typecheckat the root orpnpm --filter <pkg> typecheckper package. - TypeScript base config:
packages/config/tsconfig.base.json, consumed bytsconfig.jsonat root and per-packagetsconfig.jsonfiles. - Commit hook: commitlint with
@commitlint/config-conventional(commitlint.config.cjs).body-max-line-lengthis disabled. - Internal CLI:
pnpm checkis write-capable (biome check --write --unsafe .). Usebiome check .(no--write) for read-only inspection.
Biome Configuration (biome.json)
Formatter:
lineWidth: 120indentStyle: "tab"javascript.formatter.quoteStyle: "double"- CSS parser has
tailwindDirectives: true
Linter rules (notable):
recommended: truesuspicious.noExplicitAny: "error"—anyis forbiddensuspicious.noArrayIndexKey: "off"correctness.useExhaustiveDependencies: "info"(not an error)style.useImportType: { level: "on", options: { style: "separatedType" } }—import typeis required when an import is type-only and must be on its own line (not inlined per-specifier)style.noInferrableTypes: "error"— drop redundant annotations likeconst x: number = 1style.noUselessElse: "error"style.useSelfClosingElements: "error"style.useSingleVarDeclarator: "error"style.noParameterAssign: "error"style.useDefaultParameterLast: "error"nursery.useSortedClasses: { level: "warn", fix: "safe", functions: ["clsx", "cva", "cn"] }— Tailwind class strings inside these wrappers are sorted automatically
Import organization: Biome's assist.actions.source.organizeImports is on, grouped in this order (biome.json lines 32-39):
- Type-only imports (
{ "type": true }) - Node built-ins (
":NODE:", excluding Bun) - Vitest + Testing Library (
vitest,vitest/**,@testing-library/**) - External npm packages (
:PACKAGE:, excluding@reactive-resume/**) - Internal workspace packages (
@reactive-resume/**) - App-local aliases / relative paths (
:ALIAS:,:PATH:)
A representative file that demonstrates the order: packages/api/src/services/resume.ts (type imports → node-free externals → @reactive-resume/* → relatives).
Biome ignores: **/.turbo, **/.output, **/.vercel, **/.wrangler, **/coverage, **/reports, **/routeTree.gen.ts.
TypeScript Conventions
Strictness flags in packages/config/tsconfig.base.json are intentionally aggressive:
strict: trueverbatimModuleSyntax: true(pairs with Biome'suseImportTypeenforcement)exactOptionalPropertyTypes: truenoUncheckedIndexedAccess: truenoUncheckedSideEffectImports: truenoUnusedLocals: true,noUnusedParameters: truenoFallthroughCasesInSwitch: trueisolatedModules: true,moduleResolution: "bundler"target: "ESNext",module: "ESNext"
Type-only imports: Always use import type { … } on its own line when an import is only used in type positions. See packages/api/src/services/resume.ts:1-5 and packages/api/src/context.ts:1-2.
any: Banned by Biome (noExplicitAny: "error"). Use unknown and narrow, or use a discriminated union.
Path aliases (web app only): apps/web/tsconfig.json declares:
@/*→./src/*@reactive-resume/ui/*→../../packages/ui/src/*(build-time alias for direct source resolution)
Internal packages do NOT use @/* aliases — they import siblings via relative paths and cross-package code via the @reactive-resume/* export maps.
Package Export Conventions (source-consumed packages)
Internal packages export src files directly via package.json exports. There is no per-package build step; consumers pick up the TS source through bundler/vitest resolution. Do not assume any dist/ output.
Sample (packages/utils/package.json):
{
"name": "@reactive-resume/utils",
"type": "module",
"exports": {
"./color": "./src/color.ts",
"./date": "./src/date.ts",
"./resume/docx": "./src/resume/docx/index.ts",
"./resume/patch": "./src/resume/patch.ts",
"./url-security.node": "./src/url-security.node.ts"
}
}
Conventions when adding cross-package exports:
- Use explicit subpath exports, not wildcards in
@reactive-resume/utils/@reactive-resume/db. Some packages (@reactive-resume/api) do use wildcards like"./services/*"— match the style of the package you're editing. - Filenames ending in
.node.ts(e.g.packages/utils/src/url-security.node.ts,packages/utils/src/monorepo.node.ts) are reserved for Node-only code that must not be imported from the browser bundle. - Filename suffixes
.browser.tsx(e.g.apps/web/src/components/resume/preview.browser.tsx) mark code that must stay out of SSR paths.
React & Web App Conventions
Routing: TanStack Router with file-based routes under apps/web/src/routes.
- Each route file calls
createFileRoute("/path")({ … })and exportsRoute. Example:apps/web/src/routes/auth/login.tsx:18. apps/web/src/routeTree.gen.tsis generated — never hand-edit it (also Biome-ignored).- Server-only handlers live in
server.handlersblocks on routes likeapps/web/src/routes/api/rpc.$.ts,apps/web/src/routes/api/auth.$.ts,apps/web/src/routes/api/health.ts. - Public resume route
apps/web/src/routes/$username/$slug.tsxisssr: "data-only"; nested builder preview isssr: false.
Router context (apps/web/src/router.tsx) provides queryClient, orpc, theme, locale, session, and flags. Read them via Route.useRouteContext() rather than refetching.
Components:
- Functional components only. React 19.
- File naming: kebab-case for files and directories (e.g.
apps/web/src/components/command-palette/,packages/ui/src/components/alert-dialog.tsx,apps/web/src/dialogs/api-key/create.tsx). - Test file:
<name>.test.ts(x)colocated with the implementation (e.g.packages/ui/src/components/button.tsx+packages/ui/src/components/button.test.tsx). - shadcn/Base UI primitive components in
packages/ui/src/components/*.tsxare exported via the./components/*subpath. Hooks via./hooks/*.
Tailwind class strings: Always wrap in clsx, cva, or cn so Biome's useSortedClasses rule can sort them safely.
oRPC Conventions (packages/api)
- Procedures: Build new procedures with
publicProcedureorprotectedProcedurefrompackages/api/src/context.ts:79-99.protectedProcedureadds the authenticatedUserto context and throwsORPCError("UNAUTHORIZED")otherwise; prefer it for anything authenticated. - Routers live in
packages/api/src/routers/*.tsand are composed inpackages/api/src/routers/index.ts(ai,auth,flags,resume,statistics,storage). Each router file may export sub-routers internally (seetagsRouter,statisticsRouter,analysisRouter,updatesRouterinpackages/api/src/routers/resume.ts). - Business logic belongs in
packages/api/src/services/*.ts. Handlers must stay thin: validate input, call a service, return its output. Example pattern:packages/api/src/routers/resume.ts:24-26callsresumeService.tags.list(...). - DTOs / IO schemas live in
packages/api/src/dto/*.tsand are imported asresumeDto.<op>.input/resumeDto.<op>.output. - Errors: Declare typed errors with
.errors({ CODE: { message, status } })on the procedure (seepackages/api/src/routers/resume.ts:282-291). Thrownew ORPCError("CODE")inside services. The web side translates codes to user-facing strings inapps/web/src/libs/error-message.ts. - Route metadata: Every procedure declares
.route({ method, path, tags, operationId, summary, description, successDescription })so the OpenAPI/MCP endpoints stay accurate. - Rate limiting: Apply via
.use(resumeMutationRateLimit)/.use(resumePasswordRateLimit)frompackages/api/src/middleware/rate-limit. - Web exposure: Routers are mounted at
/api/rpcbyapps/web/src/routes/api/rpc.$.ts. The isomorphic client lives atapps/web/src/libs/orpc/client.ts— server-side calls use the in-process router client and browser calls hit/api/rpcwith credentials.
Drizzle Conventions (packages/db)
- Schema location:
packages/db/src/schema/*.ts. Tables exported frompackages/db/src/schema/index.ts. - Client:
packages/db/src/client.ts, exported as@reactive-resume/db/client. - Migrations: Generated to
migrations/at the repo root bydrizzle-kit generate. Usepnpm db:generateafter schema changes. DATABASE_URLhandling:drizzle-kitdoes not auto-load.env. Always exportDATABASE_URLin the shell (or prefix the command) before runningpnpm db:generate/pnpm db:migrate.- Runtime migration:
apps/web/plugins/1.migrate.tsruns migrations on Nitro startup, sopnpm db:migrateis mostly used for first-time setup or debugging. - Patterns observed in
packages/db/src/schema/resume.ts:- Primary keys use
pg.text("id").$defaultFn(() => generateId())(UUIDv7 from@reactive-resume/utils/string). createdAt/updatedAtusewithTimezone: true,.defaultNow(), and$onUpdate(() => new Date()).- JSONB columns get
.$type<T>()for end-to-end typing. - Composite uniques/indexes are declared in the table's tuple callback.
- Foreign keys use
onDelete: "cascade".
- Primary keys use
Schema-First Change Workflow
When changing resume data shape, propagate in this order (per AGENTS.md):
packages/schema/src/resume/*.ts— Zod schemas and types (entry point).packages/api/src/dto/*.ts— API DTOs that re-use those schemas.packages/import/src/*.tsx— importers (json-resume,reactive-resume-json,reactive-resume-v4-json).packages/pdf/src/templates/**— PDF rendering for every template (azurill,bronzor,chikorita,ditgar,ditto,gengar,glalie,kakuna,lapras,leafish,meowth,onyx,pikachu,rhyhorn). Shared filtering:packages/pdf/src/templates/shared/filtering.ts.apps/web/src/— builder forms and any consumer hooks.
Adding/renaming a template requires changes in packages/schema/src/templates.ts, packages/pdf/src/templates/index.ts, the template directory packages/pdf/src/templates/<name>/, and static previews under apps/web/public/templates/{jpg,pdf}/.
Error Handling Patterns
- Services: Throw
new ORPCError("CODE")(e.g.NOT_FOUND,UNAUTHORIZED, customRESUME_LOCKED). Example:packages/api/src/services/resume.ts:54. - Routers: Declare expected codes via
.errors({ … })so callers get typed error narrowing. - Auth helpers in
packages/api/src/context.ts:14-55catch verification errors andconsole.warn(...)rather than throwing, returningnullso the caller can fall through to the next auth method. - Web side:
apps/web/src/libs/error-message.tsexposesgetReadableErrorMessage,getOrpcErrorMessage, andgetResumeErrorMessagefor translating raw errors into UI-safe strings. Pair withsonnertoasts (seeapps/web/src/routes/auth/login.tsx:45-64).
Logging
- No dedicated logging framework. Use
console.warn/console.errorfor diagnostic output, scoped tightly (seepackages/api/src/context.ts:25). - Server logs are also where dev-mode email verification links surface when SMTP is unconfigured.
i18n & Translations (Lingui)
- Library:
@lingui/core,@lingui/reactwith the babel macro plugin enabled inapps/web/vitest.config.tsand Vite config. - Config:
apps/web/lingui.config.ts— source localeen-US, pseudo localezu-ZA, 50+ supported locales. Catalogs live inapps/web/locales/{locale}.po. - Usage:
- Import macros:
import { t } from "@lingui/core/macro"andimport { Trans } from "@lingui/react/macro"(apps/web/src/routes/auth/login.tsx:1-2). - Wrap displayed text in
<Trans>...</Trans>for JSX ort`...`/t({ message, comment })for strings. - Provide
comment:for ambiguous fallback strings (seelogin.tsx:57-60).
- Import macros:
- Extraction:
pnpm lingui:extract(turbo task inapps/web). Crowdin sync runs via.github/workflows/crowdin-sync.yml.
Comments Policy
Comments stay short and explain why, not what. Patterns observed:
- One-line
//comments before a non-obvious decision (vitest.setup.ts:5-7explains whycleanup()is registered manually;packages/utils/src/monorepo.node.test.ts:11explains therealpathSynccall). - JSDoc/TSDoc only on cross-package public functions where intent matters (e.g.
packages/api/src/context.ts:57-63documentsresolveUserFromRequestHeaders). - Inline
/* @__PURE__ */annotations on$onUpdate(() => new Date())in Drizzle schemas (packages/db/src/schema/resume.ts:36). - No commented-out code in commits.
Git Hooks & Commit Style
Lefthook (lefthook.yml):
pre-commit:
parallel: true
jobs:
- name: lint and format
glob: "*.{js,ts,cjs,mjs,d.cts,d.mts,jsx,tsx,json,jsonc}"
run: pnpm biome check --write --unsafe --no-errors-on-unmatched --files-ignore-unknown=true {staged_files}
stage_fixed: true
commit-msg:
jobs:
- name: commitlint
run: pnpm commitlint --edit {1}
The pre-commit hook rewrites staged files with Biome fixes (stage_fixed: true). Run pnpm check before staging to avoid surprises.
Commit messages: Conventional Commits (commitlint.config.cjs extends @commitlint/config-conventional). Examples from git log:
feat: implement an AI chat window for agentic resume buildingfix(pdf): register CJK fallback font so Chinese/Japanese/Korean text renders correctlyfix(lapras): adjust lapras border color to fixed graychore: migrate from jsdom to happy-dom for testing environmentdocs: update AGENTS.md with detailed codebase structuretest: add unit and component tests across the monorepochore(release): v5.1.2
Allowed types: feat, fix, chore, docs, test, refactor, style, perf, build, ci, revert. Scope is optional and lowercase. body-max-line-length is disabled, so long PR bodies are fine.
CI Workflows
.github/workflows/autofix.ymlruns on every PR and push tomain:pnpm install --frozen-lockfile→pnpm knip --fix(prune unused deps) →pnpm check(Biome) →autofix-ci/actionopens fix commits..github/workflows/docker-build.ymlisworkflow_dispatchonly, builds multi-arch Docker images..github/workflows/crowdin-sync.ymlsyncs translation catalogs.- There is no CI workflow that runs
pnpm testtoday. Tests run locally viapnpm test/pnpm test:ciand through turbo'stest:agentreporter for agent-driven runs.
Function & Module Design
- Function size: Most service functions stay under ~40 lines. Bigger flows (e.g.
resumeService.patch) are decomposed into helpers inpackages/api/src/helpers/*andpackages/api/src/services/resume-events.ts. - Parameters: Service helpers consistently take a single object argument (
async ({ id, userId })) rather than positional args. Seepackages/api/src/services/resume.ts:27,41,65. - Return values: Services return plain typed objects; routers shape the response via
.output(schema)so Zod validates at the boundary. - Module boundaries:
- Cross-package imports must go through declared
exportssubpaths. Reaching intopackages/<x>/src/internal-filedirectly is not allowed. packages/utilsexports are narrowly scoped — if you need a new helper for another package, add a new explicit subpath inpackages/utils/package.json.packages/runtime-externalsandpackages/scriptsare support packages; avoid importing them into runtime code.
- Cross-package imports must go through declared
Convention analysis: 2026-05-11