@agjs/tsforge 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 (216) hide show
  1. package/bin/tsforge.js +2 -0
  2. package/package.json +35 -0
  3. package/src/agent/agent.constants.ts +382 -0
  4. package/src/agent/agent.types.ts +34 -0
  5. package/src/agent/index.ts +4 -0
  6. package/src/agent/model-agent.ts +297 -0
  7. package/src/agent/tool-repair.ts +194 -0
  8. package/src/agent/tools.ts +190 -0
  9. package/src/browser/checks.ts +96 -0
  10. package/src/browser/index.ts +8 -0
  11. package/src/browser/oracle.ts +303 -0
  12. package/src/classify.ts +48 -0
  13. package/src/cli.ts +1333 -0
  14. package/src/config/config.constants.ts +9 -0
  15. package/src/config/flags.ts +32 -0
  16. package/src/config/index.ts +8 -0
  17. package/src/config/tsforge-config.ts +301 -0
  18. package/src/constitution/baseline.ts +257 -0
  19. package/src/detect-gate.ts +498 -0
  20. package/src/eval/eval.types.ts +36 -0
  21. package/src/eval/index.ts +3 -0
  22. package/src/eval/judge.ts +62 -0
  23. package/src/eval/score.ts +39 -0
  24. package/src/files/create.ts +22 -0
  25. package/src/files/edit.ts +193 -0
  26. package/src/files/files.constants.ts +11 -0
  27. package/src/files/files.types.ts +81 -0
  28. package/src/files/hashline-format.ts +110 -0
  29. package/src/files/hashline.ts +689 -0
  30. package/src/files/index.ts +19 -0
  31. package/src/index.ts +8 -0
  32. package/src/inference/index.ts +6 -0
  33. package/src/inference/inference.constants.ts +34 -0
  34. package/src/inference/inference.types.ts +123 -0
  35. package/src/inference/openai-compatible.ts +113 -0
  36. package/src/inference/stream-guard.ts +161 -0
  37. package/src/inference/stream.ts +370 -0
  38. package/src/inference/transport.ts +78 -0
  39. package/src/inference/wire.ts +0 -0
  40. package/src/lib/fs/fs.ts +126 -0
  41. package/src/lib/fs/fs.types.ts +5 -0
  42. package/src/lib/fs/index.ts +3 -0
  43. package/src/lib/fs/process.ts +146 -0
  44. package/src/lib/guards/guards.ts +9 -0
  45. package/src/lib/guards/index.ts +1 -0
  46. package/src/lib/json/index.ts +1 -0
  47. package/src/lib/json/json.ts +12 -0
  48. package/src/lib/scope/index.ts +2 -0
  49. package/src/lib/scope/scope.constants.ts +3 -0
  50. package/src/lib/scope/scope.ts +40 -0
  51. package/src/loop/astgrep-fix.ts +228 -0
  52. package/src/loop/feedback/feedback.ts +138 -0
  53. package/src/loop/feedback/index.ts +8 -0
  54. package/src/loop/feedback/meta-rule-docs.ts +41 -0
  55. package/src/loop/feedback/meta-rule-feedback.ts +61 -0
  56. package/src/loop/feedback/rule-docs.generated.json +112 -0
  57. package/src/loop/feedback/rule-docs.ts +342 -0
  58. package/src/loop/index.ts +19 -0
  59. package/src/loop/loop.constants.ts +68 -0
  60. package/src/loop/loop.types.ts +99 -0
  61. package/src/loop/prompt/index.ts +2 -0
  62. package/src/loop/prompt/project-map.ts +69 -0
  63. package/src/loop/prompt/prompt.ts +107 -0
  64. package/src/loop/quality.ts +174 -0
  65. package/src/loop/rule-docs.generated.json +367 -0
  66. package/src/loop/run-spec.ts +88 -0
  67. package/src/loop/run.ts +400 -0
  68. package/src/loop/session.ts +1410 -0
  69. package/src/loop/tools/add-dependency.ts +71 -0
  70. package/src/loop/tools/condense.ts +498 -0
  71. package/src/loop/tools/edit-hashline.ts +80 -0
  72. package/src/loop/tools/execute-tool.ts +80 -0
  73. package/src/loop/tools/file-ops.ts +323 -0
  74. package/src/loop/tools/index.ts +2 -0
  75. package/src/loop/tools/lsp-ops.ts +222 -0
  76. package/src/loop/tools/scaffold-routes.ts +68 -0
  77. package/src/loop/tools/scaffold-ui.ts +62 -0
  78. package/src/loop/tools/scaffold-web.ts +35 -0
  79. package/src/loop/tools/tool-context.ts +126 -0
  80. package/src/loop/ttsr-defaults.ts +53 -0
  81. package/src/loop/ttsr.ts +322 -0
  82. package/src/loop/turn.ts +856 -0
  83. package/src/lsp/index.ts +2 -0
  84. package/src/lsp/lsp.types.ts +56 -0
  85. package/src/lsp/service.ts +500 -0
  86. package/src/meta-rules/context.ts +195 -0
  87. package/src/meta-rules/index.ts +9 -0
  88. package/src/meta-rules/meta-rules.types.ts +47 -0
  89. package/src/meta-rules/parsers/package-json-parser.ts +51 -0
  90. package/src/meta-rules/registry.ts +37 -0
  91. package/src/meta-rules/rules/ci/workflow-actions-pinned.ts +59 -0
  92. package/src/meta-rules/rules/ci/workflow-runner-pinned.ts +57 -0
  93. package/src/meta-rules/rules/ci/workflow-timeout-required.ts +114 -0
  94. package/src/meta-rules/rules/config/tsconfig-paths-exist.ts +117 -0
  95. package/src/meta-rules/rules/config/tsconfig-strict.ts +91 -0
  96. package/src/meta-rules/rules/source-text/no-eslint-disable-comments.ts +34 -0
  97. package/src/meta-rules/rules/source-text/no-ts-suppressions.ts +38 -0
  98. package/src/meta-rules/rules/supply-chain/no-overlapping-libs.ts +57 -0
  99. package/src/meta-rules/rules/supply-chain/package-exact-deps.ts +55 -0
  100. package/src/meta-rules/rules/testing/test-sibling-required.ts +110 -0
  101. package/src/meta-rules/runner.ts +64 -0
  102. package/src/models-config.ts +196 -0
  103. package/src/render/ansi.ts +289 -0
  104. package/src/render/banner.ts +113 -0
  105. package/src/render/box.ts +134 -0
  106. package/src/render/index.ts +7 -0
  107. package/src/render/markdown.ts +123 -0
  108. package/src/render/render.types.ts +21 -0
  109. package/src/render/stream-markdown.ts +128 -0
  110. package/src/render/style.ts +26 -0
  111. package/src/rule-packs/bullmq/index.ts +39 -0
  112. package/src/rule-packs/bullmq/rules/index.ts +7 -0
  113. package/src/rule-packs/bullmq/rules/job-name-must-be-constant.ts +141 -0
  114. package/src/rule-packs/bullmq/rules/job-options-must-set-attempts.ts +174 -0
  115. package/src/rule-packs/bullmq/rules/no-blocking-concurrency-zero.ts +103 -0
  116. package/src/rule-packs/bullmq/rules/queue-options-must-set-removeoncomplete.ts +130 -0
  117. package/src/rule-packs/bullmq/rules/queue-options-must-set-removeonfail.ts +130 -0
  118. package/src/rule-packs/bullmq/rules/worker-must-implement-close.ts +182 -0
  119. package/src/rule-packs/bullmq/rules/worker-must-listen-failed.ts +140 -0
  120. package/src/rule-packs/bullmq/utils.ts +334 -0
  121. package/src/rule-packs/code-flow/index.ts +25 -0
  122. package/src/rule-packs/code-flow/rules/index.ts +3 -0
  123. package/src/rule-packs/code-flow/rules/no-bare-date-now.ts +138 -0
  124. package/src/rule-packs/code-flow/rules/no-template-trim-empty-ternary.ts +87 -0
  125. package/src/rule-packs/code-flow/rules/prefer-early-return.ts +80 -0
  126. package/src/rule-packs/code-flow/utils/prefer-early-return.ts +132 -0
  127. package/src/rule-packs/comment-hygiene/index.ts +25 -0
  128. package/src/rule-packs/comment-hygiene/rules/index.ts +3 -0
  129. package/src/rule-packs/comment-hygiene/rules/no-historical-comments.ts +102 -0
  130. package/src/rule-packs/comment-hygiene/rules/no-narration-comments.ts +83 -0
  131. package/src/rule-packs/comment-hygiene/rules/no-pr-reference-comments.ts +90 -0
  132. package/src/rule-packs/create-rule.ts +9 -0
  133. package/src/rule-packs/drizzle/index.ts +41 -0
  134. package/src/rule-packs/drizzle/rules/account-scoped-tables-require-where.ts +371 -0
  135. package/src/rule-packs/drizzle/rules/index.ts +8 -0
  136. package/src/rule-packs/drizzle/rules/no-nested-db-transaction.ts +127 -0
  137. package/src/rule-packs/drizzle/rules/no-raw-sql-outside-allowlist.ts +100 -0
  138. package/src/rule-packs/drizzle/rules/relations-must-cover-fks.ts +209 -0
  139. package/src/rule-packs/drizzle/rules/schema-files-must-not-import-driver.ts +127 -0
  140. package/src/rule-packs/drizzle/rules/schema-files-must-only-export-schema.ts +149 -0
  141. package/src/rule-packs/drizzle/rules/tables-must-have-timestamps.ts +312 -0
  142. package/src/rule-packs/drizzle/rules/timestamp-must-specify-mode.ts +166 -0
  143. package/src/rule-packs/drizzle/utils.ts +115 -0
  144. package/src/rule-packs/elysia/index.ts +43 -0
  145. package/src/rule-packs/elysia/rules/consistent-status-via-set.ts +69 -0
  146. package/src/rule-packs/elysia/rules/no-decorate-state-collision.ts +276 -0
  147. package/src/rule-packs/elysia/rules/no-separate-model-interfaces.ts +144 -0
  148. package/src/rule-packs/elysia/rules/prefer-destructured-context.ts +155 -0
  149. package/src/rule-packs/elysia/rules/prefer-direct-return.ts +176 -0
  150. package/src/rule-packs/elysia/rules/prefer-static-services.ts +159 -0
  151. package/src/rule-packs/elysia/rules/prefer-throw-status.ts +151 -0
  152. package/src/rule-packs/elysia/rules/require-hooks-before-routes.ts +209 -0
  153. package/src/rule-packs/elysia/rules/require-plugin-name.ts +107 -0
  154. package/src/rule-packs/elysia/utils/elysiaChain.ts +306 -0
  155. package/src/rule-packs/env-access/index.ts +23 -0
  156. package/src/rule-packs/env-access/rules/index.ts +2 -0
  157. package/src/rule-packs/env-access/rules/no-direct-process-env.ts +133 -0
  158. package/src/rule-packs/env-access/rules/no-process-exit.ts +95 -0
  159. package/src/rule-packs/i18n-keys/index.ts +19 -0
  160. package/src/rule-packs/i18n-keys/rules/static-translation-key-exists.ts +173 -0
  161. package/src/rule-packs/index.ts +139 -0
  162. package/src/rule-packs/jwt-cookies/index.ts +25 -0
  163. package/src/rule-packs/jwt-cookies/rules/auth-cookie-must-be-httponly.ts +150 -0
  164. package/src/rule-packs/jwt-cookies/rules/auth-cookie-must-be-secure-in-prod.ts +149 -0
  165. package/src/rule-packs/jwt-cookies/rules/bcrypt-rounds-min.ts +195 -0
  166. package/src/rule-packs/jwt-cookies/utils.ts +188 -0
  167. package/src/rule-packs/oauth-security/index.ts +25 -0
  168. package/src/rule-packs/oauth-security/rules/pkce-required-for-oidc.ts +296 -0
  169. package/src/rule-packs/oauth-security/rules/state-must-be-redis-backed.ts +193 -0
  170. package/src/rule-packs/oauth-security/rules/state-ttl-bounded.ts +219 -0
  171. package/src/rule-packs/oauth-security/utils.ts +127 -0
  172. package/src/rule-packs/react-component-architecture/index.ts +35 -0
  173. package/src/rule-packs/react-component-architecture/rules/component-folder-structure.ts +123 -0
  174. package/src/rule-packs/react-component-architecture/rules/forwardref-display-name.ts +93 -0
  175. package/src/rule-packs/react-component-architecture/rules/index-must-reexport-default.ts +123 -0
  176. package/src/rule-packs/react-component-architecture/rules/max-hooks-per-file.ts +122 -0
  177. package/src/rule-packs/react-component-architecture/rules/no-cross-feature-imports.ts +170 -0
  178. package/src/rule-packs/react-component-architecture/rules/no-inline-jsx-functions.ts +66 -0
  179. package/src/rule-packs/react-component-architecture/utils.ts +47 -0
  180. package/src/rule-packs/rule-packs.types.ts +18 -0
  181. package/src/rule-packs/structured-logging/index.ts +26 -0
  182. package/src/rule-packs/structured-logging/rules/mask-pii-fields.ts +221 -0
  183. package/src/rule-packs/structured-logging/rules/no-error-stringify.ts +217 -0
  184. package/src/rule-packs/structured-logging/rules/require-event-field.ts +136 -0
  185. package/src/rule-packs/structured-logging/utils/logger.ts +104 -0
  186. package/src/rule-packs/tanstack-query/index.ts +20 -0
  187. package/src/rule-packs/tanstack-query/rules/prefix-query-key-must-use-set-queries-data.ts +321 -0
  188. package/src/rule-packs/test-conventions/index.ts +23 -0
  189. package/src/rule-packs/test-conventions/rules/index.ts +2 -0
  190. package/src/rule-packs/test-conventions/rules/no-focused-tests.ts +170 -0
  191. package/src/rule-packs/test-conventions/rules/test-file-mirrors-source.ts +127 -0
  192. package/src/rule-packs/utils.ts +142 -0
  193. package/src/session-store.ts +359 -0
  194. package/src/spec/generate-tests.ts +213 -0
  195. package/src/spec/index.ts +5 -0
  196. package/src/spec/parse.ts +152 -0
  197. package/src/spec/review-tests.ts +162 -0
  198. package/src/spec/spec.constants.ts +13 -0
  199. package/src/spec/spec.types.ts +79 -0
  200. package/src/stack-detection/detect.ts +246 -0
  201. package/src/stack-detection/index.ts +3 -0
  202. package/src/stack-detection/packs.ts +174 -0
  203. package/src/stack-detection/stack-detection.types.ts +47 -0
  204. package/src/validate/accept.ts +49 -0
  205. package/src/validate/errors.ts +35 -0
  206. package/src/validate/index.ts +12 -0
  207. package/src/validate/parse.ts +148 -0
  208. package/src/validate/run-tests.ts +59 -0
  209. package/src/validate/validate.ts +40 -0
  210. package/src/validate/validate.types.ts +52 -0
  211. package/src/web-components.ts +638 -0
  212. package/src/web-coverage.ts +89 -0
  213. package/src/web-routes.ts +151 -0
  214. package/src/web-templates.ts +1011 -0
  215. package/strict.eslint.config.mjs +84 -0
  216. package/strict.web.eslint.config.mjs +185 -0
