@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,174 @@
1
+ import type { IRulePackDescriptor } from "./stack-detection.types";
2
+
3
+ /**
4
+ * Registry of all available rule packs. This is the source of truth for which packs
5
+ * exist and under what conditions they are enabled during stack detection.
6
+ */
7
+ export const PACK_REGISTRY = {
8
+ "generic-ts": {
9
+ id: "generic-ts",
10
+ label: "TypeScript Fundamentals",
11
+ description:
12
+ "Core TypeScript safety rules for all projects (strict mode, index safety, no casts)",
13
+ category: "language",
14
+ appliesWhen: { always: true },
15
+ guidance: "Enforce strict TypeScript rules on all projects.",
16
+ } as const satisfies IRulePackDescriptor,
17
+
18
+ react: {
19
+ id: "react",
20
+ label: "React",
21
+ description: "React component patterns and hooks usage",
22
+ category: "framework",
23
+ appliesWhen: { allDeps: ["react", "react-dom"] },
24
+ guidance: "Follow React best practices for component composition.",
25
+ } as const satisfies IRulePackDescriptor,
26
+
27
+ "react-component-architecture": {
28
+ id: "react-component-architecture",
29
+ label: "React Component Architecture",
30
+ description:
31
+ "Component structure, composition, and file organization for React",
32
+ category: "framework",
33
+ appliesWhen: { allDeps: ["react"] },
34
+ guidance: "Organize React components with clear separation of concerns.",
35
+ } as const satisfies IRulePackDescriptor,
36
+
37
+ "tanstack-query": {
38
+ id: "tanstack-query",
39
+ label: "TanStack Query",
40
+ description: "Patterns for data fetching with TanStack Query",
41
+ category: "library",
42
+ appliesWhen: { anyDeps: ["@tanstack/react-query"] },
43
+ guidance: "Use TanStack Query for async state management.",
44
+ } as const satisfies IRulePackDescriptor,
45
+
46
+ drizzle: {
47
+ id: "drizzle",
48
+ label: "Drizzle ORM",
49
+ description: "Database access patterns using Drizzle ORM",
50
+ category: "library",
51
+ appliesWhen: { anyDeps: ["drizzle-orm"] },
52
+ guidance: "Use Drizzle ORM type-safely for database queries.",
53
+ } as const satisfies IRulePackDescriptor,
54
+
55
+ elysia: {
56
+ id: "elysia",
57
+ label: "Elysia",
58
+ description: "HTTP server patterns with Elysia framework",
59
+ category: "framework",
60
+ appliesWhen: { anyDeps: ["elysia"] },
61
+ guidance: "Follow Elysia patterns for HTTP routing and middleware.",
62
+ } as const satisfies IRulePackDescriptor,
63
+
64
+ bullmq: {
65
+ id: "bullmq",
66
+ label: "BullMQ",
67
+ description: "Job queue patterns with BullMQ",
68
+ category: "library",
69
+ appliesWhen: { anyDeps: ["bullmq"] },
70
+ guidance: "Use BullMQ for reliable async job processing.",
71
+ } as const satisfies IRulePackDescriptor,
72
+
73
+ "test-conventions": {
74
+ id: "test-conventions",
75
+ label: "Test Conventions",
76
+ description:
77
+ "Testing patterns and file structure for vitest, jest, or Bun tests",
78
+ category: "testing",
79
+ appliesWhen: {
80
+ anyDeps: ["vitest", "jest", "bun-types"],
81
+ files: ["vitest.config.*", "jest.config.*"],
82
+ },
83
+ guidance: "Follow consistent test organization and naming.",
84
+ } as const satisfies IRulePackDescriptor,
85
+
86
+ "env-access": {
87
+ id: "env-access",
88
+ label: "Environment Variables",
89
+ description:
90
+ "Safe environment variable access patterns (validation and typing)",
91
+ category: "infra",
92
+ appliesWhen: { always: true },
93
+ guidance: "Always validate and type environment variables.",
94
+ } as const satisfies IRulePackDescriptor,
95
+
96
+ "module-boundaries": {
97
+ id: "module-boundaries",
98
+ label: "Module Boundaries",
99
+ description: "Enforce clear module boundaries and layering",
100
+ category: "infra",
101
+ appliesWhen: { always: true },
102
+ guidance:
103
+ "Maintain clean module boundaries to avoid circular dependencies.",
104
+ } as const satisfies IRulePackDescriptor,
105
+
106
+ "code-flow": {
107
+ id: "code-flow",
108
+ label: "Code Flow",
109
+ description: "Control flow clarity and early returns",
110
+ category: "language",
111
+ appliesWhen: { always: true },
112
+ guidance: "Keep control flow clear and readable with early returns.",
113
+ } as const satisfies IRulePackDescriptor,
114
+
115
+ "comment-hygiene": {
116
+ id: "comment-hygiene",
117
+ label: "Comment Hygiene",
118
+ description: "Meaningful comments and documentation standards",
119
+ category: "language",
120
+ appliesWhen: { always: true },
121
+ guidance: "Write comments that explain intent, not what the code does.",
122
+ } as const satisfies IRulePackDescriptor,
123
+
124
+ "structured-logging": {
125
+ id: "structured-logging",
126
+ label: "Structured Logging",
127
+ description: "Logging patterns using Pino, Winston, or structured loggers",
128
+ category: "infra",
129
+ appliesWhen: { anyDeps: ["pino", "winston"] },
130
+ guidance: "Use structured logging with consistent JSON output.",
131
+ } as const satisfies IRulePackDescriptor,
132
+
133
+ "jwt-cookies": {
134
+ id: "jwt-cookies",
135
+ label: "JWT & Cookies",
136
+ description: "Secure JWT and cookie handling patterns",
137
+ category: "library",
138
+ appliesWhen: { anyDeps: ["jsonwebtoken", "jose"] },
139
+ guidance: "Implement JWT and cookies securely.",
140
+ } as const satisfies IRulePackDescriptor,
141
+
142
+ "oauth-security": {
143
+ id: "oauth-security",
144
+ label: "OAuth Security",
145
+ description: "OAuth and OpenID patterns and security considerations",
146
+ category: "library",
147
+ appliesWhen: { anyDeps: ["arctic", "openid-client", "passport"] },
148
+ guidance: "Follow OAuth/OpenID best practices.",
149
+ } as const satisfies IRulePackDescriptor,
150
+
151
+ "i18n-keys": {
152
+ id: "i18n-keys",
153
+ label: "i18n Keys",
154
+ description: "Internationalization key management and translation patterns",
155
+ category: "library",
156
+ appliesWhen: { anyDeps: ["i18next", "react-i18next"] },
157
+ guidance: "Keep i18n keys organized and validated.",
158
+ } as const satisfies IRulePackDescriptor,
159
+ } as const;
160
+
161
+ /** Ordered list of always-on pack IDs (for deterministic ordering). */
162
+ export const ALWAYS_ON_PACKS = [
163
+ "generic-ts",
164
+ "env-access",
165
+ "module-boundaries",
166
+ "code-flow",
167
+ "comment-hygiene",
168
+ ] as const;
169
+
170
+ /** Type-safe record of all pack descriptors. */
171
+ export type IPackRegistry = typeof PACK_REGISTRY;
172
+
173
+ /** Type-safe pack ID type. */
174
+ export type IPackId = keyof IPackRegistry;
@@ -0,0 +1,47 @@
1
+ /** A detected technology stack profile that determines which rule packs to enable. */
2
+ export interface IStackProfile {
3
+ /** Friendly stack name: "react", "node-backend", "generic", or combined like "react+drizzle". */
4
+ readonly name: string;
5
+
6
+ /** Enabled rule pack IDs, always includes "generic-ts" and other always-on packs. */
7
+ readonly packs: readonly string[];
8
+
9
+ /** How confident the detection is: "certain" (deps found), "likely" (files exist), "guess" (fallback). */
10
+ readonly confidence: "certain" | "likely" | "guess";
11
+
12
+ /** Human-readable reason for the profile, e.g., "react + drizzle-orm in package.json". */
13
+ readonly reason: string;
14
+ }
15
+
16
+ /** Metadata for a rule pack that determines when it should be enabled. */
17
+ export interface IRulePackDescriptor {
18
+ /** Unique identifier for the pack, used in enabled lists. */
19
+ readonly id: string;
20
+
21
+ /** Human-readable label. */
22
+ readonly label: string;
23
+
24
+ /** Short description of what the pack covers. */
25
+ readonly description: string;
26
+
27
+ /** Category for UI/filtering: "language" | "framework" | "library" | "infra" | "testing". */
28
+ readonly category: "language" | "framework" | "library" | "infra" | "testing";
29
+
30
+ /** Conditions under which this pack should be enabled. */
31
+ readonly appliesWhen: {
32
+ /** Enable if ANY of these deps (in dependencies or devDependencies) are present. */
33
+ readonly anyDeps?: readonly string[];
34
+
35
+ /** Enable only if ALL of these deps are present. */
36
+ readonly allDeps?: readonly string[];
37
+
38
+ /** Enable if any of these file paths/globs exist. */
39
+ readonly files?: readonly string[];
40
+
41
+ /** Enable this pack unconditionally (used for always-on packs like generic-ts). */
42
+ readonly always?: boolean;
43
+ };
44
+
45
+ /** Short guidance text injected into system prompt when pack is active (populated in later tasks). */
46
+ readonly guidance?: string;
47
+ }
@@ -0,0 +1,49 @@
1
+ import type { ITask } from "../spec";
2
+ import { runShellCommand } from "../lib/fs";
3
+
4
+ import type { IAcceptResult } from "./validate.types";
5
+
6
+ /** Default kill-timeout for the GATE (ms). More generous than the `run` tool —
7
+ * a web gate is `vite build` + headless chromium, legitimately slow — but still
8
+ * bounded so a stuck build can't wedge the harness. TSFORGE_GATE_TIMEOUT_MS to
9
+ * override (0 = no timeout). */
10
+ const DEFAULT_GATE_TIMEOUT_MS = 600_000;
11
+
12
+ function gateTimeoutMs(): number {
13
+ const env = Number(process.env.TSFORGE_GATE_TIMEOUT_MS);
14
+
15
+ return Number.isFinite(env) && env >= 0 ? env : DEFAULT_GATE_TIMEOUT_MS;
16
+ }
17
+
18
+ /** Optional knobs for `runAccept`: stream the output live (`onChunk`) and/or
19
+ * cancel it (`signal`). */
20
+ export interface IAcceptOptions {
21
+ onChunk?: (text: string) => void;
22
+ signal?: AbortSignal;
23
+ }
24
+
25
+ /**
26
+ * Run a task's `accept:` command in `cwd`. This is the deterministic oracle in
27
+ * miniature — pass/fail comes from the exit code, never from model judgment.
28
+ * Pass `onChunk` to stream the command's output live (the CLI does, so a slow
29
+ * gate like `vite build` + browser isn't silent); omit it and output is just
30
+ * captured (the eval path). Cancellable via `signal`, and killed after the gate
31
+ * timeout so a hung build can't wedge the harness.
32
+ */
33
+ export async function runAccept(
34
+ task: ITask,
35
+ cwd: string,
36
+ opts: IAcceptOptions = {}
37
+ ): Promise<IAcceptResult> {
38
+ const run = await runShellCommand(cwd, task.accept, {
39
+ timeoutMs: gateTimeoutMs(),
40
+ ...(opts.onChunk === undefined ? {} : { onChunk: opts.onChunk }),
41
+ ...(opts.signal === undefined ? {} : { signal: opts.signal }),
42
+ });
43
+
44
+ const note = run.timedOut
45
+ ? `\n[gate killed after ${gateTimeoutMs()}ms timeout — TSFORGE_GATE_TIMEOUT_MS to change]`
46
+ : "";
47
+
48
+ return { passed: run.exitCode === 0, output: run.stdout + run.stderr + note };
49
+ }
@@ -0,0 +1,35 @@
1
+ import type { ErrorSet, IErrorDiff } from "./validate.types";
2
+
3
+ /** What changed between two cycles — the gradient the model descends. */
4
+ export function diffErrorSets(prev: ErrorSet, next: ErrorSet): IErrorDiff {
5
+ const prevKeys = new Set(prev.map((e) => e.key));
6
+ const nextKeys = new Set(next.map((e) => e.key));
7
+
8
+ return {
9
+ fixed: prev.filter((e) => !nextKeys.has(e.key)),
10
+ introduced: next.filter((e) => !prevKeys.has(e.key)),
11
+ remaining: next.filter((e) => prevKeys.has(e.key)),
12
+ };
13
+ }
14
+
15
+ /** Progress = fewer errors than last cycle. */
16
+ export function shrank(prev: ErrorSet, next: ErrorSet): boolean {
17
+ return next.length < prev.length;
18
+ }
19
+
20
+ /**
21
+ * True when two cycles produced the EXACT same error set (same keys). This —
22
+ * not "didn't shrink" — is the honest signal of a stuck model: it's spinning
23
+ * without changing the gate outcome at all. Any difference (fewer, more, or a
24
+ * different mix) means the model is still moving the problem and we keep going.
25
+ */
26
+ export function sameErrorSet(prev: ErrorSet, next: ErrorSet): boolean {
27
+ if (prev.length !== next.length) {
28
+ return false;
29
+ }
30
+
31
+ const a = prev.map((e) => e.key).sort();
32
+ const b = next.map((e) => e.key).sort();
33
+
34
+ return a.every((key, i) => key === b[i]);
35
+ }
@@ -0,0 +1,12 @@
1
+ export * from "./validate.types";
2
+ export { validate } from "./validate";
3
+ export {
4
+ parseTsc,
5
+ genericErrors,
6
+ parseEslintJson,
7
+ combinedParser,
8
+ parserFor,
9
+ } from "./parse";
10
+ export { diffErrorSets, shrank, sameErrorSet } from "./errors";
11
+ export { runTests, isRealRed } from "./run-tests";
12
+ export { runAccept } from "./accept";
@@ -0,0 +1,148 @@
1
+ import type { IErrorItem, ErrorParserFn } from "./validate.types";
2
+ import { isArray, isRecord } from "../lib/guards";
3
+
4
+ const TSC = /^(.+?)\((\d+),(\d+)\): error (TS\d+): (.+)$/;
5
+
6
+ /** Parse `tsc` output into a structured error set (one item per diagnostic). */
7
+ export function parseTsc(output: string): IErrorItem[] {
8
+ const items: IErrorItem[] = [];
9
+
10
+ for (const line of output.split("\n")) {
11
+ const m = TSC.exec(line);
12
+
13
+ if (!m) {
14
+ continue;
15
+ }
16
+
17
+ const [, file, lineStr, , rule, message] = m;
18
+
19
+ if (
20
+ file === undefined ||
21
+ lineStr === undefined ||
22
+ rule === undefined ||
23
+ message === undefined
24
+ ) {
25
+ continue;
26
+ }
27
+
28
+ items.push({
29
+ key: `${file}:${lineStr}:${rule}`,
30
+ file,
31
+ line: Number(lineStr),
32
+ rule,
33
+ message: message.trim(),
34
+ });
35
+ }
36
+
37
+ return items;
38
+ }
39
+
40
+ /** Fallback when we have no tool-specific parser: the whole output is one error. */
41
+ export function genericErrors(output: string): IErrorItem[] {
42
+ const text = output.trim();
43
+
44
+ return text.length > 0 ? [{ key: "raw", message: text.slice(0, 500) }] : [];
45
+ }
46
+
47
+ /**
48
+ * Parse `eslint --format json`. Errors (severity 2) only; warnings ignored.
49
+ * Carries the `ruleId` — including custom plugin rules like
50
+ * `@boring-stack/structured-logging` — so the repair loop sees exactly which
51
+ * rule failed. Narrowed with guards (no type assertions).
52
+ */
53
+ export function parseEslintJson(output: string): IErrorItem[] {
54
+ let data: unknown;
55
+
56
+ try {
57
+ data = JSON.parse(output);
58
+ } catch {
59
+ return [];
60
+ }
61
+
62
+ if (!isArray(data)) {
63
+ return [];
64
+ }
65
+
66
+ return data.flatMap(eslintFileItems);
67
+ }
68
+
69
+ /** Error items from one eslint JSON file entry (severity-2 messages only). */
70
+ function eslintFileItems(file: unknown): IErrorItem[] {
71
+ if (!isRecord(file)) {
72
+ return [];
73
+ }
74
+
75
+ const filePath = typeof file.filePath === "string" ? file.filePath : "";
76
+ const messages = file.messages;
77
+
78
+ if (!isArray(messages)) {
79
+ return [];
80
+ }
81
+
82
+ const items: IErrorItem[] = [];
83
+
84
+ for (const m of messages) {
85
+ if (!isRecord(m) || m.severity !== 2) {
86
+ continue;
87
+ }
88
+
89
+ const rule = typeof m.ruleId === "string" ? m.ruleId : "syntax";
90
+ const lineNo = typeof m.line === "number" ? m.line : undefined;
91
+ const message = typeof m.message === "string" ? m.message : "";
92
+
93
+ items.push({
94
+ key: `${filePath}:${lineNo ?? 0}:${rule}`,
95
+ file: filePath,
96
+ line: lineNo,
97
+ rule,
98
+ message: message.trim(),
99
+ });
100
+ }
101
+
102
+ return items;
103
+ }
104
+
105
+ /**
106
+ * For chained gates like `tsc -p … && eslint --format json … && bun test`,
107
+ * `&&` short-circuits: when tsc fails the output is tsc's TEXT (eslint never
108
+ * ran), and when it passes the output is eslint's JSON. A single tool-specific
109
+ * parser is wrong for both phases — pick eslint-json and tsc-text output parses
110
+ * to nothing (so the whole wall dumps as one blob); pick tsc and eslint's JSON
111
+ * is missed. Run BOTH and union: their formats don't overlap (tsc-text vs JSON),
112
+ * and only one is ever present at a time, so this is lossless either way.
113
+ */
114
+ export function combinedParser(output: string): IErrorItem[] {
115
+ const merged = [...parseTsc(output), ...parseEslintJson(output)];
116
+ const seen = new Set<string>();
117
+
118
+ return merged.filter((e) => {
119
+ if (seen.has(e.key)) {
120
+ return false;
121
+ }
122
+
123
+ seen.add(e.key);
124
+
125
+ return true;
126
+ });
127
+ }
128
+
129
+ /** Pick a parser from the command. Add tools here as we support them. */
130
+ export function parserFor(command: string): ErrorParserFn {
131
+ const hasTsc = /\btsc\b/.test(command);
132
+ const hasEslint = /\beslint\b/.test(command);
133
+
134
+ // A chained tsc+eslint gate needs the combined parser (see above).
135
+ if (hasTsc && hasEslint) {
136
+ return combinedParser;
137
+ }
138
+
139
+ if (hasEslint) {
140
+ return parseEslintJson;
141
+ }
142
+
143
+ if (hasTsc) {
144
+ return parseTsc;
145
+ }
146
+
147
+ return genericErrors;
148
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Run a single test file and report how many tests the runner collected.
3
+ *
4
+ * This is the deterministic answer to "are these real, runnable tests?" — we let
5
+ * `bun test` be the oracle rather than counting `test(` calls by regex. The
6
+ * load-bearing fact: an empty/vacuous file EXITS 0, so the exit code lies; only
7
+ * the collected count (`total >= 1`) proves the suite actually asserts anything.
8
+ */
9
+ import { runArgvCommand } from "../lib/fs";
10
+
11
+ import type { IRunTestsResult } from "./validate.types";
12
+
13
+ export async function runTests(
14
+ testFile: string,
15
+ cwd: string
16
+ ): Promise<IRunTestsResult> {
17
+ // Route through the shared runner, which drains stdout/stderr CONCURRENTLY
18
+ // with the process. Awaiting `proc.exited` before reading (the old code) can
19
+ // deadlock when a chatty test fills the pipe buffer — the child blocks on a
20
+ // full pipe while we wait for an exit that never arrives.
21
+ const run = await runArgvCommand(cwd, ["bun", "test", testFile]);
22
+ const output = run.stdout + run.stderr;
23
+
24
+ return { ...countTests(output), output };
25
+ }
26
+
27
+ /**
28
+ * The deterministic "real, runnable, RED suite" predicate: loads cleanly, has
29
+ * tests, and every one fails against a do-nothing stub. Shared by test
30
+ * generation and review so "acceptable suite" means one thing everywhere.
31
+ */
32
+ export function isRealRed(run: IRunTestsResult): boolean {
33
+ return run.errors === 0 && run.total >= 1 && run.pass === 0;
34
+ }
35
+
36
+ function countTests(output: string): {
37
+ pass: number;
38
+ fail: number;
39
+ total: number;
40
+ errors: number;
41
+ } {
42
+ const pass = firstNumber(/(\d+)\s+pass/.exec(output));
43
+ const fail = firstNumber(/(\d+)\s+fail/.exec(output));
44
+ const ran = /Ran\s+(\d+)\s+tests?/.exec(output);
45
+ const total = ran !== null ? firstNumber(ran) : pass + fail;
46
+
47
+ const counted = firstNumber(/(\d+)\s+error/.exec(output));
48
+ // bun sometimes prints the banner without a count; treat it as one error.
49
+ const errors =
50
+ counted > 0 || !output.includes("Unhandled error") ? counted : 1;
51
+
52
+ return { pass, fail, total, errors };
53
+ }
54
+
55
+ function firstNumber(match: RegExpExecArray | null): number {
56
+ const raw = match?.[1];
57
+
58
+ return raw === undefined ? 0 : Number(raw);
59
+ }
@@ -0,0 +1,40 @@
1
+ import type { ITask } from "../spec";
2
+ import type { ErrorParser, ErrorSet, IValidateResult } from "./validate.types";
3
+ import { runAccept, type IAcceptOptions } from "./accept";
4
+ import { parserFor } from "./parse";
5
+
6
+ /**
7
+ * Run a task's gate and turn the result into a structured error set. When no
8
+ * parser is given, one is auto-picked from the command (tsc/eslint/generic).
9
+ * `opts` forwards live-output streaming (`onChunk`) and cancellation (`signal`)
10
+ * down to the gate process.
11
+ */
12
+ export async function validate(
13
+ task: ITask,
14
+ cwd: string,
15
+ parse?: ErrorParser,
16
+ opts: IAcceptOptions = {}
17
+ ): Promise<IValidateResult> {
18
+ const parser = parse ?? parserFor(task.accept);
19
+ const r = await runAccept(task, cwd, opts);
20
+
21
+ if (r.passed) {
22
+ return { passed: true, errors: [], output: r.output };
23
+ }
24
+
25
+ // A failing gate must surface at least one error, even with empty output.
26
+ const parsed = parser(r.output);
27
+ const trimmed = r.output.trim();
28
+ const fallback: ErrorSet = [
29
+ {
30
+ key: "nonzero",
31
+ message: trimmed.length > 0 ? trimmed : "command exited non-zero",
32
+ },
33
+ ];
34
+
35
+ return {
36
+ passed: false,
37
+ errors: parsed.length > 0 ? parsed : fallback,
38
+ output: r.output,
39
+ };
40
+ }
@@ -0,0 +1,52 @@
1
+ /** One failure from the gate, with a stable key so we can diff across cycles. */
2
+ export interface IErrorItem {
3
+ key: string;
4
+ file?: string;
5
+ line?: number;
6
+ rule?: string;
7
+ message: string;
8
+ }
9
+
10
+ export type ErrorSet = IErrorItem[];
11
+
12
+ export interface IErrorDiff {
13
+ fixed: ErrorSet;
14
+ introduced: ErrorSet;
15
+ remaining: ErrorSet;
16
+ }
17
+
18
+ /** Parse raw gate output (tsc/eslint/test) into a structured error set. */
19
+ export type ErrorParser = (output: string) => IErrorItem[];
20
+
21
+ /** Alias kept for the parser-registry call sites. */
22
+ export type ErrorParserFn = ErrorParser;
23
+
24
+ export interface IValidateResult {
25
+ passed: boolean;
26
+ errors: ErrorSet;
27
+ output: string;
28
+ }
29
+
30
+ export interface IRunTestsResult {
31
+ /** Tests that passed. */
32
+ pass: number;
33
+ /** Tests that failed. */
34
+ fail: number;
35
+ /** Total tests the runner collected. 0 means empty/unparseable/vacuous. */
36
+ total: number;
37
+ /**
38
+ * Load/parse errors (missing import, syntax error). bun reports these as a
39
+ * failing "test", so `errors > 0` tells "ran and some assertions failed"
40
+ * (RED) from "the file never loaded".
41
+ */
42
+ errors: number;
43
+ /** Combined stdout + stderr, for feeding a failure back to the model. */
44
+ output: string;
45
+ }
46
+
47
+ export interface IAcceptResult {
48
+ /** True when the command exits 0. */
49
+ passed: boolean;
50
+ /** Combined stdout + stderr, for feeding back into the loop. */
51
+ output: string;
52
+ }