@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.
- package/package.json +1 -1
- package/scripts/boot-check.ts +106 -0
- package/scripts/build-rule-docs.ts +5 -2
- package/scripts/test-coverage-check.ts +138 -0
- package/src/detect-gate.ts +32 -0
- package/src/loop/feedback/meta-rule-docs.ts +6 -0
- package/src/loop/feedback/rule-docs.ts +44 -1
- package/src/loop/rule-docs.generated.json +242 -222
- package/src/meta-rules/registry.ts +6 -0
- package/src/meta-rules/rules/structure/no-circular-imports.ts +195 -0
- package/src/meta-rules/rules/supply-chain/no-undeclared-dependencies.ts +180 -0
- package/strict.type-aware.eslint.config.mjs +36 -3
|
@@ -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": "
|
|
115
|
-
"good": "
|
|
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": "
|
|
120
|
-
"good": "
|
|
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": "
|
|
125
|
-
"good": "
|
|
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": "
|
|
130
|
-
"good": "
|
|
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": "
|
|
135
|
-
"good": "
|
|
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": "
|
|
140
|
-
"good": "
|
|
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": "
|
|
145
|
-
"good": "
|
|
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": "
|
|
150
|
-
"good": "
|
|
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": "
|
|
155
|
-
"good": "
|
|
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": "
|
|
160
|
-
"good": "
|
|
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": "
|
|
165
|
-
"good": "
|
|
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": "
|
|
170
|
-
"good": "
|
|
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": "
|
|
175
|
-
"good": "
|
|
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": "
|
|
180
|
-
"good": "
|
|
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": "
|
|
185
|
-
"good": "
|
|
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": "
|
|
190
|
-
"good": "
|
|
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": "
|
|
195
|
-
"good": "
|
|
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": "
|
|
200
|
-
"good": "
|
|
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": "
|
|
205
|
-
"good": "
|
|
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": "
|
|
210
|
-
"good": "
|
|
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": "
|
|
215
|
-
"good": "
|
|
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": "
|
|
220
|
-
"good": "
|
|
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": "
|
|
225
|
-
"good": "
|
|
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": "
|
|
230
|
-
"good": "
|
|
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": "
|
|
235
|
-
"good": "
|
|
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": "
|
|
240
|
-
"good": "
|
|
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": "
|
|
245
|
-
"good": "
|
|
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": "
|
|
250
|
-
"good": "
|
|
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": "
|
|
255
|
-
"good": "
|
|
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": "
|
|
260
|
-
"good": "
|
|
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": "
|
|
265
|
-
"good": "
|
|
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": "
|
|
270
|
-
"good": "
|
|
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": "
|
|
275
|
-
"good": "
|
|
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": "
|
|
280
|
-
"good": "
|
|
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": "
|
|
285
|
-
"good": "
|
|
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": "
|
|
290
|
-
"good": "
|
|
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": "
|
|
295
|
-
"good": "
|
|
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": "
|
|
300
|
-
"good": "
|
|
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": "
|
|
305
|
-
"good": "
|
|
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": "
|
|
310
|
-
"good": "
|
|
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": "
|
|
315
|
-
"good": "
|
|
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": "
|
|
320
|
-
"good": "
|
|
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": "
|
|
325
|
-
"good": "
|
|
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": "
|
|
330
|
-
"good": "
|
|
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": "
|
|
335
|
-
"good": "
|
|
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": "
|
|
340
|
-
"good": "
|
|
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": "
|
|
345
|
-
"good": "
|
|
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": "
|
|
350
|
-
"good": "
|
|
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": "
|
|
355
|
-
"good": "
|
|
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": "
|
|
360
|
-
"good": "
|
|
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": "
|
|
365
|
-
"good": "
|
|
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": "
|
|
370
|
-
"good": "
|
|
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": "
|
|
375
|
-
"good": "
|
|
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": "
|
|
380
|
-
"good": "
|
|
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": "
|
|
385
|
-
"good": "
|
|
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": "
|
|
390
|
-
"good": "
|
|
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": "
|
|
395
|
-
"good": "
|
|
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": "
|
|
400
|
-
"good": "
|
|
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": "
|
|
405
|
-
"good": "
|
|
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": "
|
|
410
|
-
"good": "
|
|
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": "
|
|
415
|
-
"good": "
|
|
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": "
|
|
420
|
-
"good": "
|
|
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": "
|
|
425
|
-
"good": "
|
|
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": "
|
|
430
|
-
"good": "
|
|
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": "
|
|
435
|
-
"good": "
|
|
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": "
|
|
440
|
-
"good": "
|
|
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": "
|
|
445
|
-
"good": "
|
|
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": "
|
|
450
|
-
"good": "
|
|
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": "
|
|
455
|
-
"good": "
|
|
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": "
|
|
460
|
-
"good": "
|
|
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": "
|
|
465
|
-
"good": "
|
|
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": "
|
|
470
|
-
"good": "
|
|
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": "
|
|
475
|
-
"good": "
|
|
489
|
+
"bad": "",
|
|
490
|
+
"good": ""
|
|
476
491
|
},
|
|
477
492
|
"tsforge/forwardref-display-name": {
|
|
478
493
|
"what": "forwardRef components must have displayName set",
|
|
479
|
-
"bad": "
|
|
480
|
-
"good": "
|
|
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": "
|
|
485
|
-
"good": "
|
|
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": "
|
|
490
|
-
"good": "
|
|
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": "
|
|
495
|
-
"good": "
|
|
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": "
|
|
500
|
-
"good": "
|
|
514
|
+
"bad": "",
|
|
515
|
+
"good": ""
|
|
501
516
|
},
|
|
502
517
|
"tsforge/no-cross-feature-imports": {
|
|
503
518
|
"what": "Prevent imports across different features",
|
|
504
|
-
"bad": "
|
|
505
|
-
"good": "
|
|
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": "
|
|
510
|
-
"good": "
|
|
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": "
|
|
515
|
-
"good": "
|
|
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": "
|
|
520
|
-
"good": "
|
|
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": "
|
|
525
|
-
"good": "
|
|
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": "
|
|
530
|
-
"good": "
|
|
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": "
|
|
535
|
-
"good": "
|
|
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": "
|
|
540
|
-
"good": "
|
|
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": "
|
|
545
|
-
"good": "
|
|
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": "
|
|
550
|
-
"good": "
|
|
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": "
|
|
555
|
-
"good": "
|
|
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": "
|
|
560
|
-
"good": "
|
|
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": "
|
|
565
|
-
"good": "
|
|
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": "
|
|
570
|
-
"good": "
|
|
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": "
|
|
575
|
-
"good": "
|
|
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": "
|
|
580
|
-
"good": "
|
|
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": "
|
|
585
|
-
"good": "
|
|
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": "
|
|
590
|
-
"good": "
|
|
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": "
|
|
595
|
-
"good": "
|
|
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": "
|
|
600
|
-
"good": "
|
|
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": "
|
|
605
|
-
"good": "
|
|
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": "
|
|
610
|
-
"good": "
|
|
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": "
|
|
615
|
-
"good": "
|
|
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": "
|
|
620
|
-
"good": "
|
|
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": "
|
|
625
|
-
"good": "
|
|
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": "
|
|
630
|
-
"good": "
|
|
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": "
|
|
635
|
-
"good": "
|
|
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": "
|
|
640
|
-
"good": "
|
|
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": "
|
|
645
|
-
"good": "
|
|
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": "
|
|
650
|
-
"good": "
|
|
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": "
|
|
655
|
-
"good": "
|
|
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": "
|
|
660
|
-
"good": "
|
|
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": "
|
|
665
|
-
"good": "
|
|
684
|
+
"bad": "",
|
|
685
|
+
"good": ""
|
|
666
686
|
}
|
|
667
687
|
}
|