@agjs/tsforge 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -109,559 +109,579 @@
109
109
  "bad": "[1, 2, 3].reduce((arr, num) => arr.concat(num * 2), [] as number[]);\n\n['a', 'b'].reduce(\n (accumulator, name) => ({\n ...accumulator,\n [name]: true,\n }),\n {} as Record<string, boolean>,",
110
110
  "good": "[1, 2, 3].reduce<number[]>((arr, num) => arr.concat(num * 2), []);\n\n['a', 'b'].reduce<Record<string, boolean>>(\n (accumulator, name) => ({\n ...accumulator,\n [name]: true,\n }),\n {},"
111
111
  },
112
+ "tsforge/no-api-key-in-client": {
113
+ "what": "Disallow constructing an AI provider client in a client component — it leaks the API key into the browser bundle. Call the model from a server route/action.",
114
+ "bad": "",
115
+ "good": ""
116
+ },
117
+ "tsforge/require-completion-token-limit": {
118
+ "what": "Require a token limit (maxTokens / max_tokens) on AI completion calls to bound runaway cost and latency.",
119
+ "bad": "",
120
+ "good": ""
121
+ },
122
+ "tsforge/no-user-input-in-system-prompt": {
123
+ "what": "Warn when a system prompt is built by string interpolation/concatenation — splicing request data into the system role enables prompt injection. Keep the system prompt constant; pass user input as a user message.",
124
+ "bad": "",
125
+ "good": ""
126
+ },
112
127
  "tsforge/id-param-requires-object-authz": {
113
128
  "what": "Warn when a handler reads `params.id` and queries the database without an authorization check in the same function.",
114
- "bad": "// Example that violates the rule",
115
- "good": "// Corrected version"
129
+ "bad": "",
130
+ "good": ""
116
131
  },
117
132
  "tsforge/mutating-route-requires-authz": {
118
133
  "what": "POST/PUT/PATCH/DELETE route handlers must call an authorization helper before mutating state.",
119
- "bad": "// Example that violates the rule",
120
- "good": "// Corrected version"
134
+ "bad": "",
135
+ "good": ""
121
136
  },
122
137
  "tsforge/server-action-requires-authz": {
123
138
  "what": "Files with `\"use server\"` that perform database mutations must call an authorization helper in the same function.",
124
- "bad": "// Example that violates the rule",
125
- "good": "// Corrected version"
139
+ "bad": "",
140
+ "good": ""
126
141
  },
127
142
  "tsforge/job-name-must-be-constant": {
128
143
  "what": "Disallow string-literal job names in `<queue>.add(name, ...)` calls — use a constant identifier so all consumers share one source of truth.",
129
- "bad": "// Example that violates the rule",
130
- "good": "// Corrected version"
144
+ "bad": "",
145
+ "good": ""
131
146
  },
132
147
  "tsforge/job-options-must-set-attempts": {
133
148
  "what": "Every `<queue>.add(...)` must configure `attempts` (per-call or via `defaultJobOptions`); when `attempts > 1`, also require `backoff`.",
134
- "bad": "// Example that violates the rule",
135
- "good": "// Corrected version"
149
+ "bad": "",
150
+ "good": ""
136
151
  },
137
152
  "tsforge/no-blocking-concurrency-zero": {
138
153
  "what": "Disallow `new Worker(name, processor, { concurrency: <numericLiteral ≤ 0> })` — non-positive concurrency blocks job processing.",
139
- "bad": "// Example that violates the rule",
140
- "good": "// Corrected version"
154
+ "bad": "",
155
+ "good": ""
141
156
  },
142
157
  "tsforge/queue-options-must-set-removeoncomplete": {
143
158
  "what": "Every `<queue>.add(...)` must configure `removeOnComplete` (per-call or via `defaultJobOptions`) so completed jobs don't accumulate in Redis.",
144
- "bad": "// Example that violates the rule",
145
- "good": "// Corrected version"
159
+ "bad": "",
160
+ "good": ""
146
161
  },
147
162
  "tsforge/queue-options-must-set-removeonfail": {
148
163
  "what": "Every `<queue>.add(...)` must configure `removeOnFail` (per-call or via `defaultJobOptions`) so failed jobs don't accumulate in Redis.",
149
- "bad": "// Example that violates the rule",
150
- "good": "// Corrected version"
164
+ "bad": "",
165
+ "good": ""
151
166
  },
152
167
  "tsforge/worker-must-implement-close": {
153
168
  "what": "Classes that own a `new Worker(...)` instance must declare a close-equivalent method for graceful shutdown.",
154
- "bad": "// Example that violates the rule",
155
- "good": "// Corrected version"
169
+ "bad": "",
170
+ "good": ""
156
171
  },
157
172
  "tsforge/worker-must-listen-failed": {
158
173
  "what": "Every `new Worker(...)` must register listeners for required events (default `failed`) — BullMQ failures are silent unless explicitly subscribed.",
159
- "bad": "// Example that violates the rule",
160
- "good": "// Corrected version"
174
+ "bad": "",
175
+ "good": ""
161
176
  },
