@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,312 @@
1
+ import { AST_NODE_TYPES, type TSESTree } from "@typescript-eslint/utils";
2
+ import type { JSONSchema4 } from "@typescript-eslint/utils/json-schema";
3
+
4
+ import { createRule } from "../../create-rule";
5
+ import { isPgTableCall } from "../utils";
6
+
7
+ export const RULE_NAME = "tables-must-have-timestamps";
8
+
9
+ export interface TablesMustHaveTimestampsOptions {
10
+ readonly requireColumns?: readonly string[];
11
+ readonly requireOnUpdate?: readonly string[];
12
+ readonly ignoreTablePattern?: string;
13
+ }
14
+
15
+ type RuleOptions = [TablesMustHaveTimestampsOptions];
16
+ type MessageIds = "missingTimestamp" | "missingOnUpdate";
17
+
18
+ const DEFAULT_REQUIRE_COLUMNS = ["createdAt"] as const;
19
+ const DEFAULT_REQUIRE_ON_UPDATE: readonly string[] = [];
20
+ const ON_UPDATE_METHODS = new Set(["$onUpdate", "$onUpdateFn"]);
21
+
22
+ const optionSchema: JSONSchema4 = {
23
+ type: "object",
24
+ additionalProperties: false,
25
+ properties: {
26
+ requireColumns: {
27
+ type: "array",
28
+ items: {
29
+ type: "string",
30
+ },
31
+ uniqueItems: true,
32
+ },
33
+ requireOnUpdate: {
34
+ type: "array",
35
+ items: {
36
+ type: "string",
37
+ },
38
+ uniqueItems: true,
39
+ },
40
+ ignoreTablePattern: {
41
+ type: "string",
42
+ },
43
+ },
44
+ };
45
+
46
+ export const tablesMustHaveTimestampsRule = createRule<RuleOptions, MessageIds>(
47
+ {
48
+ name: RULE_NAME,
49
+ meta: {
50
+ type: "suggestion",
51
+ docs: {
52
+ description:
53
+ "Require Drizzle tables to declare standard timestamp columns (createdAt by default).",
54
+ },
55
+ schema: [optionSchema],
56
+ messages: {
57
+ missingTimestamp:
58
+ "Table '{{name}}' missing required column(s): {{missing}}.",
59
+ missingOnUpdate:
60
+ "Column '{{column}}' on table '{{name}}' is in `requireOnUpdate` but its `timestamp(...)` chain does not include `.$onUpdate(...)` — without it, the column will not auto-update on row mutations.",
61
+ },
62
+ },
63
+ defaultOptions: [
64
+ {
65
+ requireColumns: [...DEFAULT_REQUIRE_COLUMNS],
66
+ requireOnUpdate: [...DEFAULT_REQUIRE_ON_UPDATE],
67
+ },
68
+ ],
69
+ create(context, [options]) {
70
+ const requireColumns = options.requireColumns ?? DEFAULT_REQUIRE_COLUMNS;
71
+ const requireOnUpdate =
72
+ options.requireOnUpdate ?? DEFAULT_REQUIRE_ON_UPDATE;
73
+ const ignorePattern = compilePattern(options.ignoreTablePattern);
74
+
75
+ return {
76
+ VariableDeclarator(node) {
77
+ if (node.init?.type !== AST_NODE_TYPES.CallExpression) {
78
+ return;
79
+ }
80
+
81
+ if (!isPgTableCall(node.init)) {
82
+ return;
83
+ }
84
+
85
+ const tableName = getTableName(node);
86
+
87
+ if (!tableName) {
88
+ return;
89
+ }
90
+
91
+ if (ignorePattern?.test(tableName)) {
92
+ return;
93
+ }
94
+
95
+ const columnsArg = node.init.arguments[1];
96
+
97
+ const definedColumns =
98
+ columnsArg?.type === AST_NODE_TYPES.ObjectExpression
99
+ ? columnsArg
100
+ : null;
101
+
102
+ const reportNode =
103
+ node.id.type === AST_NODE_TYPES.Identifier ? node.id : node;
104
+
105
+ if (requireColumns.length > 0) {
106
+ const missing = requireColumns.filter(
107
+ (column) => !hasTimestampColumn(definedColumns, column)
108
+ );
109
+
110
+ if (missing.length > 0) {
111
+ context.report({
112
+ node: reportNode,
113
+ messageId: "missingTimestamp",
114
+ data: {
115
+ name: tableName,
116
+ missing: missing.join(", "),
117
+ },
118
+ });
119
+ }
120
+ }
121
+
122
+ if (definedColumns) {
123
+ for (const violation of findOnUpdateViolations(
124
+ definedColumns,
125
+ requireOnUpdate
126
+ )) {
127
+ context.report({
128
+ node: violation.property,
129
+ messageId: "missingOnUpdate",
130
+ data: {
131
+ name: tableName,
132
+ column: violation.column,
133
+ },
134
+ });
135
+ }
136
+ }
137
+ },
138
+ };
139
+ },
140
+ }
141
+ );
142
+
143
+ function compilePattern(source: string | undefined): RegExp | null {
144
+ if (!source) {
145
+ return null;
146
+ }
147
+
148
+ try {
149
+ return new RegExp(source);
150
+ } catch {
151
+ return null;
152
+ }
153
+ }
154
+
155
+ function getTableName(node: TSESTree.VariableDeclarator): string | null {
156
+ if (node.id.type === AST_NODE_TYPES.Identifier) {
157
+ return node.id.name;
158
+ }
159
+
160
+ if (node.init?.type === AST_NODE_TYPES.CallExpression) {
161
+ const firstArg = node.init.arguments[0];
162
+
163
+ if (
164
+ firstArg?.type === AST_NODE_TYPES.Literal &&
165
+ typeof firstArg.value === "string"
166
+ ) {
167
+ return firstArg.value;
168
+ }
169
+ }
170
+
171
+ return null;
172
+ }
173
+
174
+ function findTimestampProperty(
175
+ columns: TSESTree.ObjectExpression,
176
+ columnName: string
177
+ ): TSESTree.Property | null {
178
+ for (const property of columns.properties) {
179
+ if (property.type !== AST_NODE_TYPES.Property) {
180
+ continue;
181
+ }
182
+
183
+ if (!matchesPropertyKey(property, columnName)) {
184
+ continue;
185
+ }
186
+
187
+ if (
188
+ property.value.type === AST_NODE_TYPES.CallExpression &&
189
+ isTimestampInitializer(property.value)
190
+ ) {
191
+ return property;
192
+ }
193
+ }
194
+
195
+ return null;
196
+ }
197
+
198
+ function matchesPropertyKey(
199
+ property: TSESTree.Property,
200
+ name: string
201
+ ): boolean {
202
+ if (
203
+ property.key.type === AST_NODE_TYPES.Identifier &&
204
+ property.key.name === name
205
+ ) {
206
+ return true;
207
+ }
208
+
209
+ if (
210
+ property.key.type === AST_NODE_TYPES.Literal &&
211
+ property.key.value === name
212
+ ) {
213
+ return true;
214
+ }
215
+
216
+ return false;
217
+ }
218
+
219
+ function isTimestampInitializer(node: TSESTree.CallExpression): boolean {
220
+ const calleeName = getCalleeIdentifierName(node);
221
+
222
+ return calleeName === "timestamp";
223
+ }
224
+
225
+ function getCalleeIdentifierName(node: TSESTree.CallExpression): string | null {
226
+ if (node.callee.type === AST_NODE_TYPES.Identifier) {
227
+ return node.callee.name;
228
+ }
229
+
230
+ if (
231
+ node.callee.type === AST_NODE_TYPES.MemberExpression &&
232
+ node.callee.property.type === AST_NODE_TYPES.Identifier
233
+ ) {
234
+ return node.callee.property.name;
235
+ }
236
+
237
+ return null;
238
+ }
239
+
240
+ function chainHasOnUpdate(startCall: TSESTree.CallExpression): boolean {
241
+ let current: TSESTree.Node = startCall;
242
+ let parent = getParent(current);
243
+
244
+ while (parent !== undefined) {
245
+ if (
246
+ parent.type === AST_NODE_TYPES.MemberExpression &&
247
+ parent.object === current &&
248
+ parent.property.type === AST_NODE_TYPES.Identifier &&
249
+ ON_UPDATE_METHODS.has(parent.property.name)
250
+ ) {
251
+ const methodCall = getParent(parent);
252
+
253
+ if (methodCall?.type === AST_NODE_TYPES.CallExpression) {
254
+ return true;
255
+ }
256
+ }
257
+
258
+ if (
259
+ parent.type === AST_NODE_TYPES.MemberExpression ||
260
+ parent.type === AST_NODE_TYPES.CallExpression
261
+ ) {
262
+ current = parent;
263
+ parent = getParent(current);
264
+
265
+ continue;
266
+ }
267
+
268
+ break;
269
+ }
270
+
271
+ return false;
272
+ }
273
+
274
+ function getParent(node: TSESTree.Node): TSESTree.Node | undefined {
275
+ return node.parent;
276
+ }
277
+
278
+ /** Columns from `requireOnUpdate` whose timestamp chain lacks `.$onUpdate(...)`. */
279
+ function findOnUpdateViolations(
280
+ definedColumns: TSESTree.ObjectExpression,
281
+ requireOnUpdate: readonly string[]
282
+ ): { property: TSESTree.Property; column: string }[] {
283
+ const violations: { property: TSESTree.Property; column: string }[] = [];
284
+
285
+ for (const column of requireOnUpdate) {
286
+ const property = findTimestampProperty(definedColumns, column);
287
+
288
+ if (!property) {
289
+ continue;
290
+ }
291
+
292
+ if (
293
+ property.value.type === AST_NODE_TYPES.CallExpression &&
294
+ !chainHasOnUpdate(property.value)
295
+ ) {
296
+ violations.push({ property, column });
297
+ }
298
+ }
299
+
300
+ return violations;
301
+ }
302
+
303
+ function hasTimestampColumn(
304
+ columns: TSESTree.ObjectExpression | null,
305
+ columnName: string
306
+ ): boolean {
307
+ if (!columns) {
308
+ return false;
309
+ }
310
+
311
+ return findTimestampProperty(columns, columnName) !== null;
312
+ }
@@ -0,0 +1,166 @@
1
+ import { AST_NODE_TYPES, type TSESTree } from "@typescript-eslint/utils";
2
+ import type { JSONSchema4 } from "@typescript-eslint/utils/json-schema";
3
+
4
+ import { createRule } from "../../create-rule";
5
+
6
+ export const RULE_NAME = "timestamp-must-specify-mode";
7
+
8
+ export interface TimestampMustSpecifyModeOptions {
9
+ readonly allowedModes?: readonly ("date" | "string")[];
10
+ }
11
+
12
+ type RuleOptions = [TimestampMustSpecifyModeOptions];
13
+ type MessageIds = "missingMode" | "invalidMode";
14
+
15
+ const DEFAULT_ALLOWED_MODES = ["date", "string"] as const;
16
+
17
+ const optionSchema: JSONSchema4 = {
18
+ type: "object",
19
+ additionalProperties: false,
20
+ properties: {
21
+ allowedModes: {
22
+ type: "array",
23
+ uniqueItems: true,
24
+ items: {
25
+ type: "string",
26
+ enum: ["date", "string"],
27
+ },
28
+ minItems: 1,
29
+ },
30
+ },
31
+ };
32
+
33
+ export const timestampMustSpecifyModeRule = createRule<RuleOptions, MessageIds>(
34
+ {
35
+ name: RULE_NAME,
36
+ meta: {
37
+ type: "problem",
38
+ docs: {
39
+ description:
40
+ "Require every Drizzle timestamp(...) call to explicitly set `mode: 'date'` or `mode: 'string'`.",
41
+ },
42
+ schema: [optionSchema],
43
+ messages: {
44
+ missingMode:
45
+ "timestamp(...) call does not specify `mode` — pass `{ mode: 'date' }` or `{ mode: 'string' }` so return types are deterministic across drivers.",
46
+ invalidMode:
47
+ "timestamp(...) `mode` must be one of: {{allowed}}. Got: {{actual}}.",
48
+ },
49
+ },
50
+ defaultOptions: [{ allowedModes: [...DEFAULT_ALLOWED_MODES] }],
51
+ create(context, [options]) {
52
+ const allowedModes = options.allowedModes ?? DEFAULT_ALLOWED_MODES;
53
+
54
+ return {
55
+ CallExpression(node) {
56
+ if (!isTimestampCallee(node)) {
57
+ return;
58
+ }
59
+
60
+ const optionsArg = node.arguments[1];
61
+
62
+ if (optionsArg?.type !== AST_NODE_TYPES.ObjectExpression) {
63
+ context.report({ node, messageId: "missingMode" });
64
+
65
+ return;
66
+ }
67
+
68
+ const modeProperty = findModeProperty(optionsArg);
69
+
70
+ if (!modeProperty) {
71
+ context.report({ node: optionsArg, messageId: "missingMode" });
72
+
73
+ return;
74
+ }
75
+
76
+ if (
77
+ modeProperty.value.type !== AST_NODE_TYPES.Literal ||
78
+ typeof modeProperty.value.value !== "string"
79
+ ) {
80
+ context.report({
81
+ node: modeProperty,
82
+ messageId: "invalidMode",
83
+ data: {
84
+ allowed: allowedModes.join(" | "),
85
+ actual: "<non-literal>",
86
+ },
87
+ });
88
+
89
+ return;
90
+ }
91
+
92
+ const modeValue = modeProperty.value.value;
93
+
94
+ if (!allowedModesIncludes(allowedModes, modeValue)) {
95
+ context.report({
96
+ node: modeProperty,
97
+ messageId: "invalidMode",
98
+ data: {
99
+ allowed: allowedModes.join(" | "),
100
+ actual: modeValue,
101
+ },
102
+ });
103
+ }
104
+ },
105
+ };
106
+ },
107
+ }
108
+ );
109
+
110
+ function isTimestampCallee(node: TSESTree.CallExpression): boolean {
111
+ if (
112
+ node.callee.type === AST_NODE_TYPES.Identifier &&
113
+ node.callee.name === "timestamp"
114
+ ) {
115
+ return true;
116
+ }
117
+
118
+ if (
119
+ node.callee.type === AST_NODE_TYPES.MemberExpression &&
120
+ node.callee.property.type === AST_NODE_TYPES.Identifier &&
121
+ node.callee.property.name === "timestamp"
122
+ ) {
123
+ return true;
124
+ }
125
+
126
+ return false;
127
+ }
128
+
129
+ function findModeProperty(
130
+ obj: TSESTree.ObjectExpression
131
+ ): TSESTree.Property | null {
132
+ for (const property of obj.properties) {
133
+ if (property.type !== AST_NODE_TYPES.Property) {
134
+ continue;
135
+ }
136
+
137
+ if (propertyKeyMatches(property, "mode")) {
138
+ return property;
139
+ }
140
+ }
141
+
142
+ return null;
143
+ }
144
+
145
+ function propertyKeyMatches(
146
+ property: TSESTree.Property,
147
+ name: string
148
+ ): boolean {
149
+ if (
150
+ property.key.type === AST_NODE_TYPES.Identifier &&
151
+ property.key.name === name
152
+ ) {
153
+ return true;
154
+ }
155
+
156
+ return (
157
+ property.key.type === AST_NODE_TYPES.Literal && property.key.value === name
158
+ );
159
+ }
160
+
161
+ function allowedModesIncludes(
162
+ allowedModes: readonly string[],
163
+ value: string
164
+ ): boolean {
165
+ return allowedModes.includes(value);
166
+ }
@@ -0,0 +1,115 @@
1
+ import { AST_NODE_TYPES, type TSESTree } from "@typescript-eslint/utils";
2
+
3
+ import { walkAll } from "../utils";
4
+
5
+ /**
6
+ * Helper utilities for Drizzle rules.
7
+ */
8
+
9
+ export function isPgTableCall(node: TSESTree.CallExpression): boolean {
10
+ if (node.callee.type !== AST_NODE_TYPES.Identifier) {
11
+ return false;
12
+ }
13
+
14
+ const name = node.callee.name;
15
+
16
+ return name === "pgTable" || name === "pgTableCreator";
17
+ }
18
+
19
+ export function isRelationsCall(node: TSESTree.CallExpression): boolean {
20
+ if (node.callee.type !== AST_NODE_TYPES.Identifier) {
21
+ return false;
22
+ }
23
+
24
+ return node.callee.name === "relations";
25
+ }
26
+
27
+ export function isForeignKeyCall(node: TSESTree.CallExpression): boolean {
28
+ if (node.callee.type !== AST_NODE_TYPES.Identifier) {
29
+ return false;
30
+ }
31
+
32
+ return node.callee.name === "foreignKey";
33
+ }
34
+
35
+ export function isSchemaBuilderCall(node: TSESTree.CallExpression): boolean {
36
+ const calleeName = getCalleeIdentifierName(node);
37
+
38
+ if (!calleeName) {
39
+ return false;
40
+ }
41
+
42
+ // Drizzle schema builders: tables, relations, indices, checks, uniqueConstraints, etc.
43
+ const schemaBuilders = new Set([
44
+ "pgTable",
45
+ "pgTableCreator",
46
+ "relations",
47
+ "index",
48
+ "uniqueIndex",
49
+ "primaryKey",
50
+ "foreignKey",
51
+ "check",
52
+ "unique",
53
+ "schema",
54
+ "serial",
55
+ "smallserial",
56
+ "bigserial",
57
+ "varchar",
58
+ "char",
59
+ "text",
60
+ "integer",
61
+ "smallint",
62
+ "bigint",
63
+ "decimal",
64
+ "numeric",
65
+ "real",
66
+ "doublePrecision",
67
+ "boolean",
68
+ "date",
69
+ "time",
70
+ "timestamp",
71
+ "interval",
72
+ "json",
73
+ "jsonb",
74
+ "uuid",
75
+ "bytea",
76
+ "citext",
77
+ "inet",
78
+ "array",
79
+ "enum",
80
+ "geometry",
81
+ "geography",
82
+ ]);
83
+
84
+ return schemaBuilders.has(calleeName);
85
+ }
86
+
87
+ function getCalleeIdentifierName(node: TSESTree.CallExpression): string | null {
88
+ if (node.callee.type === AST_NODE_TYPES.Identifier) {
89
+ return node.callee.name;
90
+ }
91
+
92
+ if (
93
+ node.callee.type === AST_NODE_TYPES.MemberExpression &&
94
+ node.callee.property.type === AST_NODE_TYPES.Identifier
95
+ ) {
96
+ return node.callee.property.name;
97
+ }
98
+
99
+ return null;
100
+ }
101
+
102
+ export function findCallExpressionsDeep(
103
+ root: TSESTree.Node,
104
+ predicate: (node: TSESTree.CallExpression) => boolean
105
+ ): TSESTree.CallExpression[] {
106
+ const results: TSESTree.CallExpression[] = [];
107
+
108
+ walkAll(root, (node) => {
109
+ if (node.type === AST_NODE_TYPES.CallExpression && predicate(node)) {
110
+ results.push(node);
111
+ }
112
+ });
113
+
114
+ return results;
115
+ }
@@ -0,0 +1,43 @@
1
+ import type { TSESLint } from "@typescript-eslint/utils";
2
+
3
+ import { consistentStatusViaSetRule } from "./rules/consistent-status-via-set";
4
+ import { noDecorateStateCollisionRule } from "./rules/no-decorate-state-collision";
5
+ import { noSeparateModelInterfacesRule } from "./rules/no-separate-model-interfaces";
6
+ import { preferDestructuredContextRule } from "./rules/prefer-destructured-context";
7
+ import { preferDirectReturnRule } from "./rules/prefer-direct-return";
8
+ import { preferStaticServicesRule } from "./rules/prefer-static-services";
9
+ import { preferThrowStatusRule } from "./rules/prefer-throw-status";
10
+ import { requireHooksBeforeRoutesRule } from "./rules/require-hooks-before-routes";
11
+ import { requirePluginNameRule } from "./rules/require-plugin-name";
12
+ import type { IRulePack } from "../rule-packs.types";
13
+
14
+ const rules: Record<string, TSESLint.RuleModule<string, readonly unknown[]>> = {
15
+ "consistent-status-via-set": consistentStatusViaSetRule,
16
+ "no-decorate-state-collision": noDecorateStateCollisionRule,
17
+ "no-separate-model-interfaces": noSeparateModelInterfacesRule,
18
+ "prefer-destructured-context": preferDestructuredContextRule,
19
+ "prefer-direct-return": preferDirectReturnRule,
20
+ "prefer-static-services": preferStaticServicesRule,
21
+ "prefer-throw-status": preferThrowStatusRule,
22
+ "require-hooks-before-routes": requireHooksBeforeRoutesRule,
23
+ "require-plugin-name": requirePluginNameRule,
24
+ };
25
+
26
+ export const elysiaPack: IRulePack = {
27
+ id: "elysia",
28
+ description: "Elysia framework best practices and type-safety patterns",
29
+ rules,
30
+ rulesConfig: {
31
+ "consistent-status-via-set": "error",
32
+ "no-decorate-state-collision": "error",
33
+ "no-separate-model-interfaces": "warn",
34
+ "prefer-destructured-context": "warn",
35
+ "prefer-direct-return": "warn",
36
+ "prefer-static-services": "warn",
37
+ "prefer-throw-status": "warn",
38
+ "require-hooks-before-routes": "error",
39
+ "require-plugin-name": "error",
40
+ },
41
+ };
42
+
43
+ export default elysiaPack;
@@ -0,0 +1,69 @@
1
+ import { AST_NODE_TYPES } from "@typescript-eslint/utils";
2
+
3
+ import { createRule } from "../../create-rule";
4
+ import {
5
+ collectElysiaVariables,
6
+ findEnclosingRouteHandler,
7
+ findObjectProperty,
8
+ } from "../utils/elysiaChain";
9
+
10
+ export const RULE_NAME = "consistent-status-via-set";
11
+
12
+ type RuleOptions = [];
13
+ type MessageIds = "useSetStatus";
14
+
15
+ export const consistentStatusViaSetRule = createRule<RuleOptions, MessageIds>({
16
+ name: RULE_NAME,
17
+ meta: {
18
+ type: "problem",
19
+ docs: {
20
+ description:
21
+ "Inside Elysia route handlers, set HTTP status via `set.status = N`, not by returning a `new Response(body, { status: N })`.",
22
+ },
23
+ schema: [],
24
+ messages: {
25
+ useSetStatus:
26
+ "Do not return `new Response(body, { status })` from an Elysia route handler — assign `set.status = N` and return the body directly.",
27
+ },
28
+ },
29
+ defaultOptions: [],
30
+ create(context) {
31
+ let elysiaVars = new Set<string>();
32
+
33
+ return {
34
+ Program(program) {
35
+ elysiaVars = collectElysiaVariables(program);
36
+ },
37
+ ReturnStatement(node) {
38
+ if (node.argument?.type !== AST_NODE_TYPES.NewExpression) {
39
+ return;
40
+ }
41
+
42
+ const newExpr = node.argument;
43
+
44
+ if (
45
+ newExpr.callee.type !== AST_NODE_TYPES.Identifier ||
46
+ newExpr.callee.name !== "Response"
47
+ ) {
48
+ return;
49
+ }
50
+
51
+ const optionsArg = newExpr.arguments[1];
52
+
53
+ if (optionsArg?.type !== AST_NODE_TYPES.ObjectExpression) {
54
+ return;
55
+ }
56
+
57
+ if (!findObjectProperty(optionsArg, "status")) {
58
+ return;
59
+ }
60
+
61
+ if (!findEnclosingRouteHandler(node, elysiaVars)) {
62
+ return;
63
+ }
64
+
65
+ context.report({ node: newExpr, messageId: "useSetStatus" });
66
+ },
67
+ };
68
+ },
69
+ });