@aarock1234/slopscan 0.1.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.
Files changed (195) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +143 -0
  3. package/SKILL.md +149 -0
  4. package/dist/analyzer.d.ts +5 -0
  5. package/dist/analyzer.js +13 -0
  6. package/dist/analyzer.js.map +1 -0
  7. package/dist/analyzers/ast.d.ts +12 -0
  8. package/dist/analyzers/ast.js +73 -0
  9. package/dist/analyzers/ast.js.map +1 -0
  10. package/dist/analyzers/judge-chunks.d.ts +13 -0
  11. package/dist/analyzers/judge-chunks.js +87 -0
  12. package/dist/analyzers/judge-chunks.js.map +1 -0
  13. package/dist/analyzers/judge-validate.d.ts +19 -0
  14. package/dist/analyzers/judge-validate.js +81 -0
  15. package/dist/analyzers/judge-validate.js.map +1 -0
  16. package/dist/analyzers/judge.d.ts +44 -0
  17. package/dist/analyzers/judge.js +177 -0
  18. package/dist/analyzers/judge.js.map +1 -0
  19. package/dist/change.d.ts +19 -0
  20. package/dist/change.js +87 -0
  21. package/dist/change.js.map +1 -0
  22. package/dist/cli.d.ts +2 -0
  23. package/dist/cli.js +125 -0
  24. package/dist/cli.js.map +1 -0
  25. package/dist/config.d.ts +91 -0
  26. package/dist/config.js +61 -0
  27. package/dist/config.js.map +1 -0
  28. package/dist/confirm.d.ts +11 -0
  29. package/dist/confirm.js +100 -0
  30. package/dist/confirm.js.map +1 -0
  31. package/dist/finding.d.ts +13 -0
  32. package/dist/finding.js +18 -0
  33. package/dist/finding.js.map +1 -0
  34. package/dist/lang.d.ts +16 -0
  35. package/dist/lang.js +37 -0
  36. package/dist/lang.js.map +1 -0
  37. package/dist/model.d.ts +2 -0
  38. package/dist/model.js +34 -0
  39. package/dist/model.js.map +1 -0
  40. package/dist/report.d.ts +124 -0
  41. package/dist/report.js +112 -0
  42. package/dist/report.js.map +1 -0
  43. package/dist/rule.d.ts +66 -0
  44. package/dist/rule.js +259 -0
  45. package/dist/rule.js.map +1 -0
  46. package/dist/scan.d.ts +14 -0
  47. package/dist/scan.js +33 -0
  48. package/dist/scan.js.map +1 -0
  49. package/dist/score.d.ts +147 -0
  50. package/dist/score.js +142 -0
  51. package/dist/score.js.map +1 -0
  52. package/dist/shared/concurrency.d.ts +5 -0
  53. package/dist/shared/concurrency.js +17 -0
  54. package/dist/shared/concurrency.js.map +1 -0
  55. package/dist/shared/env.d.ts +27 -0
  56. package/dist/shared/env.js +29 -0
  57. package/dist/shared/env.js.map +1 -0
  58. package/dist/shared/errors.d.ts +18 -0
  59. package/dist/shared/errors.js +26 -0
  60. package/dist/shared/errors.js.map +1 -0
  61. package/dist/shared/log.d.ts +2 -0
  62. package/dist/shared/log.js +18 -0
  63. package/dist/shared/log.js.map +1 -0
  64. package/dist/shared/paths.d.ts +3 -0
  65. package/dist/shared/paths.js +6 -0
  66. package/dist/shared/paths.js.map +1 -0
  67. package/dist/shared/prompts.d.ts +3 -0
  68. package/dist/shared/prompts.js +34 -0
  69. package/dist/shared/prompts.js.map +1 -0
  70. package/package.json +79 -0
  71. package/prompts/judge.md +37 -0
  72. package/rules/any/futureproof/abstraction/pass-through-wrapper.md +70 -0
  73. package/rules/any/futureproof/abstraction/single-caller-helper.md +38 -0
  74. package/rules/any/futureproof/abstraction/single-impl-interface.md +83 -0
  75. package/rules/any/futureproof/exports/dead-export.md +62 -0
  76. package/rules/any/futureproof/layering/framework-type-in-domain.md +83 -0
  77. package/rules/any/futureproof/layering/logic-in-handler.md +94 -0
  78. package/rules/any/futureproof/params/boolean-positional-param.md +128 -0
  79. package/rules/any/futureproof/params/positional-config-args.md +135 -0
  80. package/rules/any/futureproof/structure/wide-function.md +138 -0
  81. package/rules/any/futureproof/testing/test-asserts-implementation.md +70 -0
  82. package/rules/any/hacky/comments/comment-restates-code.md +50 -0
  83. package/rules/any/hacky/comments/shipped-todo-comment.md +55 -0
  84. package/rules/any/hacky/concurrency/sleep-based-sync.md +95 -0
  85. package/rules/any/hacky/config/hardcoded-url.md +88 -0
  86. package/rules/any/hacky/constants/magic-number.md +94 -0
  87. package/rules/any/hacky/debugging/debug-print.md +73 -0
  88. package/rules/any/hacky/duplication/copy-paste-block.md +117 -0
  89. package/rules/any/hacky/types/stringly-typed-enum.md +86 -0
  90. package/rules/any/idiom/comments/no-section-banners.md +47 -0
  91. package/rules/go/futureproof/constants/string-enums-when-serialized.md +47 -0
  92. package/rules/go/futureproof/constants/typed-constants-for-enums.md +45 -0
  93. package/rules/go/futureproof/interfaces/compile-time-impl-assertion.md +43 -0
  94. package/rules/go/futureproof/interfaces/define-interfaces-at-consumer.md +47 -0
  95. package/rules/go/futureproof/interfaces/keep-interfaces-small.md +53 -0
  96. package/rules/go/futureproof/naming/no-utils-helpers-common-package.md +34 -0
  97. package/rules/go/futureproof/structs/zero-value-usable.md +50 -0
  98. package/rules/go/futureproof/types/generics-over-any.md +55 -0
  99. package/rules/go/hacky/concurrency/no-goroutine-without-wait.md +44 -0
  100. package/rules/go/hacky/context/never-pass-nil-context.md +64 -0
  101. package/rules/go/hacky/context/no-context-in-struct.md +44 -0
  102. package/rules/go/hacky/errors/check-close-error-on-writable.md +53 -0
  103. package/rules/go/hacky/errors/custom-error-type-errors-as.md +54 -0
  104. package/rules/go/hacky/errors/ignored-error-blank.md +84 -0
  105. package/rules/go/hacky/errors/panic-only-unrecoverable.md +77 -0
  106. package/rules/go/hacky/errors/sentinel-errors-with-errors-is.md +63 -0
  107. package/rules/go/hacky/errors/wrap-errors-with-w.md +71 -0
  108. package/rules/go/hacky/state/package-level-mutable-var.md +95 -0
  109. package/rules/go/hacky/structure/os-exit-only-in-main.md +61 -0
  110. package/rules/go/hacky/type-safety/no-map-string-any.md +49 -0
  111. package/rules/go/hacky/types/json-into-struct-not-map.md +48 -0
  112. package/rules/go/hacky/types/no-any.md +71 -0
  113. package/rules/go/hacky/types/no-map-any-any.md +35 -0
  114. package/rules/go/idiom/comments/doc-comment-on-exported.md +81 -0
  115. package/rules/go/idiom/comments/doc-comment-starts-with-name.md +36 -0
  116. package/rules/go/idiom/concurrency/mutex-over-channels-for-state.md +47 -0
  117. package/rules/go/idiom/constructors/constructor-named-new.md +37 -0
  118. package/rules/go/idiom/context/context-is-first-param.md +47 -0
  119. package/rules/go/idiom/context/io-takes-context.md +50 -0
  120. package/rules/go/idiom/errors/capitalized-error-message.md +52 -0
  121. package/rules/go/idiom/errors/check-error-immediately.md +45 -0
  122. package/rules/go/idiom/errors/defer-close-explicit-discard.md +65 -0
  123. package/rules/go/idiom/errors/error-is-last-return-value.md +53 -0
  124. package/rules/go/idiom/errors/no-log-and-return.md +45 -0
  125. package/rules/go/idiom/formatting/blank-line-before-return.md +48 -0
  126. package/rules/go/idiom/formatting/multiline-struct-literals.md +56 -0
  127. package/rules/go/idiom/imports/import-order-groups.md +42 -0
  128. package/rules/go/idiom/imports/side-effect-imports-own-group.md +51 -0
  129. package/rules/go/idiom/interfaces/accept-interfaces-return-concrete.md +39 -0
  130. package/rules/go/idiom/iterators/prefer-iter-seq.md +47 -0
  131. package/rules/go/idiom/logging/lowercase-log-messages.md +80 -0
  132. package/rules/go/idiom/logging/use-slog.md +46 -0
  133. package/rules/go/idiom/naming/acronyms-consistent-case.md +97 -0
  134. package/rules/go/idiom/naming/interface-er-suffix.md +33 -0
  135. package/rules/go/idiom/naming/no-package-name-stutter.md +37 -0
  136. package/rules/go/idiom/naming/package-name-single-lowercase-word.md +40 -0
  137. package/rules/go/idiom/receivers/consistent-receiver-kind-per-type.md +40 -0
  138. package/rules/go/idiom/receivers/no-this-receiver.md +37 -0
  139. package/rules/go/idiom/receivers/pointer-vs-value-receiver-choice.md +49 -0
  140. package/rules/go/idiom/receivers/short-receiver-names.md +43 -0
  141. package/rules/go/idiom/structs/new-expr-for-pointer-fields.md +40 -0
  142. package/rules/go/idiom/structure/main-delegates-to-run.md +54 -0
  143. package/rules/ts/futureproof/classes/composition-over-abstract-base.md +73 -0
  144. package/rules/ts/futureproof/functions/explicit-return-type-on-exports.md +50 -0
  145. package/rules/ts/futureproof/structure/colocate-zod-schemas.md +46 -0
  146. package/rules/ts/futureproof/types/exhaustive-switch-never-check.md +52 -0
  147. package/rules/ts/futureproof/types/readonly-for-immutable-data.md +46 -0
  148. package/rules/ts/futureproof/zod/enum-values-from-const-object.md +37 -0
  149. package/rules/ts/futureproof/zod/no-z-native-enum.md +36 -0
  150. package/rules/ts/futureproof/zod/schema-first-infer-type.md +46 -0
  151. package/rules/ts/hacky/async/no-floating-promises.md +52 -0
  152. package/rules/ts/hacky/config/env-read-outside-config.md +56 -0
  153. package/rules/ts/hacky/errors/catch-param-typed-unknown.md +67 -0
  154. package/rules/ts/hacky/errors/custom-error-class-instanceof.md +66 -0
  155. package/rules/ts/hacky/errors/empty-catch.md +52 -0
  156. package/rules/ts/hacky/errors/log-and-swallow.md +65 -0
  157. package/rules/ts/hacky/errors/throw-typed-error-with-context.md +64 -0
  158. package/rules/ts/hacky/operators/nullish-coalescing-over-or.md +50 -0
  159. package/rules/ts/hacky/state/exported-let.md +60 -0
  160. package/rules/ts/hacky/type-safety/no-double-assertion.md +34 -0
  161. package/rules/ts/hacky/type-safety/no-explicit-any.md +48 -0
  162. package/rules/ts/hacky/type-safety/no-non-null-assertion.md +43 -0
  163. package/rules/ts/hacky/type-safety/no-unchecked-type-assertion.md +54 -0
  164. package/rules/ts/hacky/type-safety/validate-parsed-json.md +48 -0
  165. package/rules/ts/idiom/async/no-promise-chains.md +45 -0
  166. package/rules/ts/idiom/async/parallelize-independent-awaits.md +46 -0
  167. package/rules/ts/idiom/comments/doc-comment-above-declaration.md +43 -0
  168. package/rules/ts/idiom/constants/as-const-for-literal-config.md +65 -0
  169. package/rules/ts/idiom/constants/no-enum.md +37 -0
  170. package/rules/ts/idiom/control-flow/always-brace-if.md +56 -0
  171. package/rules/ts/idiom/control-flow/guard-clauses-early-return.md +52 -0
  172. package/rules/ts/idiom/control-flow/no-nested-ternary.md +54 -0
  173. package/rules/ts/idiom/errors/error-message-lowercase.md +56 -0
  174. package/rules/ts/idiom/errors/log-at-boundary-not-every-layer.md +66 -0
  175. package/rules/ts/idiom/exports/inline-export-at-declaration.md +49 -0
  176. package/rules/ts/idiom/exports/no-default-export.md +37 -0
  177. package/rules/ts/idiom/formatting/multiline-object-literals.md +46 -0
  178. package/rules/ts/idiom/functions/arrow-for-callbacks.md +44 -0
  179. package/rules/ts/idiom/functions/function-declaration-for-top-level.md +56 -0
  180. package/rules/ts/idiom/imports/export-type-for-types.md +34 -0
  181. package/rules/ts/idiom/imports/import-order.md +66 -0
  182. package/rules/ts/idiom/imports/import-type-for-types.md +50 -0
  183. package/rules/ts/idiom/imports/side-effect-imports-last.md +42 -0
  184. package/rules/ts/idiom/logging/structured-logging-lowercase.md +45 -0
  185. package/rules/ts/idiom/naming/boolean-name-question-prefix.md +42 -0
  186. package/rules/ts/idiom/naming/descriptive-identifier-quality.md +48 -0
  187. package/rules/ts/idiom/naming/screaming-snake-module-constants.md +58 -0
  188. package/rules/ts/idiom/strings/template-literals-over-concat.md +51 -0
  189. package/rules/ts/idiom/types/interface-extends-over-intersection.md +44 -0
  190. package/rules/ts/idiom/types/null-vs-undefined-convention.md +43 -0
  191. package/rules/ts/idiom/types/prefer-type-over-interface.md +64 -0
  192. package/rules/ts/idiom/types/satisfies-over-annotation.md +43 -0
  193. package/rules/ts/idiom/types/type-predicates-for-narrowing.md +53 -0
  194. package/rules/ts/idiom/variables/const-by-default.md +41 -0
  195. package/rules/ts/idiom/variables/no-var.md +36 -0