162
177
  "tsforge/no-bare-date-now": {
163
178
  "what": "Disallow direct calls to non-deterministic time/random sources (`Date.now()`, `new Date()`, `Date()`, `Math.random()`) outside an allowlisted set of utility paths. Determinism is required for snapshot tests, workflow replays, and time-travel debugging — every consumer should route through a typed util that can be faked in tests.",
164
- "bad": "// Example that violates the rule",
165
- "good": "// Corrected version"
179
+ "bad": "",
180
+ "good": ""
166
181
  },
167
182
  "tsforge/no-template-trim-empty-ternary": {
168
183
  "what": "Disallow inline `<template>.trim() === '' ? fallback : <template>.trim()` patterns. Extract to a named utility.",
169
- "bad": "// Example that violates the rule",
170
- "good": "// Corrected version"
184
+ "bad": "",
185
+ "good": ""
171
186
  },
172
187
  "tsforge/no-throw-literal": {
173
188
  "what": "Disallow throwing primitive literals (strings, numbers) — throw Error instances so error handlers can propagate status and stack traces correctly.",
174
- "bad": "// Example that violates the rule",
175
- "good": "// Corrected version"
189
+ "bad": "",
190
+ "good": ""
176
191
  },
177
192
  "tsforge/prefer-early-return": {
178
193
  "what": "Prefer guard clauses (early return) over wrapping the function body in a multi-statement `if` without an `else`.",
179
- "bad": "// Example that violates the rule",
180
- "good": "// Corrected version"
194
+ "bad": "",
195
+ "good": ""
181
196
  },
182
197
  "tsforge/no-historical-comments": {
183
198
  "what": "Disallow comments that frame code relative to what it used to do or to a past incident ('Codex flagged X', 'before the fix', 'after the refactor', 'we used to', 'no longer'). Source comments must describe the current invariant; history belongs in the commit message or PR description, where it doesn't rot when the code changes again.",
184
- "bad": "// Example that violates the rule",
185
- "good": "// Corrected version"
199
+ "bad": "",
200
+ "good": ""
186
201
  },
187
202
  "tsforge/no-narration-comments": {
188
203
  "what": "Disallow narrative comments like 'Here we...', 'Now we...', 'First, we...'. These read as step-by-step prose and add no information a future reader can't get from the code itself. Often a tell that the comment was generated by an agent describing its own changes.",
189
- "bad": "// Example that violates the rule",
190
- "good": "// Corrected version"
204
+ "bad": "",
205
+ "good": ""
191
206
  },
192
207
  "tsforge/no-pr-reference-comments": {
193
208
  "what": "Disallow PR/issue references in comments. They belong in commit messages and PR descriptions — leaving them in source rots when the repo moves, the issue tracker migrates, or the numbering changes.",
194
- "bad": "// Example that violates the rule",
195
- "good": "// Corrected version"
209
+ "bad": "",
210
+ "good": ""
196
211
  },
197
212
  "tsforge/account-scoped-tables-require-where": {
198
213
  "what": "Require every Drizzle query against a configured account-scoped table to filter by a scope column (accountId by default).",
199
- "bad": "// Example that violates the rule",
200
- "good": "// Corrected version"
214
+ "bad": "",
215
+ "good": ""
201
216
  },
202
217
  "tsforge/no-nested-db-transaction": {
203
218
  "what": "Forbid invoking the outer db's `.transaction(...)` method inside a transaction callback — use the callback's `tx` parameter instead to avoid deadlocks.",
204
- "bad": "// Example that violates the rule",
205
- "good": "// Corrected version"
219
+ "bad": "",
220
+ "good": ""
206
221
  },
207
222
  "tsforge/no-raw-sql-outside-allowlist": {
208
223
  "what": "Disallow drizzle-orm `sql` tagged template literals outside an allowlist of files (migrations, raw queries).",
209
- "bad": "// Example that violates the rule",
210
- "good": "// Corrected version"
224
+ "bad": "",
225
+ "good": ""
211
226
  },
212
227
  "tsforge/relations-must-cover-fks": {
213
228
  "what": "Every Drizzle table that declares a foreignKey(...) must be covered by a relations(...) call. Searches sibling `relations.ts` files by default.",
214
- "bad": "// Example that violates the rule",
215
- "good": "// Corrected version"
229
+ "bad": "",
230
+ "good": ""
216
231
  },
217
232
  "tsforge/schema-files-must-not-import-driver": {
218
233
  "what": "Disallow imports from database driver packages inside schema files. Schema files must remain driver-agnostic.",
219
- "bad": "// Example that violates the rule",
220
- "good": "// Corrected version"
234
+ "bad": "",
235
+ "good": ""
221
236
  },
222
237
  "tsforge/schema-files-must-only-export-schema": {
223
238
  "what": "Restrict schema files to exporting only Drizzle schema artifacts (tables, schemas, relations, indices) and types.",
224
- "bad": "// Example that violates the rule",
225
- "good": "// Corrected version"
239
+ "bad": "",
240
+ "good": ""
226
241
  },
227
242
  "tsforge/tables-must-have-timestamps": {
228
243
  "what": "Require Drizzle tables to declare standard timestamp columns (createdAt by default).",
229
- "bad": "// Example that violates the rule",
230
- "good": "// Corrected version"
244
+ "bad": "",
245
+ "good": ""
231
246
  },
