@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,367 @@
1
+ {
2
+ "@typescript-eslint/no-explicit-any": {
3
+ "what": "Disallow the `any` type.",
4
+ "bad": "const age: any = 'seventeen';",
5
+ "good": "const age: number = 17;"
6
+ },
7
+ "@typescript-eslint/no-unsafe-argument": {
8
+ "what": "Disallow calling a function with a value with type `any`.",
9
+ "bad": "declare function foo(arg1: string, arg2: number, arg3: string): void;\n\nconst anyTyped = 1 as any;\n\nfoo(...anyTyped);\nfoo(anyTyped, 1, 'a');\n\nconst anyArray: any[] = [];",
10
+ "good": "declare function foo(arg1: string, arg2: number, arg3: string): void;\n\nfoo('a', 1, 'b');\n\nconst tuple1 = ['a', 1, 'b'] as const;\nfoo(...tuple1);\n\ndeclare function bar(arg1: string, arg2: number, ...rest: string[]): void;"
11
+ },
12
+ "@typescript-eslint/no-unsafe-assignment": {
13
+ "what": "Disallow assigning a value with type `any` to variables and properties.",
14
+ "bad": "const x = 1 as any,\n y = 1 as any;\nconst [x] = 1 as any;\nconst [x] = [] as any[];\nconst [x] = [1 as any];\n[x] = [1] as [any];\n\nfunction foo(a = 1 as any) {}",
15
+ "good": "const x = 1,\n y = 1;\nconst [x] = [1];\n[x] = [1] as [number];\n\nfunction foo(a = 1) {}\nclass Foo {\n constructor(private a = 1) {}"
16
+ },
17
+ "@typescript-eslint/no-unsafe-call": {
18
+ "what": "Disallow calling a value with type `any`.",
19
+ "bad": "declare const anyVar: any;\ndeclare const nestedAny: { prop: any };\n\nanyVar();\nanyVar.a.b();\n\nnestedAny.prop();\nnestedAny.prop['a']();",
20
+ "good": "declare const typedVar: () => void;\ndeclare const typedNested: { prop: { a: () => void } };\n\ntypedVar();\ntypedNested.prop.a();\n\n(() => {})();\n"
21
+ },
22
+ "@typescript-eslint/no-unsafe-member-access": {
23
+ "what": "Disallow member access on a value with type `any`.",
24
+ "bad": "declare const anyVar: any;\ndeclare const nestedAny: { prop: any };\n\nanyVar.a;\nanyVar.a.b;\nanyVar['a'];\nanyVar['a']['b'];\n",
25
+ "good": "declare const properlyTyped: { prop: { a: string } };\n\nproperlyTyped.prop.a;\nproperlyTyped.prop['a'];\n\nconst key = 'a';\nproperlyTyped.prop[key];\n"
26
+ },
27
+ "@typescript-eslint/no-unsafe-return": {
28
+ "what": "Disallow returning a value with type `any` from a function.",
29
+ "bad": "function foo1() {\n return 1 as any;\n}\nfunction foo2() {\n return Object.create(null);\n}\nconst foo3 = () => {\n return 1 as any;",
30
+ "good": "function foo1() {\n return 1;\n}\nfunction foo2() {\n return Object.create(null) as Record<string, unknown>;\n}\n\nconst foo3 = () => [];"
31
+ },
32
+ "@typescript-eslint/no-non-null-assertion": {
33
+ "what": "Disallow non-null assertions using the `!` postfix operator.",
34
+ "bad": "interface Example {\n property?: string;\n}\n\ndeclare const example: Example;\nconst includesBaz = example.property!.includes('baz');",
35
+ "good": "interface Example {\n property?: string;\n}\n\ndeclare const example: Example;\nconst includesBaz = example.property?.includes('baz') ?? false;"
36
+ },
37
+ "@typescript-eslint/restrict-plus-operands": {
38
+ "what": "Require both operands of addition to be the same type and be `bigint`, `number`, or `string`.",
39
+ "bad": "let foo = 1n + 1;\nlet fn = (a: string, b: never) => a + b;",
40
+ "good": "let foo = 1n + 1n;\nlet fn = (a: string, b: string) => a + b;"
41
+ },
42
+ "@typescript-eslint/restrict-template-expressions": {
43
+ "what": "Enforce template literal expressions to be of `string` type.",
44
+ "bad": "const arg1 = [1, 2];\nconst msg1 = `arg1 = ${arg1}`;\n\nconst arg2 = { name: 'Foo' };\nconst msg2 = `arg2 = ${arg2 || null}`;",
45
+ "good": "const arg = 'foo';\nconst msg1 = `arg = ${arg}`;\nconst msg2 = `arg = ${arg || 'default'}`;\n\nconst stringWithKindProp: string & { _kind?: 'MyString' } = 'foo';\nconst msg3 = `stringWithKindProp = ${stringWithKindProp}`;"
46
+ },
47
+ "@typescript-eslint/no-floating-promises": {
48
+ "what": "Require Promise-like statements to be handled appropriately.",
49
+ "bad": "const promise = new Promise((resolve, reject) => resolve('value'));\npromise;\n\nasync function returnsPromise() {\n return 'value';\n}\nreturnsPromise().then(() => {});\n",
50
+ "good": "const promise = new Promise((resolve, reject) => resolve('value'));\nawait promise;\n\nasync function returnsPromise() {\n return 'value';\n}\n\nvoid returnsPromise();"
51
+ },
52
+ "@typescript-eslint/await-thenable": {
53
+ "what": "Disallow awaiting a value that is not a Thenable.",
54
+ "bad": "await 'value';\n\nconst createValue = () => 'value';\nawait createValue();",
55
+ "good": "await Promise.resolve('value');\n\nconst createValue = async () => 'value';\nawait createValue();"
56
+ },
57
+ "@typescript-eslint/no-for-in-array": {
58
+ "what": "Disallow iterating over an array with a for-in loop.",
59
+ "bad": "declare const array: string[];\n\nfor (const i in array) {\n console.log(array[i]);\n}\n\nfor (const i in array) {\n console.log(i, array[i]);",
60
+ "good": "declare const array: string[];\n\nfor (const value of array) {\n console.log(value);\n}\n\nfor (let i = 0; i < array.length; i += 1) {\n console.log(i, array[i]);"
61
+ },
62
+ "@typescript-eslint/prefer-nullish-coalescing": {
63
+ "what": "Enforce using the nullish coalescing operator instead of logical assignments or chaining.",
64
+ "bad": "declare const a: string | null;\ndeclare const b: string | null;\n\nconst c = a || b;\n\ndeclare let foo: { a: string } | null;\ndeclare function makeFoo(): { a: string };\n",
65
+ "good": "declare const a: string | null;\ndeclare const b: string | null;\n\nconst c = a ?? b;\n\ndeclare let foo: { a: string } | null;\ndeclare function makeFoo(): { a: string };\n"
66
+ },
67
+ "@typescript-eslint/prefer-optional-chain": {
68
+ "what": "Enforce using concise optional chain expressions instead of chained logical ands, negated logical ors, or empty objects.",
69
+ "bad": "foo && foo.a && foo.a.b && foo.a.b.c;\nfoo && foo['a'] && foo['a'].b && foo['a'].b.c;\nfoo && foo.a && foo.a.b && foo.a.b.method && foo.a.b.method();\n\n// With empty objects\n(((foo || {}).a || {}).b || {}).c;\n(((foo || {})['a'] || {}).b || {}).c;\n",
70
+ "good": "foo?.a?.b?.c;\nfoo?.['a']?.b?.c;\nfoo?.a?.b?.method?.();\n\nfoo?.a?.b?.c?.d?.e;\n\n!foo?.bar;\n!foo?.[bar];"
71
+ },
72
+ "@typescript-eslint/no-unnecessary-condition": {
73
+ "what": "Disallow conditionals where the type is always truthy or always falsy.",
74
+ "bad": "function head<T>(items: T[]) {\n // items can never be nullable, so this is unnecessary\n if (items) {\n return items[0].toUpperCase();\n }\n}\n\nfunction foo(arg: 'bar' | 'baz') {",
75
+ "good": "function head<T>(items: T[]) {\n // Necessary, since items.length might be 0\n if (items.length) {\n return items[0].toUpperCase();\n }\n}\n\nfunction foo(arg: string) {"
76
+ },
77
+ "@typescript-eslint/no-unnecessary-type-assertion": {
78
+ "what": "Disallow type assertions that do not change the type of an expression.",
79
+ "bad": "const foo = 3;\nconst bar = foo!;",
80
+ "good": "const foo = <number>3;"
81
+ },
82
+ "@typescript-eslint/switch-exhaustiveness-check": {
83
+ "what": "Require switch-case statements to be exhaustive.",
84
+ "bad": "type Day =\n | 'Monday'\n | 'Tuesday'\n | 'Wednesday'\n | 'Thursday'\n | 'Friday'\n | 'Saturday'\n | 'Sunday';",
85
+ "good": "type Day =\n | 'Monday'\n | 'Tuesday'\n | 'Wednesday'\n | 'Thursday'\n | 'Friday'\n | 'Saturday'\n | 'Sunday';"
86
+ },
87
+ "@typescript-eslint/no-base-to-string": {
88
+ "what": "Require `.toString()` and `.toLocaleString()` to only be called on objects which provide useful information when stringified.",
89
+ "bad": "// Passing an object or class instance to string concatenation:\n'' + {};\n\nclass MyClass {}\nconst value = new MyClass();\nvalue + '';\n\n// Interpolation and manual .toString() and `toLocaleString()` calls too:",
90
+ "good": "// These types all have useful .toString() and `toLocaleString()` methods\n'Text' + true;\n`Value: ${123}`;\n`Arrays too: ${[1, 2, 3]}`;\n(() => {}).toString();\nString(42);\n(() => {}).toLocaleString();\n"
91
+ },
92
+ "@typescript-eslint/require-await": {
93
+ "what": "Disallow async functions which do not return promises and have no `await` expression.",
94
+ "bad": "async function returnNumber() {\n return 1;\n}\n\nasync function* asyncGenerator() {\n yield 1;\n}\n",
95
+ "good": "function returnNumber() {\n return 1;\n}\n\nfunction* syncGenerator() {\n yield 1;\n}\n"
96
+ },
97
+ "@typescript-eslint/no-confusing-void-expression": {
98
+ "what": "Require expressions of type void to appear in statement position.",
99
+ "bad": "// somebody forgot that `alert` doesn't return anything\nconst response = alert('Are you sure?');\nconsole.log(alert('Are you sure?'));\n\n// it's not obvious whether the chained promise will contain the response (fixable)\npromise.then(value => window.postMessage(value));\n\n// it looks like we are returning the result of `console.error` (fixable)",
100
+ "good": "// just a regular void function in a statement position\nalert('Hello, world!');\n\n// this function returns a boolean value so it's ok\nconst response = confirm('Are you sure?');\nconsole.log(confirm('Are you sure?'));\n\n// now it's obvious that `postMessage` doesn't return any response"
101
+ },
102
+ "@typescript-eslint/no-redundant-type-constituents": {
103
+ "what": "Disallow members of unions and intersections that do nothing or override type information.",
104
+ "bad": "type UnionAny = any | 'foo';\ntype UnionUnknown = unknown | 'foo';\ntype UnionNever = never | 'foo';\n\ntype UnionBooleanLiteral = boolean | false;\ntype UnionNumberLiteral = number | 1;\ntype UnionStringLiteral = string | 'foo';\n",
105
+ "good": "type UnionAny = any;\ntype UnionUnknown = unknown;\ntype UnionNever = never;\n\ntype UnionBooleanLiteral = boolean;\ntype UnionNumberLiteral = number;\ntype UnionStringLiteral = string;\n"
106
+ },
107
+ "@typescript-eslint/prefer-reduce-type-parameter": {
108
+ "what": "Enforce using type parameter when calling `Array#reduce` instead of using a type assertion.",
109
+ "bad": "[1, 2, 3].reduce((arr, num) => arr.concat(num * 2), [] as number[]);\n\n['a', 'b'].reduce(\n (accumulator, name) => ({\n ...accumulator,\n [name]: true,\n }),\n {} as Record<string, boolean>,",
110
+ "good": "[1, 2, 3].reduce<number[]>((arr, num) => arr.concat(num * 2), []);\n\n['a', 'b'].reduce<Record<string, boolean>>(\n (accumulator, name) => ({\n ...accumulator,\n [name]: true,\n }),\n {},"
111
+ },
112
+ "tsforge/job-name-must-be-constant": {
113
+ "what": "Disallow string-literal job names in `<queue>.add(name, ...)` calls — use a constant identifier so all consumers share one source of truth.",
114
+ "bad": "// Example that violates the rule",
115
+ "good": "// Corrected version"
116
+ },
117
+ "tsforge/job-options-must-set-attempts": {
118
+ "what": "Every `<queue>.add(...)` must configure `attempts` (per-call or via `defaultJobOptions`); when `attempts > 1`, also require `backoff`.",
119
+ "bad": "// Example that violates the rule",
120
+ "good": "// Corrected version"
121
+ },
122
+ "tsforge/no-blocking-concurrency-zero": {
123
+ "what": "Disallow `new Worker(name, processor, { concurrency: <numericLiteral ≤ 0> })` — non-positive concurrency blocks job processing.",
124
+ "bad": "// Example that violates the rule",
125
+ "good": "// Corrected version"
126
+ },
127
+ "tsforge/queue-options-must-set-removeoncomplete": {
128
+ "what": "Every `<queue>.add(...)` must configure `removeOnComplete` (per-call or via `defaultJobOptions`) so completed jobs don't accumulate in Redis.",
129
+ "bad": "// Example that violates the rule",
130
+ "good": "// Corrected version"
131
+ },
132
+ "tsforge/queue-options-must-set-removeonfail": {
133
+ "what": "Every `<queue>.add(...)` must configure `removeOnFail` (per-call or via `defaultJobOptions`) so failed jobs don't accumulate in Redis.",
134
+ "bad": "// Example that violates the rule",
135
+ "good": "// Corrected version"
136
+ },
137
+ "tsforge/worker-must-implement-close": {
138
+ "what": "Classes that own a `new Worker(...)` instance must declare a close-equivalent method for graceful shutdown.",
139
+ "bad": "// Example that violates the rule",
140
+ "good": "// Corrected version"
141
+ },
142
+ "tsforge/worker-must-listen-failed": {
143
+ "what": "Every `new Worker(...)` must register listeners for required events (default `failed`) — BullMQ failures are silent unless explicitly subscribed.",
144
+ "bad": "// Example that violates the rule",
145
+ "good": "// Corrected version"
146
+ },
147
+ "tsforge/no-bare-date-now": {
148
+ "what": "Disallow direct calls to non-deterministic time/random sources (`Date.now()`, `new Date()`, `Date()`, `Math.random()`) outside an allowlisted set of utility paths. Determinism is required for snapshot tests, workflow replays, and time-travel debugging — every consumer should route through a typed util that can be faked in tests.",
149
+ "bad": "// Example that violates the rule",
150
+ "good": "// Corrected version"
151
+ },
152
+ "tsforge/no-template-trim-empty-ternary": {
153
+ "what": "Disallow inline `<template>.trim() === '' ? fallback : <template>.trim()` patterns. Extract to a named utility.",
154
+ "bad": "// Example that violates the rule",
155
+ "good": "// Corrected version"
156
+ },
157
+ "tsforge/prefer-early-return": {
158
+ "what": "Prefer guard clauses (early return) over wrapping the function body in a multi-statement `if` without an `else`.",
159
+ "bad": "// Example that violates the rule",
160
+ "good": "// Corrected version"
161
+ },
162
+ "tsforge/no-historical-comments": {
163
+ "what": "Disallow comments that frame code relative to what it used to do or to a past incident ('Codex flagged X', 'before the fix', 'after the refactor', 'we used to', 'no longer'). Source comments must describe the current invariant; history belongs in the commit message or PR description, where it doesn't rot when the code changes again.",
164
+ "bad": "// Example that violates the rule",
165
+ "good": "// Corrected version"
166
+ },
167
+ "tsforge/no-narration-comments": {
168
+ "what": "Disallow narrative comments like 'Here we...', 'Now we...', 'First, we...'. These read as step-by-step prose and add no information a future reader can't get from the code itself. Often a tell that the comment was generated by an agent describing its own changes.",
169
+ "bad": "// Example that violates the rule",
170
+ "good": "// Corrected version"
171
+ },
172
+ "tsforge/no-pr-reference-comments": {
173
+ "what": "Disallow PR/issue references in comments. They belong in commit messages and PR descriptions — leaving them in source rots when the repo moves, the issue tracker migrates, or the numbering changes.",
174
+ "bad": "// Example that violates the rule",
175
+ "good": "// Corrected version"
176
+ },
177
+ "tsforge/account-scoped-tables-require-where": {
178
+ "what": "Require every Drizzle query against a configured account-scoped table to filter by a scope column (accountId by default).",
179
+ "bad": "// Example that violates the rule",
180
+ "good": "// Corrected version"
181
+ },
182
+ "tsforge/no-nested-db-transaction": {
183
+ "what": "Forbid invoking the outer db's `.transaction(...)` method inside a transaction callback — use the callback's `tx` parameter instead to avoid deadlocks.",
184
+ "bad": "// Example that violates the rule",
185
+ "good": "// Corrected version"
186
+ },
187
+ "tsforge/no-raw-sql-outside-allowlist": {
188
+ "what": "Disallow drizzle-orm `sql` tagged template literals outside an allowlist of files (migrations, raw queries).",
189
+ "bad": "// Example that violates the rule",
190
+ "good": "// Corrected version"
191
+ },
192
+ "tsforge/relations-must-cover-fks": {
193
+ "what": "Every Drizzle table that declares a foreignKey(...) must be covered by a relations(...) call. Searches sibling `relations.ts` files by default.",
194
+ "bad": "// Example that violates the rule",
195
+ "good": "// Corrected version"
196
+ },
197
+ "tsforge/schema-files-must-not-import-driver": {
198
+ "what": "Disallow imports from database driver packages inside schema files. Schema files must remain driver-agnostic.",
199
+ "bad": "// Example that violates the rule",
200
+ "good": "// Corrected version"
201
+ },
202
+ "tsforge/schema-files-must-only-export-schema": {
203
+ "what": "Restrict schema files to exporting only Drizzle schema artifacts (tables, schemas, relations, indices) and types.",
204
+ "bad": "// Example that violates the rule",
205
+ "good": "// Corrected version"
206
+ },
207
+ "tsforge/tables-must-have-timestamps": {
208
+ "what": "Require Drizzle tables to declare standard timestamp columns (createdAt by default).",
209
+ "bad": "// Example that violates the rule",
210
+ "good": "// Corrected version"
211
+ },
212
+ "tsforge/timestamp-must-specify-mode": {
213
+ "what": "Require every Drizzle timestamp(...) call to explicitly set `mode: 'date'` or `mode: 'string'`.",
214
+ "bad": "// Example that violates the rule",
215
+ "good": "// Corrected version"
216
+ },
217
+ "tsforge/consistent-status-via-set": {
218
+ "what": "Inside Elysia route handlers, set HTTP status via `set.status = N`, not by returning a `new Response(body, { status: N })`.",
219
+ "bad": "// Example that violates the rule",
220
+ "good": "// Corrected version"
221
+ },
222
+ "tsforge/no-decorate-state-collision": {
223
+ "what": "Disallow duplicate keys across `.decorate()` / `.state()` / `.derive()` / `.resolve()` calls on a single Elysia instance — duplicates silently overwrite and break plugin composition.",
224
+ "bad": "// Example that violates the rule",
225
+ "good": "// Corrected version"
226
+ },
227
+ "tsforge/no-separate-model-interfaces": {
228
+ "what": "Disallow TypeScript interfaces that duplicate the shape of a runtime schema with a matching name. Use `typeof Schema.static` (or your project's equivalent) instead.",
229
+ "bad": "// Example that violates the rule",
230
+ "good": "// Corrected version"
231
+ },
232
+ "tsforge/prefer-destructured-context": {
233
+ "what": "Prefer destructured context (`{ body, set, ... }`) over passing the entire dynamic Elysia context object into controllers/services.",
234
+ "bad": "// Example that violates the rule",
235
+ "good": "// Corrected version"
236
+ },
237
+ "tsforge/prefer-direct-return": {
238
+ "what": "Inside Elysia route handlers, return values directly instead of wrapping them in `new Response(...)` or `Response.json(...)` — Elysia handles serialization and content-type automatically.",
239
+ "bad": "// Example that violates the rule",
240
+ "good": "// Corrected version"
241
+ },
242
+ "tsforge/prefer-static-services": {
243
+ "what": "Discourage `new Service()` inside Elysia route handlers when the class is stateless — prefer static methods or a singleton.",
244
+ "bad": "// Example that violates the rule",
245
+ "good": "// Corrected version"
246
+ },
247
+ "tsforge/prefer-throw-status": {
248
+ "what": "Inside Elysia route handlers, prefer `throw status(...)` over try/catch blocks that build their own Response — local catches bypass Elysia's typed onError pipeline.",
249
+ "bad": "// Example that violates the rule",
250
+ "good": "// Corrected version"
251
+ },
252
+ "tsforge/require-hooks-before-routes": {
253
+ "what": "Elysia hooks (onError, onBeforeHandle, etc.) must register before any route methods on the same instance — top-down waterfall semantics mean a hook registered after a route does not apply to it.",
254
+ "bad": "// Example that violates the rule",
255
+ "good": "// Corrected version"
256
+ },
257
+ "tsforge/require-plugin-name": {
258
+ "what": "Exported Elysia plugin instances must declare `new Elysia({ name: '...' })` so the runtime can deduplicate plugin re-imports.",
259
+ "bad": "// Example that violates the rule",
260
+ "good": "// Corrected version"
261
+ },
262
+ "tsforge/no-direct-process-env": {
263
+ "what": "Disallow direct `process.env` access — force every consumer through a typed, boot-validated singleton.",
264
+ "bad": "// Example that violates the rule",
265
+ "good": "// Corrected version"
266
+ },
267
+ "tsforge/no-process-exit": {
268
+ "what": "Disallow `process.exit()` outside the centralized shutdown and CLI entrypoints — forces graceful teardown through the error-handlers module.",
269
+ "bad": "// Example that violates the rule",
270
+ "good": "// Corrected version"
271
+ },
272
+ "tsforge/static-translation-key-exists": {
273
+ "what": "Static string passed to `t(\"...\")` or `i18n.t(\"...\")` must exist as a leaf path in the canonical locale JSON.",
274
+ "bad": "// Example that violates the rule",
275
+ "good": "// Corrected version"
276
+ },
277
+ "tsforge/auth-cookie-must-be-httponly": {
278
+ "what": "Auth-cookie writes must set `httpOnly: true` (or spread a trusted cookie-config helper). JS-readable session cookies leak via XSS.",
279
+ "bad": "// Example that violates the rule",
280
+ "good": "// Corrected version"
281
+ },
282
+ "tsforge/auth-cookie-must-be-secure-in-prod": {
283
+ "what": "Auth-cookie writes must set `secure:` to `true` or an env-derived expression (anything non-literal). Cookies leak over HTTP without it.",
284
+ "bad": "// Example that violates the rule",
285
+ "good": "// Corrected version"
286
+ },
287
+ "tsforge/bcrypt-rounds-min": {
288
+ "what": "Disallow `bcrypt.hash` / `bcrypt.hashSync` calls with a numeric-literal rounds value below the configured minimum (default 10).",
289
+ "bad": "// Example that violates the rule",
290
+ "good": "// Corrected version"
291
+ },
292
+ "tsforge/pkce-required-for-oidc": {
293
+ "what": "OIDC providers must use PKCE: `buildAuthorizationURL` must call `generateCodeVerifier()` and pass it to `createAuthorizationURL`.",
294
+ "bad": "// Example that violates the rule",
295
+ "good": "// Corrected version"
296
+ },
297
+ "tsforge/state-must-be-redis-backed": {
298
+ "what": "OAuth state must be persisted to Redis and not stuffed into a cookie. Cookie-backed state lets attackers replay forged state across sessions.",
299
+ "bad": "// Example that violates the rule",
300
+ "good": "// Corrected version"
301
+ },
302
+ "tsforge/state-ttl-bounded": {
303
+ "what": "OAuth state writes to Redis must use a short TTL — long-lived state widens the replay window.",
304
+ "bad": "// Example that violates the rule",
305
+ "good": "// Corrected version"
306
+ },
307
+ "tsforge/component-folder-structure": {
308
+ "what": "Enforce required sibling files in component folders (hooks, types, stories, test, index)",
309
+ "bad": "// Example that violates the rule",
310
+ "good": "// Corrected version"
311
+ },
312
+ "tsforge/forwardref-display-name": {
313
+ "what": "forwardRef components must have displayName set",
314
+ "bad": "// Example that violates the rule",
315
+ "good": "// Corrected version"
316
+ },
317
+ "tsforge/index-must-reexport-default": {
318
+ "what": "index.ts in component folders must re-export the component default export and types",
319
+ "bad": "// Example that violates the rule",
320
+ "good": "// Corrected version"
321
+ },
322
+ "tsforge/max-hooks-per-file": {
323
+ "what": "Flag query/hook modules that export more than N hooks. Same-kind modules pass the single-semantic-module rule but still grow into god files; this rule sets a hard ceiling so the split conversation happens early.",
324
+ "bad": "// Example that violates the rule",
325
+ "good": "// Corrected version"
326
+ },
327
+ "tsforge/no-cross-feature-imports": {
328
+ "what": "Prevent imports across different features",
329
+ "bad": "// Example that violates the rule",
330
+ "good": "// Corrected version"
331
+ },
332
+ "tsforge/no-inline-jsx-functions": {
333
+ "what": "Disallow inline function expressions in JSX attributes",
334
+ "bad": "// Example that violates the rule",
335
+ "good": "// Corrected version"
336
+ },
337
+ "tsforge/mask-pii-fields": {
338
+ "what": "Disallow unmasked PII (email, phone, password, token, ...) in structured-logger payloads — the #1 way data leaks quietly.",
339
+ "bad": "// Example that violates the rule",
340
+ "good": "// Corrected version"
341
+ },
342
+ "tsforge/no-error-stringify": {
343
+ "what": "Disallow stringifying errors with `String(error)` / `${error}` / `error.toString()` — strips the cause chain. Use a configured extractor instead.",
344
+ "bad": "// Example that violates the rule",
345
+ "good": "// Corrected version"
346
+ },
347
+ "tsforge/require-event-field": {
348
+ "what": "Require structured logger calls to include an `event` field in their payload, so log searches in ELK/Datadog/Loki don't fall back to substring match.",
349
+ "bad": "// Example that violates the rule",
350
+ "good": "// Corrected version"
351
+ },
352
+ "tsforge/prefix-query-key-must-use-set-queries-data": {
353
+ "what": "When a hook uses `queryKey: [...prefix, extra]`, do not call `setQueryData(prefix, …)`, `cancelQueries({ queryKey: prefix })`, etc. — those only touch one cache entry. Use `setQueriesData({ queryKey: prefix }, …)` and matcher-style `cancelQueries` / `invalidateQueries` so every variant is covered.",
354
+ "bad": "// Example that violates the rule",
355
+ "good": "// Corrected version"
356
+ },
357
+ "tsforge/no-focused-tests": {
358
+ "what": "Disallow focused tests (`test.only`, `it.only`, `fdescribe`, ...) — the canonical 'I forgot to remove this before committing' leak.",
359
+ "bad": "// Example that violates the rule",
360
+ "good": "// Corrected version"
361
+ },
362
+ "tsforge/test-file-mirrors-source": {
363
+ "what": "Every test file under `tests/` must mirror a source file under `src/`. Catches orphaned tests left behind after refactors and renames.",
364
+ "bad": "// Example that violates the rule",
365
+ "good": "// Corrected version"
366
+ }
367
+ }
@@ -0,0 +1,88 @@
1
+ import type { ISpec } from "../spec";
2
+ import type { IProvider } from "../inference";
3
+ import { validate, type ErrorParser } from "../validate";
4
+ import { runTask } from "./run";
5
+ import { RUN_STATUS, SPEC_STATUS } from "./loop.constants";
6
+ import type { IRunResult, ISpecResult, Reporter } from "./loop.types";
7
+
8
+ export interface IRunSpecOptions {
9
+ parse?: ErrorParser;
10
+ onEvent?: Reporter;
11
+ temperature?: number;
12
+ enableThinking?: boolean;
13
+ thinkingTokenBudget?: number;
14
+ }
15
+
16
+ /**
17
+ * Run a spec's tasks in listed order. Each task goes through the implement
18
+ * loop; the first task that doesn't reach `done` blocks the spec. When all
19
+ * tasks pass, the whole-spec `verify` gate must also pass.
20
+ *
21
+ * (Dependency ordering via `needs:` is a later slice — v1 is file order.)
22
+ */
23
+ export async function runSpec(
24
+ spec: ISpec,
25
+ cwd: string,
26
+ provider: IProvider,
27
+ opts: IRunSpecOptions = {}
28
+ ): Promise<ISpecResult> {
29
+ const { parse, onEvent, temperature, enableThinking, thinkingTokenBudget } =
30
+ opts;
31
+ const report: Reporter = onEvent ?? (() => undefined);
32
+ const results: IRunResult[] = [];
33
+
34
+ for (const task of spec.tasks) {
35
+ const result = await runTask(task, cwd, provider, {
36
+ parse,
37
+ onEvent,
38
+ temperature,
39
+ enableThinking,
40
+ thinkingTokenBudget,
41
+ });
42
+
43
+ results.push(result);
44
+
45
+ if (result.status !== RUN_STATUS.done) {
46
+ report({
47
+ kind: "stuck",
48
+ task: task.id,
49
+ message: `spec "${spec.id}" blocked at task ${task.id}`,
50
+ });
51
+
52
+ return { status: SPEC_STATUS.blocked, results };
53
+ }
54
+ }
55
+
56
+ // Whole-spec gate: every task is green, now the spec's own verify must pass.
57
+ if (spec.verify.length > 0) {
58
+ report({
59
+ kind: "start",
60
+ task: "verify",
61
+ message: `whole-spec verify: ${spec.verify}`,
62
+ });
63
+
64
+ const verified = await validate(
65
+ { id: "verify", accept: spec.verify, files: [] },
66
+ cwd,
67
+ parse
68
+ );
69
+
70
+ report({
71
+ kind: "validated",
72
+ task: "verify",
73
+ passed: verified.passed,
74
+ errors: verified.errors.length,
75
+ message: verified.passed
76
+ ? "whole-spec verify: GREEN"
77
+ : `whole-spec verify: red (${verified.errors.length} error(s))`,
78
+ });
79
+
80
+ if (!verified.passed) {
81
+ return { status: SPEC_STATUS.blocked, results };
82
+ }
83
+ }
84
+
85
+ report({ kind: "done", task: spec.id, message: `spec "${spec.id}": done` });
86
+
87
+ return { status: SPEC_STATUS.done, results };
88
+ }