@@ -0,0 +1,54 @@
1
+ ---
2
+ severity: major
3
+ detect: judge
4
+ falsePositives:
5
+ - '`as const` on a literal, which narrows rather than widens'
6
+ - a cast inside a branded-type constructor such as `return id as UserId`, where the function is the single point that mints the brand
7
+ - a cast that follows a runtime check in the same block, such as after `typeof`, `in`, or `Array.isArray`
8
+ - a cast to `unknown` on its own, which discards type information rather than inventing it
9
+ ---
10
+
11
+ ## Why
12
+
13
+ `value as T` does not check anything at runtime; it tells the compiler to stop looking. Used on data that came from outside the program, such as a request body, a parsed file, a database row, or an environment variable, it turns a type error into a crash or silent corruption somewhere downstream. Validate external data with a schema or a type guard and let the type come from the check.
14
+
15
+ ## Message
16
+
17
+ type assertion on unvalidated data; validate with a schema or a type guard instead
18
+
19
+ ## Bad
20
+
21
+ ```ts
22
+ app.post('/users', async (req, res) => {
23
+ // BAD: whatever the client sent is now a CreateUserInput as far as the compiler knows
24
+ const input = req.body as CreateUserInput;
25
+
26
+ res.json(await createUser(input));
27
+ });
28
+ ```
29
+
30
+ ```ts
31
+ // BAD: env vars may be undefined; the cast promises otherwise
32
+ const apiKey = process.env.API_KEY as string;
33
+ ```
34
+
35
+ ## Good
36
+
37
+ ```ts
38
+ app.post('/users', async (req, res) => {
39
+ const input = createUserSchema.parse(req.body);
40
+
41
+ res.json(await createUser(input));
42
+ });
43
+ ```
44
+
45
+ ```ts
46
+ const env = envSchema.parse(process.env);
47
+ const apiKey = env.API_KEY;
48
+ ```
49
+
50
+ ```ts
51
+ function UserId(id: string): UserId {
52
+ return id as UserId;
53
+ }
54
+ ```
@@ -0,0 +1,48 @@
1
+ ---
2
+ severity: major
3
+ detect: ast
4
+ ast:
5
+ rule:
6
+ any:
7
+ - pattern: JSON.parse($$$ARGS) as $T
8
+ - pattern: 'const $X: $T = JSON.parse($$$ARGS)'
9
+ constraints:
10
+ T:
11
+ not:
12
+ regex: '^unknown$'
13
+ ---
14
+
15
+ ## Why
16
+
17
+ `JSON.parse` returns whatever the text contained, and a cast or annotation on the result is a promise the program cannot keep. The first malformed file or changed API payload then fails deep inside code that trusted the shape, with an error that says nothing about the real cause. Parse to `unknown` and run the result through a schema so the failure is caught at the boundary with a message that names the bad field.
18
+
19
+ ## Message
20
+
21
+ `JSON.parse` result cast to a type without validation; parse it through a schema
22
+
23
+ ## Bad
24
+
25
+ ```ts
26
+ // BAD: the shape of raw is asserted, not checked
27
+ const config = JSON.parse(raw) as Config;
28
+ ```
29
+
30
+ ```ts
31
+ // BAD: annotation is a cast in disguise
32
+ const config: Config = JSON.parse(raw);
33
+ ```
34
+
35
+ ## Good
36
+
37
+ ```ts
38
+ const config = configSchema.parse(JSON.parse(raw));
39
+ ```
40
+
41
+ ```ts
42
+ const data: unknown = JSON.parse(raw);
43
+ const result = configSchema.safeParse(data);
44
+
45
+ if (!result.success) {
46
+ throw new ConfigError('invalid config', { cause: result.error });
47
+ }
48
+ ```
@@ -0,0 +1,45 @@
1
+ ---
2
+ severity: minor
3
+ detect: ast
4
+ ast:
5
+ rule:
6
+ any:
7
+ - pattern: $P.then($$$ARGS)
8
+ - pattern: $P.catch($$$ARGS)
9
+ - pattern: $P.finally($$$ARGS)
10
+ ---
11
+
12
+ ## Why
13
+
14
+ `async`/`await` reads top to bottom and keeps error handling in an ordinary `try`. Promise chains split the same logic across callbacks, lose stack context, and make the return value of the surrounding function harder to see.
15
+
16
+ ## Message
17
+
18
+ promise chain; use async/await
19
+
20
+ ## Bad
21
+
22
+ ```ts
23
+ function loadUser(id: string) {
24
+ // BAD: callback chain where a straight line would do
25
+ return fetchUser(id)
26
+ .then(user => enrich(user))
27
+ .catch(error => report(error));
28
+ }
29
+ ```
30
+
31
+ ## Good
32
+
33
+ ```ts
34
+ async function loadUser(id: string): Promise<User> {
35
+ try {
36
+ const user = await fetchUser(id);
37
+
38
+ return await enrich(user);
39
+ } catch (error) {
40
+ report(error);
41
+
42
+ throw error;
43
+ }
44
+ }
45
+ ```
@@ -0,0 +1,46 @@
1
+ ---
2
+ severity: minor
3
+ detect: judge
4
+ falsePositives:
5
+ - awaits where a later call uses the result of an earlier one, or where order matters for side effects such as writes
6
+ - operations against a resource that must not see concurrent calls, such as a single database transaction or a rate-limited API
7
+ - a sequence deliberately kept serial with a comment explaining why
8
+ ---
9
+
10
+ ## Why
11
+
12
+ Awaiting independent calls one after another makes the total latency the sum of the parts when it could be the slowest one. `Promise.all` runs them together and still gives typed results in order, and `Promise.allSettled` does the same when each result should be handled on its own. Serial awaits are right when one call feeds the next or when the resource cannot take concurrent calls; otherwise they are a slow habit.
13
+
14
+ ## Message
15
+
16
+ independent awaits run one after another; run them together with `Promise.all`
17
+
18
+ ## Bad
19
+
20
+ ```ts
21
+ // BAD: three round trips in sequence that share no data
22
+ async function loadProfile(userId: string): Promise<Profile> {
23
+ const user = await getUser(userId);
24
+ const posts = await getPosts(userId);
25
+ const followers = await getFollowers(userId);
26
+
27
+ return { user, posts, followers };
28
+ }
29
+ ```
30
+
31
+ ## Good
32
+
33
+ ```ts
34
+ async function loadProfile(userId: string): Promise<Profile> {
35
+ const [user, posts, followers] = await Promise.all([getUser(userId), getPosts(userId), getFollowers(userId)]);
36
+
37
+ return { user, posts, followers };
38
+ }
39
+ ```
40
+
41
+ ```ts
42
+ async function moveFunds(from: string, to: string, amount: number): Promise<void> {
43
+ await debit(from, amount);
44
+ await credit(to, amount);
45
+ }
46
+ ```
@@ -0,0 +1,43 @@
1
+ ---
2
+ severity: info
3
+ detect: judge
4
+ falsePositives:
5
+ - a short trailing comment on a single value inside an object or array literal, such as a unit or a magic number's meaning
6
+ - 'directive comments such as `eslint-disable` or `@ts-expect-error`, which must sit where the tool expects them'
7
+ - a comment on a line of its own inside a function body that explains the following statement
8
+ ---
9
+
10
+ ## Why
11
+
12
+ A comment that documents a declaration belongs on the line directly above it, as a `//` or `/** */` block, so editors show it on hover and a reader finds it before the signature rather than after. A trailing comment at the end of a declaration line is easy to miss, wraps badly, and is not picked up as documentation. Explanations inside the body of a function describe the body, not the contract.
13
+
14
+ ## Message
15
+
16
+ declaration documented in a trailing comment; put the comment on the line above
17
+
18
+ ## Bad
19
+
20
+ ```ts
21
+ // BAD: the doc is a trailing comment the tooling cannot see
22
+ export function parseDuration(input: string): number { // parses "5m", "2h" into milliseconds
23
+ return toMilliseconds(input);
24
+ }
25
+ ```
26
+
27
+ ## Good
28
+
29
+ ```ts
30
+ // parses "5m" or "2h" into milliseconds
31
+ export function parseDuration(input: string): number {
32
+ return toMilliseconds(input);
33
+ }
34
+ ```
35
+
36
+ ```ts
37
+ /**
38
+ * retries the operation up to three times with exponential backoff, then rethrows
39
+ */
40
+ export function retry<T>(operation: () => Promise<T>): Promise<T> {
41
+ return withBackoff(operation, 3);
42
+ }
43
+ ```
@@ -0,0 +1,65 @@
1
+ ---
2
+ severity: minor
3
+ detect: ast
4
+ ast:
5
+ rule:
6
+ pattern: const $NAME = $OBJ
7
+ inside:
8
+ any:
9
+ - kind: program
10
+ - kind: export_statement
11
+ constraints:
12
+ NAME:
13
+ regex: '^[A-Z]'
14
+ OBJ:
15
+ kind: object
16
+ has:
17
+ kind: pair
18
+ not:
19
+ has:
20
+ kind: pair
21
+ has:
22
+ field: value
23
+ not:
24
+ any:
25
+ - kind: string
26
+ - kind: number
27
+ ---
28
+
29
+ ## Why
30
+
31
+ A PascalCase const object of literal values is meant to act as an enum, but without `as const` every value widens to `string` or `number`, so `typeof Status[keyof typeof Status]` is just `string` and the compiler cannot check a switch over it. Adding `as const` freezes the values to their literals, makes the derived union real, and marks the object as readonly at the type level. It is the difference between a named constant set and a bag of strings.
32
+
33
+ ## Message
34
+
35
+ literal const object without `as const`; its values widen to `string`
36
+
37
+ ## Bad
38
+
39
+ ```ts
40
+ // BAD: Status.PENDING is typed as string, not 'pending'
41
+ const Status = {
42
+ PENDING: 'pending',
43
+ ACTIVE: 'active',
44
+ };
45
+
46
+ type Status = (typeof Status)[keyof typeof Status];
47
+ ```
48
+
49
+ ## Good
50
+
51
+ ```ts
52
+ const Status = {
53
+ PENDING: 'pending',
54
+ ACTIVE: 'active',
55
+ } as const;
56
+
57
+ type Status = (typeof Status)[keyof typeof Status];
58
+ ```
59
+
60
+ ```ts
61
+ const defaults = {
62
+ timeout: 5000,
63
+ retries: 3,
64
+ };
65
+ ```
@@ -0,0 +1,37 @@
1
+ ---
2
+ severity: minor
3
+ detect: ast
4
+ ast:
5
+ rule:
6
+ kind: enum_declaration
7
+ ---
8
+
9
+ ## Why
10
+
11
+ TypeScript enums are a runtime construct with their own semantics: numeric enums accept any number, `const enum` behaves differently under isolated modules, and they do not compose with string literal unions or Zod. A const object with `as const` plus a derived union type gives the same ergonomics with plain objects and plain strings.
12
+
13
+ ## Message
14
+
15
+ enum is a runtime construct; use a const object with `as const` and a derived union
16
+
17
+ ## Bad
18
+
19
+ ```ts
20
+ // BAD: a runtime enum with numeric holes
21
+ enum Status {
22
+ Active,
23
+ Inactive,
24
+ }
25
+ ```
26
+
27
+ ## Good
28
+
29
+ ```ts
30
+ // GOOD: a const object and its derived union
31
+ export const Status = {
32
+ ACTIVE: 'active',
33
+ INACTIVE: 'inactive',
34
+ } as const;
35
+
36
+ export type Status = (typeof Status)[keyof typeof Status];
37
+ ```
@@ -0,0 +1,56 @@
1
+ ---
2
+ severity: minor
3
+ detect: ast
4
+ ast:
5
+ rule:
6
+ any:
7
+ - kind: if_statement
8
+ not:
9
+ has:
10
+ field: consequence
11
+ kind: statement_block
12
+ - kind: else_clause
13
+ not:
14
+ has:
15
+ any:
16
+ - kind: statement_block
17
+ - kind: if_statement
18
+ ---
19
+
20
+ ## Why
21
+
22
+ A braceless `if (x) return;` works until someone adds a second statement under it and only the first one stays conditional. Braces make the body a visible block, keep the diff small when a line is added, and remove a whole class of indentation bugs. The two extra characters cost nothing to read.
23
+
24
+ ## Message
25
+
26
+ braceless `if` body; wrap it in a block
27
+
28
+ ## Bad
29
+
30
+ ```ts
31
+ // BAD: a second statement added here would run unconditionally
32
+ if (!user) return undefined;
33
+ ```
34
+
35
+ ```ts
36
+ if (user.isActive) {
37
+ activate(user);
38
+ // BAD: braceless else body
39
+ } else deactivate(user);
40
+ ```
41
+
42
+ ## Good
43
+
44
+ ```ts
45
+ if (!user) {
46
+ return undefined;
47
+ }
48
+
49
+ if (user.isActive) {
50
+ activate(user);
51
+ } else if (user.isPending) {
52
+ remind(user);
53
+ } else {
54
+ deactivate(user);
55
+ }
56
+ ```
@@ -0,0 +1,52 @@
1
+ ---
2
+ severity: minor
3
+ detect: judge
4
+ falsePositives:
5
+ - a single `if` with an `else` where both branches are one or two lines and neither is an error path
6
+ - branches that must run cleanup or logging before returning, where flattening would duplicate that code
7
+ - a `switch` or a chain of `if` returning a value per case, which is already flat
8
+ ---
9
+
10
+ ## Why
11
+
12
+ Nesting the happy path inside a pyramid of `if` blocks pushes the code the function exists for to the deepest indentation and makes every reader hold the whole condition stack in their head. Checking preconditions first and returning or throwing early keeps the main logic at the top level, reads in the order the cases are decided, and makes each failure case a self-contained line. The result is shorter, flatter, and easier to change.
13
+
14
+ ## Message
15
+
16
+ nested conditionals wrap the happy path; check preconditions first and return early
17
+
18
+ ## Bad
19
+
20
+ ```ts
21
+ // BAD: the update is buried two levels deep under checks that could exit early
22
+ async function updateUser(id: string, input: UpdateUserInput): Promise<User> {
23
+ const user = await repository.find(id);
24
+ if (user) {
25
+ if (user.isActive) {
26
+ return repository.update(id, input);
27
+ } else {
28
+ throw new ForbiddenError(`user ${id} is deactivated`);
29
+ }
30
+ } else {
31
+ throw new NotFoundError(`user ${id}`);
32
+ }
33
+ }
34
+ ```
35
+
36
+ ## Good
37
+
38
+ ```ts
39
+ async function updateUser(id: string, input: UpdateUserInput): Promise<User> {
40
+ const user = await repository.find(id);
41
+
42
+ if (!user) {
43
+ throw new NotFoundError(`user ${id}`);
44
+ }
45
+
46
+ if (!user.isActive) {
47
+ throw new ForbiddenError(`user ${id} is deactivated`);
48
+ }
49
+
50
+ return repository.update(id, input);
51
+ }
52
+ ```
@@ -0,0 +1,54 @@
1
+ ---
2
+ severity: minor
3
+ detect: ast
4
+ ast:
5
+ rule:
6
+ kind: ternary_expression
7
+ inside:
8
+ kind: ternary_expression
9
+ stopBy:
10
+ not:
11
+ kind: parenthesized_expression
12
+ ---
13
+
14
+ ## Why
15
+
16
+ A ternary is readable when it has one condition and two outcomes. Nesting another inside turns it into a puzzle the reader has to unfold, and the precedence rules do not help. Use an if chain, a switch, or a lookup object.
17
+
18
+ ## Message
19
+
20
+ nested ternary; use an if chain, a switch, or a lookup
21
+
22
+ ## Bad
23
+
24
+ ```ts
25
+ // BAD: three outcomes folded into one expression
26
+ const label = count === 0 ? 'none' : count === 1 ? 'one' : 'many';
27
+ ```
28
+
29
+ ## Good
30
+
31
+ ```ts
32
+ // GOOD: a ternary inside a callback is its own expression, not a nested branch
33
+ const handler = isEnabled ? () => (isDark ? darkTheme : lightTheme) : undefined;
34
+ ```
35
+
36
+ ```ts
37
+ // GOOD: one condition, two outcomes
38
+ const label = isEmpty ? 'none' : 'some';
39
+ ```
40
+
41
+ ```ts
42
+ // GOOD: a function names the branching
43
+ function describeCount(count: number): string {
44
+ if (count === 0) {
45
+ return 'none';
46
+ }
47
+
48
+ if (count === 1) {
49
+ return 'one';
50
+ }
51
+
52
+ return 'many';
53
+ }
54
+ ```
@@ -0,0 +1,56 @@
1
+ ---
2
+ severity: minor
3
+ detect: ast
4
+ ast:
5
+ rule:
6
+ any:
7
+ - pattern: new $CLS($MSG)
8
+ - pattern: new $CLS($MSG, $$$REST)
9
+ constraints:
10
+ CLS:
11
+ regex: 'Error$'
12
+ MSG:
13
+ regex: '^.[A-Z][a-z]'
14
+ ---
15
+
16
+ ## Why
17
+
18
+ Error messages get wrapped and joined with other messages, as in `loading config: reading file: permission denied`, and a capitalized fragment in the middle of that chain reads like a new sentence. Messages are lowercase fragments without trailing punctuation so they compose cleanly and match the log lines around them. Acronyms and proper nouns at the start are fine.
19
+
20
+ ## Message
21
+
22
+ error message starts with a capital letter; write it as a lowercase fragment
23
+
24
+ ## Bad
25
+
26
+ ```ts
27
+ if (!config) {
28
+ // BAD: capitalized message reads wrong once it is wrapped
29
+ throw new Error('Config file is missing');
30
+ }
31
+ ```
32
+
33
+ ```ts
34
+ if (!user) {
35
+ // BAD: capitalized message in a custom error
36
+ throw new NotFoundError(`User ${id} does not exist`, 404);
37
+ }
38
+ ```
39
+
40
+ ## Good
41
+
42
+ ```ts
43
+ if (!config) {
44
+ throw new ConfigError('config file is missing');
45
+ }
46
+ ```
47
+
48
+ ```ts
49
+ if (!user) {
50
+ throw new NotFoundError(`user ${id} does not exist`);
51
+ }
52
+ ```
53
+
54
+ ```ts
55
+ throw new UpstreamError('HTTP 502 from payments service');
56
+ ```
@@ -0,0 +1,66 @@
1
+ ---
2
+ severity: minor
3
+ detect: judge
4
+ falsePositives:
5
+ - a catch that logs and then handles the error without rethrowing, such as falling back to a default or skipping one item in a batch
6
+ - the outermost handler, controller, job runner, or CLI entrypoint, which is the boundary and should log
7
+ - a catch that adds context by wrapping the error in a typed error before rethrowing, without logging
8
+ ---
9
+
10
+ ## Why
11
+
12
+ When every layer catches an error, logs it, and rethrows, one failure produces a stack of near-identical log lines and the boundary that finally handles it logs it again. Errors should propagate untouched, or wrapped with context, until they reach the boundary, such as the request handler or the job runner, where they are logged once with full context and turned into a response. Inner layers that only log and rethrow are noise generators.
13
+
14
+ ## Message
15
+
16
+ catch logs and rethrows in an inner layer; let it propagate and log once at the boundary
17
+
18
+ ## Bad
19
+
20
+ ```ts
21
+ class UserService {
22
+ async getUser(id: string): Promise<User> {
23
+ try {
24
+ return await this.repository.find(id);
25
+ } catch (error) {
26
+ // BAD: the handler above will log this same error again
27
+ this.logger.error('get user failed', { error });
28
+
29
+ throw error;
30
+ }
31
+ }
32
+ }
33
+ ```
34
+
35
+ ## Good
36
+
37
+ ```ts
38
+ class UserService {
39
+ async getUser(id: string): Promise<User> {
40
+ const user = await this.repository.find(id);
41
+
42
+ if (!user) {
43
+ throw new NotFoundError(`user ${id}`);
44
+ }
45
+
46
+ return user;
47
+ }
48
+ }
49
+ ```
50
+
51
+ ```ts
52
+ async function handleGetUser(req: Request, res: Response): Promise<void> {
53
+ try {
54
+ res.json(await userService.getUser(req.params.id));
55
+ } catch (error: unknown) {
56
+ if (error instanceof NotFoundError) {
57
+ res.status(404).json({ error: error.message });
58
+
59
+ return;
60
+ }
61
+
62
+ logger.error('get user failed', { error, userId: req.params.id });
63
+ res.status(500).json({ error: 'internal error' });
64
+ }
65
+ }
66
+ ```
@@ -0,0 +1,49 @@
1
+ ---
2
+ severity: minor
3
+ detect: ast
4
+ ast:
5
+ rule:
6
+ kind: export_statement
7
+ has:
8
+ kind: export_clause
9
+ not:
10
+ has:
11
+ field: source
12
+ kind: string
13
+ ---
14
+
15
+ ## Why
16
+
17
+ An export list at the bottom of a file separates the decision to export from the thing being exported, so a reader looking at a declaration cannot tell whether it is public without scrolling. Exporting at the declaration site keeps that intent next to the code and means a rename or removal touches one place instead of two. Re-exports from another module are a different construct and are fine.
18
+
19
+ ## Message
20
+
21
+ export list detached from its declarations; export at the declaration site
22
+
23
+ ## Bad
24
+
25
+ ```ts
26
+ const MAX_RETRIES = 3;
27
+
28
+ function getUser(id: string): Promise<User> {
29
+ return repository.find(id);
30
+ }
31
+
32
+ // BAD: the export decision lives far from the declarations
33
+ export { MAX_RETRIES, getUser };
34
+ ```
35
+
36
+ ## Good
37
+
38
+ ```ts
39
+ export const MAX_RETRIES = 3;
40
+
41
+ export function getUser(id: string): Promise<User> {
42
+ return repository.find(id);
43
+ }
44
+ ```
45
+
46
+ ```ts
47
+ export { createUser } from './user';
48
+ export type { User } from './user';
49
+ ```