232
247
  "tsforge/timestamp-must-specify-mode": {
233
248
  "what": "Require every Drizzle timestamp(...) call to explicitly set `mode: 'date'` or `mode: 'string'`.",
234
- "bad": "// Example that violates the rule",
235
- "good": "// Corrected version"
249
+ "bad": "",
250
+ "good": ""
236
251
  },
237
252
  "tsforge/update-delete-account-scoped-must-filter-scope": {
238
253
  "what": "Require Drizzle `.update()` / `.delete()` against account-scoped tables to filter by a scope column in `.where()`.",
239
- "bad": "// Example that violates the rule",
240
- "good": "// Corrected version"
254
+ "bad": "",
255
+ "good": ""
241
256
  },
242
257
  "tsforge/update-delete-must-have-where": {
243
258
  "what": "Require every Drizzle `.update()` and `.delete()` call to include a `.where()` clause — unscoped writes affect every row.",
244
- "bad": "// Example that violates the rule",
245
- "good": "// Corrected version"
259
+ "bad": "",
260
+ "good": ""
246
261
  },
247
262
  "tsforge/consistent-status-via-set": {
248
263
  "what": "Inside Elysia route handlers, set HTTP status via `set.status = N`, not by returning a `new Response(body, { status: N })`.",
249
- "bad": "// Example that violates the rule",
250
- "good": "// Corrected version"
264
+ "bad": "",
265
+ "good": ""
251
266
  },
252
267
  "tsforge/no-decorate-state-collision": {
253
268
  "what": "Disallow duplicate keys across `.decorate()` / `.state()` / `.derive()` / `.resolve()` calls on a single Elysia instance — duplicates silently overwrite and break plugin composition.",
254
- "bad": "// Example that violates the rule",
255
- "good": "// Corrected version"
269
+ "bad": "",
270
+ "good": ""
256
271
  },
257
272
  "tsforge/no-separate-model-interfaces": {
258
273
  "what": "Disallow TypeScript interfaces that duplicate the shape of a runtime schema with a matching name. Use `typeof Schema.static` (or your project's equivalent) instead.",
259
- "bad": "// Example that violates the rule",
260
- "good": "// Corrected version"
274
+ "bad": "",
275
+ "good": ""
261
276
  },
262
277
  "tsforge/prefer-destructured-context": {
263
278
  "what": "Prefer destructured context (`{ body, set, ... }`) over passing the entire dynamic Elysia context object into controllers/services.",
264
- "bad": "// Example that violates the rule",
265
- "good": "// Corrected version"
279
+ "bad": "",
280
+ "good": ""
266
281
  },
267
282
  "tsforge/prefer-direct-return": {
268
283
  "what": "Inside Elysia route handlers, return values directly instead of wrapping them in `new Response(...)` or `Response.json(...)` — Elysia handles serialization and content-type automatically.",
269
- "bad": "// Example that violates the rule",
270
- "good": "// Corrected version"
284
+ "bad": "",
285
+ "good": ""
271
286
  },
272
287
  "tsforge/prefer-static-services": {
273
288
  "what": "Discourage `new Service()` inside Elysia route handlers when the class is stateless — prefer static methods or a singleton.",
274
- "bad": "// Example that violates the rule",
275
- "good": "// Corrected version"
289
+ "bad": "",
290
+ "good": ""
276
291
  },
277
292
  "tsforge/prefer-throw-status": {
278
293
  "what": "Inside Elysia route handlers, prefer `throw status(...)` over try/catch blocks that build their own Response — local catches bypass Elysia's typed onError pipeline.",
279
- "bad": "// Example that violates the rule",
280
- "good": "// Corrected version"
294
+ "bad": "",
295
+ "good": ""
281
296
  },
282
297
  "tsforge/require-hooks-before-routes": {
283
298
  "what": "Elysia hooks (onError, onBeforeHandle, etc.) must register before any route methods on the same instance — top-down waterfall semantics mean a hook registered after a route does not apply to it.",
284
- "bad": "// Example that violates the rule",
285
- "good": "// Corrected version"
299
+ "bad": "",
300
+ "good": ""
286
301
  },
287
302
  "tsforge/require-plugin-name": {
288
303
  "what": "fastify-plugin (fp) wrappers must include a `name` option so Fastify can deduplicate plugin registration.",
289
- "bad": "// Example that violates the rule",
290
- "good": "// Corrected version"
304
+ "bad": "",
305
+ "good": ""
291
306
  },
292
307
  "tsforge/error-handler-must-set-status": {
293
308
  "what": "Custom Fastify setErrorHandler callbacks must call reply.code() or reply.status() — automatic status mapping is disabled when a custom handler is registered.",
294
- "bad": "// Example that violates the rule",
295
- "good": "// Corrected version"
309
+ "bad": "",
310
+ "good": ""
296
311
  },
