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.
- 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. - 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.
- 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