Coding style:
- Favor
async run() {overrun = async () => {inside ES6 classes - Favor
if (!(err instanceof Error)) { throw new Error(Was thrown a non-error: ${err}) }insidecatchblocks to ensure theerroris always an instance ofError - Favor using real paths (
../lib/schemas.ts) over aliases (@/app/lib/schemas). - Favor
for (const comment of comments) {overcomments.forEach((comment) => { - Favor named exports over default exports, with the exception of Next.js pages
- Avoid namespace
*imports unless the API is intentionally consumed as a namespace. Prefer named imports so usage is explicit and bundlers can optimize. - Avoid broad file-level lint suppressions. Prefer the narrowest scoped suppression on the exact line, with a reason when the exception is not obvious.
- Preserve existing dependency import paths and module-system interop unless the change is required
and explained. Do not rewrite package imports to
node_modules/..., switch ESM/CJS shapes, or add duplicate export styles just to satisfy local tooling. - Do not wrap each function body and function call in
try/catchblocks. It pollutes the code. Assume we will always have an e.g.main().catch((err) => { console.error(err); process.exit(1) })to catch us. I repeat: Avoid over-use of try-catch such astry { // foo } catch (err) { console.error('error while foo'); throw err }, assume we catch errors on a higher level and do not need the extra explananation. - If you must use try/catch, for simple cases, favor
alphalib/tryCatch.ts(const [err, data] = await tryCatch(promise)) overlet data; try { data = await promise } catch (err) { } - Before creating new files and new code, see if we can leverage existing work, maybe slighty adapt that without breaking BC, to keep things DRY.
- Favor early exits, so quickly
continue,return false(orthrowif needed), over nesting everything in positive conditions, creating christmas trees. - Use Prettier with 100 char line width, single quotes for JS/TS, semi: false
- Use descriptive names: PascalCase for components/types, camelCase for variables/methods/schemas
- Alphabetize imports, group by source type (built-in/external/internal)
- Preserve existing sorted lists and config ordering unless intentionally changing the order.
- Favor US English over UK English, so
summarizeErroroversummarise Error - Favor
.replaceAll('a', 'b)over.replace(/a/g, 'b')or.replace(new RegExp('a', 'g'), 'b')when the only need for regeses was replacing all strings. That's usually both easier to read and more performant. - Use typographic characters: ellipsis (
…) instead of..., curly quotes ('") instead of straight quotes in user-facing text - Generated text files should end with a trailing newline. Do not trim serializer output when the serializer intentionally emits POSIX-style text.
- Comments should explain why code exists or why an exception is needed, not narrate what the next line already says.
- Use JSDoc block comments for exported constants, functions, classes, and types when documenting their purpose or contract. Use regular line comments for local quirks, reasoning, or exceptions.
- Do not put TODOs, internal implementation notes, or future-work placeholders in user-facing text or schema descriptions. Put those in code comments, issues, or docs for maintainers instead.
- Put API keys and secrets in
.envfiles, not hardcoded in components - Do not return raw errors, stack traces, third-party responses, payment objects, database errors, or credential data to clients. Show sanitized user-facing messages and log only redacted diagnostic details server-side.
- When wrapping or rethrowing an error, preserve the original value with
new Error(message, { cause: error })when possible instead of stringifying it away. - Remove temporary debug logging/instrumentation before merge. Keep new logs only when they are intentional, useful in production, and do not expose sensitive data.
- Check for existing hooks before creating new ones (e.g.,
useUppy()for Uppy functionality)
General:
- Do not touch
.envfiles! - Favor Yarn (4) over npm
- Before starting a dev server, first check whether a suitable one is already running and use that if possible. If none is available and local browser/e2e validation needs one, start the documented dev server, capture its log path/pid, and stop it when you are done unless the user asked to keep it running.
- Avoid blocking the conversation with terminal commands. For example: A) most of my git commands run through pagers, so pipe their output to
catto avoid blocking the terminal. B) You can usetailfor logs, but be smart and use-ninstead of-f, or the conversation will block - Use the
ghtool to interact with GitHub (search/view an Issue, create a PR). - When using
fetch()directly, checkresponse.okbefore parsing the body, and surface non-2xx responses as errors with enough context for debugging. - Treat
AGENTS.mdandCLAUDE.mdas generated artifacts (single source of truth is.ai/rules/), managed by~/code/content/_scripts/alphalib-sync.ts; never edit those files directly. If you'd like to make a modification, do it here in.ai/rules/and the script will ensure proper preservation and syncing. If you need a rule specific to this repo, add it to.ai/rules/repo.mdc. - All new files are to be in TypeScript. Even if someone suggests: make this new foo3 feature, model it after
foo1.js, create:foo3.ts. Chances are, afoo2.tsalready exist that you can take a look at also for inspiration.
- Prefer user-centric locators:
getByRole/getByTextwith accessible names; avoidpage.locator('body'),innerText(), or raw CSS unless there is no accessible alternative. - Treat
data-*attributes as implementation details in browser tests unless they are the only stable contract available. If UI needs testing via state such as notifications, prefer adding or using an accessible role/name/state contract over asserting hidden implementation attributes. - Make positive assertions on expected UI/text instead of looping over regexes to assert absence.
- Keep tests simple: no control-flow loops or extra variables for straightforward assertions.
- For localized route work, cover page chrome as well as body content: navbar/mobile nav, search
labels, subnav, post metadata/bylines, dynamic cards,
html lang, canonical, and hreflang. - Navigate with relative URLs (
page.goto('/path')) by settingbaseURLinplaywright.config.ts; avoid stringing environment URLs in tests. - Stub or mock external/third‑party requests (Intercom, Sentry, etc.) and any auth/login endpoints to keep tests deterministic; return minimal valid JSON when the app expects data.
- Each unexpected error should surface and fail the test.
For Typescript:
- Favor
contentGapItemSchema = z.object()overContentGapItemSchema = z.object() - Favor
from './PosterboyCommand.ts'overfrom './PosterboyCommand' - Favor
return ideas.filter(isPresent)overideas.filter((idea): idea is Idea => idea !== null) - Favor using
.tsxover.jsxfile extensions. - Use Node v24's native typestripping vs
tsxorts-node. These days you do not even need to pass--experimental-strip-types,node app.tswill just work. - In ESM TypeScript, use
import.meta.dirname/import.meta.filenameorURLobjects instead of rebuilding__dirnamewithfileURLToPath(import.meta.url)unless compatibility requires it. - Use
satisfieswhen you need literal preservation or structural conformance while keeping the expression's inferred type. If the variable should simply have a declared type, use a type annotation instead. - Avoid redundant type annotations inside expressions when TypeScript already infers the exact type, especially callback parameters. Keep explicit return types and public boundary annotations.
- Avoid
as, consider it a sin. If a cast is unavoidable, keep it as narrow as possible and explain the upstream type mismatch or runtime invariant. - For DOM queries and browser APIs, prefer runtime narrowing such as
element instanceof HTMLAnchorElementover type casts. Decide explicitly whether a missing element should throw, return early, or no-op. - Browser/client code must not use Node-only globals or APIs such as
Buffer. Use browser platform APIs such asbtoa,TextEncoder,Blob, orURL, or isolate the logic in server-only code. - When browser APIs are missing TypeScript declarations, augment the global interface in
_types/global.ts(or the relevant shared global types file) and narrow with checks such astypeof navigator.setAppBadge === 'function'instead of castingnavigatorlocally. - Prefer typed DOM properties such as
element.inert = trueandelement.tabIndex = -1when the platform exposes them. UsesetAttribute()only when you intentionally need raw attribute semantics. - Favor
unknownoverany, consideranya sin - Avoid
as unknown as ...andbiome-ignore lint/suspicious/noExplicitAny. If an upstream library type forces either, isolate it in a tiny adapter/helper with a comment naming the bad upstream type. - Every
@ts-expect-errormust be narrow and include a short explanation of the upstream type gap or invariant that makes it safe. - Favor validating data with Zod over using
anyor custom type guards - Extract duplicated Zod object shapes, regexes, and descriptions into reusable schema fragments
when they describe the same domain concept. Keep property-level concerns such as
.optional()and.default()at the property site unless absence is intrinsic to the reusable fragment. - Boolean Zod properties should usually use explicit defaults instead of
.optional()when omission has normal default behavior. Only leave a boolean optional whenundefinedis semantically different fromfalse. - Prefer
z.union([...])over chained.or()for multi-branch unions, and preferz.enum()or literal unions over clever regexes when the accepted values are finite and autocomplete matters. - Do not duplicate supported values in user-facing schema descriptions when schema metadata such as enums or suggested values can carry that information.
- In TypeScript files, use TypeScript syntax instead of JSDoc type annotations. In JavaScript files,
prefer JSDoc
@import/@paramforms over noisy inlineimport('...')annotations, and make sure@typeannotates the expression it is meant to type. - Avoid hand-written
.d.tsfiles when the declaration can come from TypeScript source or generation. If a declaration file is unavoidable, do not rely onskipLibCheckto hide duplicate or invalid exports. - Avoid
Reflect.getfor normal object property reads. After narrowing to a record, userecord[key]or a small typed reader helper. Only keepReflect.getfor exotic receivers such as proxies or framework objects where its semantics are intentionally required, and document why. - Prefer
Number.isFinite()/Number.isNaN()over globalisFinite()/isNaN()so numeric checks do not silently coerce non-numbers. isRecordstyle guards must reject arrays:typeof value === 'object' && value !== null && !Array.isArray(value). Prefer importing a shared guard when one already exists in the relevant shared layer.- Type-only refactors must preserve runtime behavior. If the behavior intentionally changes, call it out in the PR and cover the changed behavior with tests.
- Use ECMAScript
#privatefields for private state. Do not rely on underscore names or TypeScriptprivateto imply runtime privacy. - Prefer Zod defaults/preprocessing for schema-backed default values instead of duplicating default objects in runtime code.
- For local TypeScript files, import with the
.ts/.tsxextension (not.js, not extensionless). Note: we do not currently enable the TS 5.7rewriteRelativeImportExtensionscompiler option, because it errors on non-relative imports that include.ts/.tsx(for example viapathsaliases like@/…). If/when we enable it, we will need to adjust those imports first. - Favor defining props as an interface over inline
- Favor explicit return types over inferring them as it makes typescript a lot faster in the editor on our scale