297
312
  "tsforge/prefer-return-over-reply-send": {
298
313
  "what": "Inside Fastify route handlers, prefer `return data` over `return reply.send(data)` so fast-json-stringify can serialize responses.",
299
- "bad": "// Example that violates the rule",
300
- "good": "// Corrected version"
314
+ "bad": "",
315
+ "good": ""
301
316
  },
302
317
  "tsforge/require-fp-for-shared-plugins": {
303
318
  "what": "Fastify plugins that call fastify.decorate, fastify.addHook, or fastify.register must be wrapped in fastify-plugin (fp) to break encapsulation and share state.",
304
- "bad": "// Example that violates the rule",
305
- "good": "// Corrected version"
319
+ "bad": "",
320
+ "good": ""
306
321
  },
307
322
  "tsforge/require-response-schema": {
308
323
  "what": "Fastify routes should declare schema.response for compiled fast-json-stringify serialization.",
309
- "bad": "// Example that violates the rule",
310
- "good": "// Corrected version"
324
+ "bad": "",
325
+ "good": ""
311
326
  },
312
327
  "tsforge/require-route-schema": {
313
328
  "what": "Fastify POST/PUT/PATCH routes must declare schema.body; GET/DELETE routes must declare schema.querystring or schema.params.",
314
- "bad": "// Example that violates the rule",
315
- "good": "// Corrected version"
329
+ "bad": "",
330
+ "good": ""
316
331
  },
317
332
  "tsforge/test-inject-must-close-app": {
318
333
  "what": "Test files using fastify.inject must register teardown that calls app.close() to drain connections.",
319
- "bad": "// Example that violates the rule",
320
- "good": "// Corrected version"
334
+ "bad": "",
335
+ "good": ""
321
336
  },
322
337
  "tsforge/no-direct-process-env": {
323
338
  "what": "Disallow direct `process.env` access — force every consumer through a typed, boot-validated singleton.",
324
- "bad": "// Example that violates the rule",
325
- "good": "// Corrected version"
339
+ "bad": "",
340
+ "good": ""
326
341
  },
327
342
  "tsforge/no-process-exit": {
328
343
  "what": "Disallow `process.exit()` outside the centralized shutdown and CLI entrypoints — forces graceful teardown through the error-handlers module.",
329
- "bad": "// Example that violates the rule",
330
- "good": "// Corrected version"
344
+ "bad": "",
345
+ "good": ""
331
346
  },
332
347
  "tsforge/static-translation-key-exists": {
333
348
  "what": "Static string passed to `t(\"...\")` or `i18n.t(\"...\")` must exist as a leaf path in the canonical locale JSON.",
334
- "bad": "// Example that violates the rule",
335
- "good": "// Corrected version"
349
+ "bad": "",
350
+ "good": ""
336
351
  },
337
352
  "tsforge/auth-cookie-must-be-httponly": {
338
353
  "what": "Auth-cookie writes must set `httpOnly: true` (or spread a trusted cookie-config helper). JS-readable session cookies leak via XSS.",
339
- "bad": "// Example that violates the rule",
340
- "good": "// Corrected version"
354
+ "bad": "",
355
+ "good": ""
341
356
  },
342
357
  "tsforge/auth-cookie-must-be-secure-in-prod": {
343
358
  "what": "Auth-cookie writes must set `secure:` to `true` or an env-derived expression (anything non-literal). Cookies leak over HTTP without it.",
344
- "bad": "// Example that violates the rule",
345
- "good": "// Corrected version"
359
+ "bad": "",
360
+ "good": ""
346
361
  },
347
362
  "tsforge/auth-cookie-must-set-maxage-or-expires": {
348
363
  "what": "Auth-cookie writes should set `maxAge` or `expires` so session cookies do not live forever by default.",
349
- "bad": "// Example that violates the rule",
350
- "good": "// Corrected version"
364
+ "bad": "",
365
+ "good": ""
351
366
  },
352
367
  "tsforge/auth-cookie-must-set-samesite": {
353
368
  "what": "Auth-cookie writes must set `sameSite` (`strict` or `lax`) — missing SameSite allows cross-site cookie delivery.",
354
- "bad": "// Example that violates the rule",
355
- "good": "// Corrected version"
369
+ "bad": "",
370
+ "good": ""
356
371
  },
357
372
  "tsforge/bcrypt-rounds-min": {
358
373
  "what": "Disallow `bcrypt.hash` / `bcrypt.hashSync` calls with a numeric-literal rounds value below the configured minimum (default 10).",
359
- "bad": "// Example that violates the rule",
360
- "good": "// Corrected version"
374
+ "bad": "",
375
+ "good": ""
361
376
  },
362
377
  "tsforge/jwt-must-verify-not-decode": {
363
378
  "what": "Disallow `jwt.decode` / `decodeJwt` — decoding without verification accepts forged tokens. Use `jwt.verify` or `jwtVerify` instead.",
364
- "bad": "// Example that violates the rule",
365
- "good": "// Corrected version"
379
+ "bad": "",
380
+ "good": ""
366
381
  },
367
382
  "tsforge/no-import-build-output": {
368
383
  "what": "Disallow importing from build/output directories within the project. Source must import source, not compiled artifacts, to avoid stale-code drift and broken module boundaries.",
369
- "bad": "// Example that violates the rule",
370
- "good": "// Corrected version"
384
+ "bad": "",
385
+ "good": ""
371
386
  },