@@ -0,0 +1,334 @@
1
+ import { AST_NODE_TYPES, type TSESTree } from "@typescript-eslint/utils";
2
+
3
+ import { walkAll } from "../utils";
4
+
5
+ export { walkAll, walkSome } from "../utils";
6
+
7
+ /**
8
+ * Helper utilities for BullMQ rules.
9
+ */
10
+
11
+ export interface BullmqImports {
12
+ hasBullmqImport: boolean;
13
+ workerLocalNames: Set<string>;
14
+ queueLocalNames: Set<string>;
15
+ queueEventsLocalNames: Set<string>;
16
+ }
17
+
18
+ export interface QueueDefinition {
19
+ bindingKey: string;
20
+ defaultJobOptions?: TSESTree.ObjectExpression | null;
21
+ }
22
+
23
+ export interface WorkerDefinition {
24
+ bindingKey: string | null;
25
+ node: TSESTree.NewExpression;
26
+ }
27
+
28
+ export function analyzeBullmqImports(program: TSESTree.Program): BullmqImports {
29
+ const result: BullmqImports = {
30
+ hasBullmqImport: false,
31
+ workerLocalNames: new Set(),
32
+ queueLocalNames: new Set(),
33
+ queueEventsLocalNames: new Set(),
34
+ };
35
+
36
+ for (const stmt of program.body) {
37
+ if (stmt.type !== AST_NODE_TYPES.ImportDeclaration) {
38
+ continue;
39
+ }
40
+
41
+ if (stmt.source.value !== "bullmq") {
42
+ continue;
43
+ }
44
+
45
+ result.hasBullmqImport = true;
46
+
47
+ for (const specifier of stmt.specifiers) {
48
+ recordImportSpecifier(specifier, result);
49
+ }
50
+ }
51
+
52
+ return result;
53
+ }
54
+
55
+ function recordImportSpecifier(
56
+ specifier: TSESTree.ImportClause,
57
+ result: BullmqImports
58
+ ): void {
59
+ if (specifier.type !== AST_NODE_TYPES.ImportSpecifier) {
60
+ return;
61
+ }
62
+
63
+ if (specifier.imported.type !== AST_NODE_TYPES.Identifier) {
64
+ return;
65
+ }
66
+
67
+ const target = {
68
+ Worker: result.workerLocalNames,
69
+ Queue: result.queueLocalNames,
70
+ QueueEvents: result.queueEventsLocalNames,
71
+ }[specifier.imported.name];
72
+
73
+ target?.add(specifier.local.name);
74
+ }
75
+
76
+ function extractDefaultJobOptions(
77
+ newExpr: TSESTree.NewExpression
78
+ ): TSESTree.ObjectExpression | null {
79
+ const opts = getOptionsObjectArg(newExpr, 1);
80
+
81
+ if (!opts) {
82
+ return null;
83
+ }
84
+
85
+ const property = findObjectProperty(opts, "defaultJobOptions");
86
+
87
+ if (!property) {
88
+ return null;
89
+ }
90
+
91
+ if (property.value.type === AST_NODE_TYPES.ObjectExpression) {
92
+ return property.value;
93
+ }
94
+
95
+ return null;
96
+ }
97
+
98
+ function isNewQueue(
99
+ node: TSESTree.Node,
100
+ imports: BullmqImports
101
+ ): node is TSESTree.NewExpression {
102
+ if (node.type !== AST_NODE_TYPES.NewExpression) {
103
+ return false;
104
+ }
105
+
106
+ if (node.callee.type !== AST_NODE_TYPES.Identifier) {
107
+ return false;
108
+ }
109
+
110
+ return imports.queueLocalNames.has(node.callee.name);
111
+ }
112
+
113
+ export function collectQueueDefinitions(
114
+ program: TSESTree.Program,
115
+ imports: BullmqImports
116
+ ): Map<string, QueueDefinition> {
117
+ const queues = new Map<string, QueueDefinition>();
118
+
119
+ walkAll(program, (node) => {
120
+ if (
121
+ node.type === AST_NODE_TYPES.VariableDeclarator &&
122
+ node.id.type === AST_NODE_TYPES.Identifier &&
123
+ node.init &&
124
+ isNewQueue(node.init, imports)
125
+ ) {
126
+ const varName = node.id.name;
127
+ const firstArg = node.init.arguments[0];
128
+
129
+ if (firstArg?.type === AST_NODE_TYPES.Literal) {
130
+ const queueName = firstArg.value;
131
+
132
+ if (typeof queueName === "string") {
133
+ const defaultJobOptions = extractDefaultJobOptions(node.init);
134
+
135
+ queues.set(varName, {
136
+ bindingKey: varName,
137
+ defaultJobOptions,
138
+ });
139
+ }
140
+ }
141
+ }
142
+ });
143
+
144
+ walkAll(program, (node) => {
145
+ if (!isNewQueue(node, imports)) {
146
+ return;
147
+ }
148
+
149
+ const firstArg = node.arguments[0];
150
+
151
+ if (firstArg?.type !== AST_NODE_TYPES.Literal) {
152
+ return;
153
+ }
154
+
155
+ const queueName = firstArg.value;
156
+
157
+ if (typeof queueName !== "string") {
158
+ return;
159
+ }
160
+
161
+ const defaultJobOptions = extractDefaultJobOptions(node);
162
+ const { parent } = node;
163
+
164
+ if (
165
+ parent.type !== AST_NODE_TYPES.VariableDeclarator ||
166
+ parent.id.type !== AST_NODE_TYPES.Identifier
167
+ ) {
168
+ queues.set(queueName, {
169
+ bindingKey: queueName,
170
+ defaultJobOptions,
171
+ });
172
+ }
173
+ });
174
+
175
+ return queues;
176
+ }
177
+
178
+ export function collectWorkerDefinitions(
179
+ program: TSESTree.Program,
180
+ imports: BullmqImports
181
+ ): WorkerDefinition[] {
182
+ const workers: WorkerDefinition[] = [];
183
+ const varToWorkerMap = new Map<string, TSESTree.NewExpression>();
184
+
185
+ walkAll(program, (node) => {
186
+ if (
187
+ node.type === AST_NODE_TYPES.VariableDeclarator &&
188
+ node.id.type === AST_NODE_TYPES.Identifier &&
189
+ node.init &&
190
+ isNewWorker(node.init, imports)
191
+ ) {
192
+ varToWorkerMap.set(node.id.name, node.init);
193
+ }
194
+ });
195
+
196
+ walkAll(program, (node) => {
197
+ if (!isNewWorker(node, imports)) {
198
+ return;
199
+ }
200
+
201
+ const bindingKey = workerBindingKey(node.parent);
202
+
203
+ workers.push({
204
+ bindingKey,
205
+ node,
206
+ });
207
+ });
208
+
209
+ return workers;
210
+ }
211
+
212
+ function workerBindingKey(parent: TSESTree.Node): string | null {
213
+ if (
214
+ parent.type === AST_NODE_TYPES.VariableDeclarator &&
215
+ parent.id.type === AST_NODE_TYPES.Identifier
216
+ ) {
217
+ return parent.id.name;
218
+ }
219
+
220
+ if (parent.type === AST_NODE_TYPES.PropertyDefinition) {
221
+ if (parent.key.type === AST_NODE_TYPES.Identifier && !parent.computed) {
222
+ return `this.${parent.key.name}`;
223
+ }
224
+
225
+ if (
226
+ parent.key.type === AST_NODE_TYPES.Literal &&
227
+ typeof parent.key.value === "string"
228
+ ) {
229
+ return `this.${parent.key.value}`;
230
+ }
231
+
232
+ return null;
233
+ }
234
+
235
+ if (parent.type === AST_NODE_TYPES.AssignmentExpression) {
236
+ if (parent.left.type === AST_NODE_TYPES.Identifier) {
237
+ return parent.left.name;
238
+ }
239
+
240
+ if (parent.left.type === AST_NODE_TYPES.MemberExpression) {
241
+ return getReceiverKey(parent.left);
242
+ }
243
+ }
244
+
245
+ return null;
246
+ }
247
+
248
+ export function isNewWorker(
249
+ node: TSESTree.Node,
250
+ imports: BullmqImports
251
+ ): node is TSESTree.NewExpression {
252
+ if (node.type !== AST_NODE_TYPES.NewExpression) {
253
+ return false;
254
+ }
255
+
256
+ if (node.callee.type !== AST_NODE_TYPES.Identifier) {
257
+ return false;
258
+ }
259
+
260
+ return imports.workerLocalNames.has(node.callee.name);
261
+ }
262
+
263
+ export function isQueueAddCall(node: TSESTree.CallExpression): boolean {
264
+ return (
265
+ node.callee.type === AST_NODE_TYPES.MemberExpression &&
266
+ node.callee.property.type === AST_NODE_TYPES.Identifier &&
267
+ node.callee.property.name === "add"
268
+ );
269
+ }
270
+
271
+ export function getCallReceiverKey(
272
+ node: TSESTree.CallExpression
273
+ ): string | null {
274
+ return getReceiverKey(node.callee);
275
+ }
276
+
277
+ export function getReceiverKey(callee: TSESTree.Node): string | null {
278
+ if (callee.type === AST_NODE_TYPES.MemberExpression) {
279
+ if (callee.object.type === AST_NODE_TYPES.Identifier) {
280
+ return callee.object.name;
281
+ }
282
+
283
+ if (
284
+ callee.object.type === AST_NODE_TYPES.MemberExpression &&
285
+ callee.object.property.type === AST_NODE_TYPES.Identifier
286
+ ) {
287
+ const base = getReceiverKey(callee.object);
288
+
289
+ if (base) {
290
+ return `${base}.${callee.object.property.name}`;
291
+ }
292
+ }
293
+ }
294
+
295
+ return null;
296
+ }
297
+
298
+ export function isQueueLikeReceiverName(name: string): boolean {
299
+ return /[Qq]ueue/.test(name);
300
+ }
301
+
302
+ export function getOptionsObjectArg(
303
+ node: TSESTree.NewExpression | TSESTree.CallExpression,
304
+ argIndex: number
305
+ ): TSESTree.ObjectExpression | null {
306
+ const arg = node.arguments[argIndex];
307
+
308
+ if (arg?.type === AST_NODE_TYPES.ObjectExpression) {
309
+ return arg;
310
+ }
311
+
312
+ return null;
313
+ }
314
+
315
+ export function findObjectProperty(
316
+ obj: TSESTree.ObjectExpression,
317
+ name: string
318
+ ): TSESTree.Property | null {
319
+ for (const prop of obj.properties) {
320
+ if (prop.type !== AST_NODE_TYPES.Property) {
321
+ continue;
322
+ }
323
+
324
+ if (prop.key.type === AST_NODE_TYPES.Identifier && prop.key.name === name) {
325
+ return prop;
326
+ }
327
+
328
+ if (prop.key.type === AST_NODE_TYPES.Literal && prop.key.value === name) {
329
+ return prop;
330
+ }
331
+ }
332
+
333
+ return null;
334
+ }
@@ -0,0 +1,25 @@
1
+ import type { TSESLint } from "@typescript-eslint/utils";
2
+
3
+ import { noBareDateNowRule } from "./rules/no-bare-date-now";
4
+ import { noTemplateTrimEmptyTernaryRule } from "./rules/no-template-trim-empty-ternary";
5
+ import { preferEarlyReturnRule } from "./rules/prefer-early-return";
6
+ import type { IRulePack } from "../rule-packs.types";
7
+
8
+ const rules: Record<string, TSESLint.RuleModule<string, readonly unknown[]>> = {
9
+ "no-bare-date-now": noBareDateNowRule,
10
+ "no-template-trim-empty-ternary": noTemplateTrimEmptyTernaryRule,
11
+ "prefer-early-return": preferEarlyReturnRule,
12
+ };
13
+
14
+ export const codeFlowPack: IRulePack = {
15
+ id: "code-flow",
16
+ description: "Control flow clarity and early returns",
17
+ rules,
18
+ rulesConfig: {
19
+ "no-bare-date-now": "error",
20
+ "no-template-trim-empty-ternary": "error",
21
+ "prefer-early-return": "error",
22
+ },
23
+ };
24
+
25
+ export default codeFlowPack;
@@ -0,0 +1,3 @@
1
+ export { noBareDateNowRule } from "./no-bare-date-now";
2
+ export { noTemplateTrimEmptyTernaryRule } from "./no-template-trim-empty-ternary";
3
+ export { preferEarlyReturnRule } from "./prefer-early-return";
@@ -0,0 +1,138 @@
1
+ import { AST_NODE_TYPES, type TSESTree } from "@typescript-eslint/utils";
2
+
3
+ import { createRule } from "../../create-rule";
4
+
5
+ export const RULE_NAME = "no-bare-date-now";
6
+
7
+ type MessageIds =
8
+ | "bareDateNow"
9
+ | "bareNewDate"
10
+ | "bareMathRandom"
11
+ | "bareDateConstructor";
12
+
13
+ export interface INoBareDateNowOptions {
14
+ readonly allowedPaths?: readonly string[];
15
+ }
16
+
17
+ const DEFAULTS: Required<INoBareDateNowOptions> = {
18
+ allowedPaths: [],
19
+ };
20
+
21
+ function fileMatchesAllowlist(
22
+ filename: string,
23
+ allowed: readonly string[]
24
+ ): boolean {
25
+ if (allowed.length === 0) {
26
+ return false;
27
+ }
28
+
29
+ const normalized = filename.replace(/\\/g, "/");
30
+
31
+ return allowed.some((segment) => normalized.includes(segment));
32
+ }
33
+
34
+ function isDateNow(node: TSESTree.CallExpression): boolean {
35
+ const callee = node.callee;
36
+
37
+ return (
38
+ callee.type === AST_NODE_TYPES.MemberExpression &&
39
+ callee.object.type === AST_NODE_TYPES.Identifier &&
40
+ callee.object.name === "Date" &&
41
+ callee.property.type === AST_NODE_TYPES.Identifier &&
42
+ callee.property.name === "now" &&
43
+ !callee.computed
44
+ );
45
+ }
46
+
47
+ function isMathRandom(node: TSESTree.CallExpression): boolean {
48
+ const callee = node.callee;
49
+
50
+ return (
51
+ callee.type === AST_NODE_TYPES.MemberExpression &&
52
+ callee.object.type === AST_NODE_TYPES.Identifier &&
53
+ callee.object.name === "Math" &&
54
+ callee.property.type === AST_NODE_TYPES.Identifier &&
55
+ callee.property.name === "random" &&
56
+ !callee.computed
57
+ );
58
+ }
59
+
60
+ function isBareDate(
61
+ node: TSESTree.NewExpression | TSESTree.CallExpression
62
+ ): boolean {
63
+ return (
64
+ node.callee.type === AST_NODE_TYPES.Identifier &&
65
+ node.callee.name === "Date" &&
66
+ node.arguments.length === 0
67
+ );
68
+ }
69
+
70
+ export const noBareDateNowRule = createRule<
71
+ [INoBareDateNowOptions],
72
+ MessageIds
73
+ >({
74
+ name: RULE_NAME,
75
+ meta: {
76
+ type: "problem",
77
+ docs: {
78
+ description:
79
+ "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.",
80
+ },
81
+ schema: [
82
+ {
83
+ type: "object",
84
+ properties: {
85
+ allowedPaths: {
86
+ type: "array",
87
+ items: { type: "string" },
88
+ },
89
+ },
90
+ additionalProperties: false,
91
+ },
92
+ ],
93
+ messages: {
94
+ bareDateNow:
95
+ "Direct `Date.now()` is non-deterministic. Import the project's `now()` util instead (or add the file to this rule's `allowedPaths` if it IS the util).",
96
+ bareNewDate:
97
+ "Direct `new Date()` (no args) is non-deterministic. Import the project's `now()` util and pass the millisecond timestamp explicitly, or add the file to `allowedPaths`.",
98
+ bareDateConstructor:
99
+ "Direct `Date()` (no args) is non-deterministic. Import the project's `now()` util and pass the millisecond timestamp explicitly, or add the file to `allowedPaths`.",
100
+ bareMathRandom:
101
+ "Direct `Math.random()` is non-deterministic. Import the project's random util (which can be seeded in tests) instead, or add the file to `allowedPaths`.",
102
+ },
103
+ },
104
+ defaultOptions: [DEFAULTS],
105
+ create(context, optionsArg) {
106
+ const options = optionsArg[0] ?? DEFAULTS;
107
+ const allowed = options.allowedPaths ?? DEFAULTS.allowedPaths;
108
+
109
+ if (fileMatchesAllowlist(context.filename, allowed)) {
110
+ return {};
111
+ }
112
+
113
+ return {
114
+ CallExpression(node: TSESTree.CallExpression) {
115
+ if (isDateNow(node)) {
116
+ context.report({ node, messageId: "bareDateNow" });
117
+
118
+ return;
119
+ }
120
+
121
+ if (isMathRandom(node)) {
122
+ context.report({ node, messageId: "bareMathRandom" });
123
+
124
+ return;
125
+ }
126
+
127
+ if (isBareDate(node)) {
128
+ context.report({ node, messageId: "bareDateConstructor" });
129
+ }
130
+ },
131
+ NewExpression(node: TSESTree.NewExpression) {
132
+ if (isBareDate(node)) {
133
+ context.report({ node, messageId: "bareNewDate" });
134
+ }
135
+ },
136
+ };
137
+ },
138
+ });
@@ -0,0 +1,87 @@
1
+ import { AST_NODE_TYPES, type TSESTree } from "@typescript-eslint/utils";
2
+
3
+ import { createRule } from "../../create-rule";
4
+
5
+ export const RULE_NAME = "no-template-trim-empty-ternary";
6
+
7
+ type MessageIds = "extractToUtil";
8
+
9
+ /**
10
+ * Bans the "build a string from a template literal, trim it, ternary
11
+ * against empty" pattern:
12
+ *
13
+ * `${first} ${last}`.trim() === "" ? email : `${first} ${last}`.trim()
14
+ *
15
+ * Patterns like this are testability traps: the same expression is
16
+ * built twice in the call site, no name documents the intent, and tests
17
+ * have to duplicate the construction to verify behaviour. Extract to a
18
+ * named util (e.g. `buildDisplayName({ first, last, fallback }))`) and
19
+ * test it in one place.
20
+ */
21
+ export const noTemplateTrimEmptyTernaryRule = createRule<[], MessageIds>({
22
+ name: RULE_NAME,
23
+ meta: {
24
+ type: "suggestion",
25
+ docs: {
26
+ description:
27
+ "Disallow inline `<template>.trim() === '' ? fallback : <template>.trim()` patterns. Extract to a named utility.",
28
+ },
29
+ schema: [],
30
+ messages: {
31
+ extractToUtil:
32
+ "Extract this `<template>.trim() === ''` fallback pattern to a named util (e.g. `buildDisplayName(...)`). Inline ternaries duplicate the expression and aren't unit-testable in one place.",
33
+ },
34
+ },
35
+ defaultOptions: [],
36
+ create(context) {
37
+ return {
38
+ ConditionalExpression(node) {
39
+ if (matchesTemplateTrimEmptyTest(node.test)) {
40
+ context.report({ node, messageId: "extractToUtil" });
41
+ }
42
+ },
43
+ };
44
+ },
45
+ });
46
+
47
+ function matchesTemplateTrimEmptyTest(test: TSESTree.Expression): boolean {
48
+ if (test.type !== AST_NODE_TYPES.BinaryExpression) {
49
+ return false;
50
+ }
51
+
52
+ if (test.operator !== "===" && test.operator !== "!==") {
53
+ return false;
54
+ }
55
+
56
+ return (
57
+ (isTrimCallOnTemplate(test.left) && isEmptyStringLiteral(test.right)) ||
58
+ (isEmptyStringLiteral(test.left) && isTrimCallOnTemplate(test.right))
59
+ );
60
+ }
61
+
62
+ function isTrimCallOnTemplate(node: TSESTree.Node): boolean {
63
+ if (node.type !== AST_NODE_TYPES.CallExpression) {
64
+ return false;
65
+ }
66
+
67
+ if (node.callee.type !== AST_NODE_TYPES.MemberExpression) {
68
+ return false;
69
+ }
70
+
71
+ if (
72
+ node.callee.property.type !== AST_NODE_TYPES.Identifier ||
73
+ node.callee.property.name !== "trim"
74
+ ) {
75
+ return false;
76
+ }
77
+
78
+ return node.callee.object.type === AST_NODE_TYPES.TemplateLiteral;
79
+ }
80
+
81
+ function isEmptyStringLiteral(node: TSESTree.Node): boolean {
82
+ return (
83
+ node.type === AST_NODE_TYPES.Literal &&
84
+ typeof node.value === "string" &&
85
+ node.value === ""
86
+ );
87
+ }
@@ -0,0 +1,80 @@
1
+ import type { TSESTree } from "@typescript-eslint/utils";
2
+ import type { RuleFixer } from "@typescript-eslint/utils/ts-eslint";
3
+
4
+ import { createRule } from "../../create-rule";
5
+ import {
6
+ buildGuardClauseReplacement,
7
+ findWrappedHappyPathIf,
8
+ getFunctionBlockBody,
9
+ } from "../utils/prefer-early-return";
10
+
11
+ export const RULE_NAME = "prefer-early-return";
12
+
13
+ type MessageIds = "preferEarlyReturn";
14
+
15
+ export const preferEarlyReturnRule = createRule<[], MessageIds>({
16
+ name: RULE_NAME,
17
+ meta: {
18
+ type: "problem",
19
+ docs: {
20
+ description:
21
+ "Prefer guard clauses (early return) over wrapping the function body in a multi-statement `if` without an `else`.",
22
+ },
23
+ schema: [],
24
+ messages: {
25
+ preferEarlyReturn:
26
+ "Use a guard clause (early return) instead of wrapping the function body in an `if`. Invert the condition and return early so the happy path stays at the top level.",
27
+ },
28
+ hasSuggestions: true,
29
+ },
30
+ defaultOptions: [],
31
+ create(context) {
32
+ const sourceCode = context.sourceCode;
33
+
34
+ function reportWrappedHappyPath(ifStatement: TSESTree.IfStatement): void {
35
+ const replacement = buildGuardClauseReplacement(sourceCode, ifStatement);
36
+ const suggest =
37
+ replacement === null
38
+ ? []
39
+ : [
40
+ {
41
+ messageId: "preferEarlyReturn" as const,
42
+ fix(fixer: RuleFixer) {
43
+ return fixer.replaceText(ifStatement, replacement);
44
+ },
45
+ },
46
+ ];
47
+
48
+ context.report({
49
+ node: ifStatement,
50
+ messageId: "preferEarlyReturn",
51
+ suggest,
52
+ });
53
+ }
54
+
55
+ function checkFunctionBody(
56
+ node:
57
+ | TSESTree.FunctionDeclaration
58
+ | TSESTree.FunctionExpression
59
+ | TSESTree.ArrowFunctionExpression
60
+ ): void {
61
+ const body = getFunctionBlockBody(node);
62
+
63
+ if (body === null) {
64
+ return;
65
+ }
66
+
67
+ const wrappedIf = findWrappedHappyPathIf(body);
68
+
69
+ if (wrappedIf !== null) {
70
+ reportWrappedHappyPath(wrappedIf);
71
+ }
72
+ }
73
+
74
+ return {
75
+ FunctionDeclaration: checkFunctionBody,
76
+ FunctionExpression: checkFunctionBody,
77
+ ArrowFunctionExpression: checkFunctionBody,
78
+ };
79
+ },
80
+ });