372
387
  "tsforge/no-import-test-from-source": {
373
388
  "what": "Disallow production/source files from importing test files. Tests may depend on source, never the reverse — test code must not ship in the production graph.",
374
- "bad": "// Example that violates the rule",
375
- "good": "// Corrected version"
389
+ "bad": "",
390
+ "good": ""
376
391
  },
377
392
  "tsforge/no-react-in-services": {
378
393
  "what": "Service and data-fetch modules must not import React — keep business logic decoupled from the view layer.",
379
- "bad": "// Example that violates the rule",
380
- "good": "// Corrected version"
394
+ "bad": "",
395
+ "good": ""
381
396
  },
382
397
  "tsforge/await-dynamic-request-apis": {
383
398
  "what": "Require awaiting Next.js dynamic request APIs (cookies, headers, draftMode) in app-router Server Components.",
384
- "bad": "// Example that violates the rule",
385
- "good": "// Corrected version"
399
+ "bad": "",
400
+ "good": ""
386
401
  },
387
402
  "tsforge/client-hooks-require-use-client": {
388
403
  "what": "Require the 'use client' directive in app-router page/layout/template files that call client-only hooks. Server Components cannot use state/effect/navigation hooks — doing so crashes at runtime.",
389
- "bad": "// Example that violates the rule",
390
- "good": "// Corrected version"
404
+ "bad": "",
405
+ "good": ""
391
406
  },
392
407
  "tsforge/error-boundary-require-use-client": {
393
408
  "what": "Require 'use client' in app-router error.tsx and global-error.tsx — Next.js error boundaries must be Client Components.",
394
- "bad": "// Example that violates the rule",
395
- "good": "// Corrected version"
409
+ "bad": "",
410
+ "good": ""
396
411
  },
397
412
  "tsforge/mutation-should-revalidate-cache": {
398
413
  "what": "After database mutations in server actions or route handlers, call `revalidatePath` or `revalidateTag` so cached pages reflect the change.",
399
- "bad": "// Example that violates the rule",
400
- "good": "// Corrected version"
414
+ "bad": "",
415
+ "good": ""
401
416
  },
402
417
  "tsforge/no-html-img-element": {
403
418
  "what": "Prefer next/image over raw <img> elements for optimized responsive images and Core Web Vitals.",
404
- "bad": "// Example that violates the rule",
405
- "good": "// Corrected version"
419
+ "bad": "",
420
+ "good": ""
406
421
  },
407
422
  "tsforge/no-internal-api-fetch": {
408
423
  "what": "Disallow Server Components from fetching the app's own /api routes — import services or ORM modules directly to avoid loopback HTTP overhead.",
409
- "bad": "// Example that violates the rule",
410
- "good": "// Corrected version"
424
+ "bad": "",
425
+ "good": ""
411
426
  },
412
427
  "tsforge/no-next-head-in-app": {
413
428
  "what": "Disallow importing 'next/head' in app-router files. The <Head> component is a no-op under app/ — use the Metadata API (export const metadata / generateMetadata) instead.",
414
- "bad": "// Example that violates the rule",
415
- "good": "// Corrected version"
429
+ "bad": "",
430
+ "good": ""
416
431
  },
417
432
  "tsforge/no-pages-router-data-fetching-in-app": {
418
433
  "what": "Disallow pages-router data-fetching exports (getServerSideProps, getStaticProps, getStaticPaths, getInitialProps) in app-router files. Next.js ignores them under app/, so they are silent dead code — use async Server Components or route handlers instead.",
419
- "bad": "// Example that violates the rule",
420
- "good": "// Corrected version"
434
+ "bad": "",
435
+ "good": ""
421
436
  },
422
437
  "tsforge/no-secret-props-to-client": {
423
438
  "what": "Warn when Server Components pass secret-looking props to JSX — values may cross the client boundary.",
424
- "bad": "// Example that violates the rule",
425
- "good": "// Corrected version"
439
+ "bad": "",
440
+ "good": ""
426
441
  },
427
442
  "tsforge/no-sensitive-next-public-env": {
428
443
  "what": "Disallow NEXT_PUBLIC_* env vars whose names suggest secrets — public build-time vars are visible in the client bundle.",
429
- "bad": "// Example that violates the rule",
430
- "good": "// Corrected version"
444
+ "bad": "",
445
+ "good": ""
431
446
  },
432
447
  "tsforge/prefer-lazy-use-state-init": {
433
448
  "what": "Prefer lazy useState initializers when parsing localStorage/sessionStorage — avoids re-parsing on every render.",
434
- "bad": "// Example that violates the rule",
435
- "good": "// Corrected version"
449
+ "bad": "",
450
+ "good": ""
436
451
  },
437
452
  "tsforge/server-action-requires-authz-and-validation": {
438
453
  "what": "Server actions (`\"use server\"`) that mutate the database must call authorization helpers and validate input with `.parse()` / `.safeParse()`.",
439
- "bad": "// Example that violates the rule",
440
- "good": "// Corrected version"
454
+ "bad": "",
455
+ "good": ""
441
456
  },
442
457
  "tsforge/server-only-modules-import-server-only": {
443
458
  "what": "App-router server modules must import `\"server-only\"` so accidental client bundling fails at build time.",
444
- "bad": "// Example that violates the rule",
445
- "good": "// Corrected version"
459
+ "bad": "",
460
+ "good": ""
446
461
  },
447
462
  "tsforge/pkce-required-for-oidc": {
448
463
  "what": "OIDC providers must use PKCE: `buildAuthorizationURL` must call `generateCodeVerifier()` and pass it to `createAuthorizationURL`.",
449
- "bad": "// Example that violates the rule",
450
- "good": "// Corrected version"
464
+ "bad": "",
465
+ "good": ""
451
466
  },
452
467
  "tsforge/state-must-be-redis-backed": {
453
468
  "what": "OAuth state must be persisted to Redis and not stuffed into a cookie. Cookie-backed state lets attackers replay forged state across sessions.",
454
- "bad": "// Example that violates the rule",
455
- "good": "// Corrected version"
469
+ "bad": "",
470
+ "good": ""
456
471
  },
457
472
  "tsforge/state-ttl-bounded": {
458
473
  "what": "OAuth state writes to Redis must use a short TTL — long-lived state widens the replay window.",
459
- "bad": "// Example that violates the rule",
460
- "good": "// Corrected version"
474
+ "bad": "",
475
+ "good": ""
461
476
  },
462
477
  "tsforge/component-file-purity": {
463
478
  "what": "A component .tsx contains only imports and the component itself — types go to <feature>.types.ts, constants to <feature>.constants.ts, helpers to src/lib",
464
- "bad": "// Example that violates the rule",
465
- "good": "// Corrected version"
479
+ "bad": "",
480
+ "good": ""
466
481
  },
467
482
  "tsforge/component-folder-structure": {
468
483
  "what": "A component .tsx must live in src/views/<Feature>/components/ (feature component), src/components/ui/ (shared primitive), or be the view root src/views/<Feature>/index.tsx",
469
- "bad": "// Example that violates the rule",
470
- "good": "// Corrected version"
484
+ "bad": "",
485
+ "good": ""
471
486
  },
472
487
  "tsforge/dangerous-html-requires-sanitize": {
473
488
  "what": "dangerouslySetInnerHTML requires a sanitization library (DOMPurify or equivalent) imported in the same file.",
474
- "bad": "// Example that violates the rule",
475
- "good": "// Corrected version"
489
+ "bad": "",
490
+ "good": ""
476
491
  },
477
492
  "tsforge/forwardref-display-name": {
478
493
  "what": "forwardRef components must have displayName set",
479
- "bad": "// Example that violates the rule",
480
- "good": "// Corrected version"
494
+ "bad": "",
495
+ "good": ""
481
496
  },
482
497
  "tsforge/index-must-reexport-default": {
483
498
  "what": "index.ts in component folders must re-export the component default export and types",
484
- "bad": "// Example that violates the rule",
485
- "good": "// Corrected version"
499
+ "bad": "",
500
+ "good": ""
486
501
  },
487
502
  "tsforge/max-hooks-per-file": {
488
503
  "what": "Flag query/hook modules that export more than N hooks. Same-kind modules pass the single-semantic-module rule but still grow into god files; this rule sets a hard ceiling so the split conversation happens early.",
489
- "bad": "// Example that violates the rule",
490
- "good": "// Corrected version"
504
+ "bad": "",
505
+ "good": ""
491
506
  },
492
507
  "tsforge/no-anonymous-useEffect": {
493
508
  "what": "Disallow anonymous arrow functions passed to useEffect — use a named function for debuggable stack traces.",
494
- "bad": "// Example that violates the rule",
495
- "good": "// Corrected version"
509
+ "bad": "",
510
+ "good": ""
496
511
  },
497
512
  "tsforge/no-component-invocation": {
498
513
  "what": "Disallow invoking React components as plain functions — use JSX (`<Header />`) instead of `{Header()}`.",
499
- "bad": "// Example that violates the rule",
500
- "good": "// Corrected version"
514
+ "bad": "",
515
+ "good": ""
501
516
  },
502
517
  "tsforge/no-cross-feature-imports": {
503
518
  "what": "Prevent imports across different features",
504
- "bad": "// Example that violates the rule",
505
- "good": "// Corrected version"
519
+ "bad": "",
520
+ "good": ""
506
521
  },
507
522
  "tsforge/no-derived-state-in-effect": {
508
523
  "what": "Disallow setting local state inside useEffect when the value can be derived during render (or memoized with useMemo).",
509
- "bad": "// Example that violates the rule",
510
- "good": "// Corrected version"
524
+ "bad": "",
525
+ "good": ""
511
526
  },
512
527
  "tsforge/no-inline-jsx-functions": {
513
528
  "what": "Disallow inline function expressions in JSX attributes",
514
- "bad": "// Example that violates the rule",
515
- "good": "// Corrected version"
529
+ "bad": "",
530
+ "good": ""
516
531
  },
517
532
  "tsforge/no-jsx-computation": {
518
533
  "what": "Move complex computations out of JSX into hooks or helper functions",
519
- "bad": "// Example that violates the rule",
520
- "good": "// Corrected version"
534
+ "bad": "",
535
+ "good": ""
536
+ },
537
+ "tsforge/no-loading-text-use-skeleton": {
538
+ "what": "Loading states must render a <Skeleton/>, not loading text or a spinner",
539
+ "bad": "",
540
+ "good": ""
521
541
  },
522
542
  "tsforge/no-nested-component": {
523
543
  "what": "Disallow declaring React components inside another component body — nested components reset state on every parent render.",
524
- "bad": "// Example that violates the rule",
525
- "good": "// Corrected version"
544
+ "bad": "",
545
+ "good": ""
526
546
  },
527
547
  "tsforge/no-react-fc": {
528
548
  "what": "Disallow React.FC / FunctionComponent — type props explicitly on the function parameter instead.",
529
- "bad": "// Example that violates the rule",
530
- "good": "// Corrected version"
549
+ "bad": "",
550
+ "good": ""
531
551
  },
532
552
  "tsforge/no-state-in-component-body": {
533
553
  "what": "State hooks must be in .hooks.ts files, not directly in components",
534
- "bad": "// Example that violates the rule",
535
- "good": "// Corrected version"
554
+ "bad": "",
555
+ "good": ""
536
556
  },
537
557
  "tsforge/no-prototype-polluting-merge": {
538
558
  "what": "Disallow merging request body/query/params into objects — enables prototype pollution.",
539
- "bad": "// Example that violates the rule",
540
- "good": "// Corrected version"
559
+ "bad": "",
560
+ "good": ""
541
561
  },
542
562
  "tsforge/no-user-controlled-fetch-url": {
543
563
  "what": "Disallow fetch/axios requests to non-literal URLs — dynamic URLs enable SSRF.",
544
- "bad": "// Example that violates the rule",
545
- "good": "// Corrected version"
564
+ "bad": "",
565
+ "good": ""
546
566
  },
547
567
  "tsforge/no-user-controlled-redirect": {
548
568
  "what": "Disallow redirects to non-literal URLs — user-controlled redirects enable open redirects.",
549
- "bad": "// Example that violates the rule",
550
- "good": "// Corrected version"
569
+ "bad": "",
570
+ "good": ""
551
571
  },
552
572
  "tsforge/upload-must-set-limits": {
553
573
  "what": "Multipart upload handlers should declare `limits` or `maxFileSize` to bound request size.",
554
- "bad": "// Example that violates the rule",
555
- "good": "// Corrected version"
574
+ "bad": "",
575
+ "good": ""
556
576
  },
557
577
  "tsforge/webhook-must-verify-signature-before-parse": {
558
578
  "what": "Webhook handlers must verify signatures before calling `.json()` on the request body.",
559
- "bad": "// Example that violates the rule",
560
- "good": "// Corrected version"
579
+ "bad": "",
580
+ "good": ""
561
581
  },
562
582
  "tsforge/catch-must-handle": {
563
583
  "what": "Catch blocks must log, rethrow, or propagate errors — not silently return empty defaults on failure.",
564
- "bad": "// Example that violates the rule",
565
- "good": "// Corrected version"
584
+ "bad": "",
585
+ "good": ""
566
586
  },
567
587
  "tsforge/no-auth-token-in-storage": {
568
588
  "what": "Disallow storing or reading auth tokens from localStorage/sessionStorage — use httpOnly cookies instead.",
569
- "bad": "// Example that violates the rule",
570
- "good": "// Corrected version"
589
+ "bad": "",
590
+ "good": ""
571
591
  },
572
592
  "tsforge/no-child-process-exec": {
573
593
  "what": "Disallow child_process.exec/execSync — they run commands in a shell. Use execFile or spawn without shell instead.",
574
- "bad": "// Example that violates the rule",
575
- "good": "// Corrected version"
594
+ "bad": "",
595
+ "good": ""
576
596
  },
577
597
  "tsforge/no-dynamic-regexp": {
578
598
  "what": "Disallow new RegExp(non-literal) — dynamic patterns enable ReDoS. Use string-literal regexes or a safe engine like re2.",
579
- "bad": "// Example that violates the rule",
580
- "good": "// Corrected version"
599
+ "bad": "",
600
+ "good": ""
581
601
  },
582
602
  "tsforge/no-inner-html-assignment": {
583
603
  "what": "Disallow assigning to innerHTML — use textContent/innerText or sanitize with DOMPurify before injecting HTML.",
584
- "bad": "// Example that violates the rule",
585
- "good": "// Corrected version"
604
+ "bad": "",
605
+ "good": ""
586
606
  },
587
607
  "tsforge/no-spawn-with-shell": {
588
608
  "what": "Disallow child_process.spawn/spawnSync with shell: true — shell execution enables command injection.",
589
- "bad": "// Example that violates the rule",
590
- "good": "// Corrected version"
609
+ "bad": "",
610
+ "good": ""
591
611
  },
592
612
  "tsforge/caught-error-log-requires-cause": {
593
613
  "what": "When logging a caught error, include a `cause` field in the structured payload so downstream tools preserve the error chain.",
594
- "bad": "// Example that violates the rule",
595
- "good": "// Corrected version"
614
+ "bad": "",
615
+ "good": ""
596
616
  },
597
617
  "tsforge/logger-not-console": {
598
618
  "what": "Service modules should use the structured logger instead of `console.*` — console output is unstructured and hard to search.",
599
- "bad": "// Example that violates the rule",
600
- "good": "// Corrected version"
619
+ "bad": "",
620
+ "good": ""
601
621
  },
602
622
  "tsforge/mask-pii-fields": {
603
623
  "what": "Disallow unmasked PII (email, phone, password, token, ...) in structured-logger payloads — the #1 way data leaks quietly.",
604
- "bad": "// Example that violates the rule",
605
- "good": "// Corrected version"
624
+ "bad": "",
625
+ "good": ""
606
626
  },
607
627
  "tsforge/no-error-stringify": {
608
628
  "what": "Disallow stringifying errors with `String(error)` / `${error}` / `error.toString()` — strips the cause chain. Use a configured extractor instead.",
609
- "bad": "// Example that violates the rule",
610
- "good": "// Corrected version"
629
+ "bad": "",
630
+ "good": ""
611
631
  },
612
632
  "tsforge/require-event-field": {
613
633
  "what": "Require structured logger calls to include an `event` field in their payload, so log searches in ELK/Datadog/Loki don't fall back to substring match.",
614
- "bad": "// Example that violates the rule",
615
- "good": "// Corrected version"
634
+ "bad": "",
635
+ "good": ""
616
636
  },
617
637
  "tsforge/prefix-query-key-must-use-set-queries-data": {
618
638
  "what": "When a hook uses `queryKey: [...prefix, extra]`, do not call `setQueryData(prefix, …)`, `cancelQueries({ queryKey: prefix })`, etc. — those only touch one cache entry. Use `setQueriesData({ queryKey: prefix }, …)` and matcher-style `cancelQueries` / `invalidateQueries` so every variant is covered.",
619
- "bad": "// Example that violates the rule",
620
- "good": "// Corrected version"
639
+ "bad": "",
640
+ "good": ""
621
641
  },
622
642
  "tsforge/fake-timers-must-be-restored": {
623
643
  "what": "When a test file calls `useFakeTimers()`, it must also call `useRealTimers()` so later tests are not affected.",
624
- "bad": "// Example that violates the rule",
625
- "good": "// Corrected version"
644
+ "bad": "",
645
+ "good": ""
626
646
  },
627
647
  "tsforge/no-conditional-expect": {
628
648
  "what": "Disallow `expect()` inside conditionals — tests must fail when assertions are skipped.",
629
- "bad": "// Example that violates the rule",
630
- "good": "// Corrected version"
649
+ "bad": "",
650
+ "good": ""
631
651
  },
632
652
  "tsforge/no-focused-tests": {
633
653
  "what": "Disallow focused tests (`test.only`, `it.only`, `fdescribe`, ...) — the canonical 'I forgot to remove this before committing' leak.",
634
- "bad": "// Example that violates the rule",
635
- "good": "// Corrected version"
654
+ "bad": "",
655
+ "good": ""
636
656
  },
637
657
  "tsforge/no-real-network-in-unit-tests": {
638
658
  "what": "Unit tests should not perform real network I/O — mock HTTP clients or move the test to an integration suite.",
639
- "bad": "// Example that violates the rule",
640
- "good": "// Corrected version"
659
+ "bad": "",
660
+ "good": ""
641
661
  },
642
662
  "tsforge/test-file-mirrors-source": {
643
663
  "what": "Every test file under `tests/` must mirror a source file under `src/`. Catches orphaned tests left behind after refactors and renames.",
644
- "bad": "// Example that violates the rule",
645
- "good": "// Corrected version"
664
+ "bad": "",
665
+ "good": ""
646
666
  },
647
667
  "tsforge/exported-functions-require-return-type": {
648
668
  "what": "Exported functions should declare an explicit return type at module boundaries.",
649
- "bad": "// Example that violates the rule",
650
- "good": "// Corrected version"
669
+ "bad": "",
670
+ "good": ""
651
671
  },
652
672
  "tsforge/fetch-must-check-ok": {
653
673
  "what": "HTTP fetch responses must check `.ok` or status before calling `.json()`.",
654
- "bad": "// Example that violates the rule",
655
- "good": "// Corrected version"
674
+ "bad": "",
675
+ "good": ""
656
676
  },
657
677
  "tsforge/json-parse-must-validate": {
658
678
  "what": "Disallow bare JSON.parse on untrusted input — validate through a schema library.",
659
- "bad": "// Example that violates the rule",
660
- "good": "// Corrected version"
679
+ "bad": "",
680
+ "good": ""
661
681
  },
662
682
  "tsforge/no-unsafe-boundary-cast": {
663
683
  "what": "Disallow type assertions immediately after parsing untrusted boundary input.",
664
- "bad": "// Example that violates the rule",
665
- "good": "// Corrected version"
684
+ "bad": "",
685
+ "good": ""
666
686
  }
667
687
  }