@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,9 @@
1
+ /** The "flag on" sentinel + the env var names the harness recognizes. */
2
+ export const FLAG_ON = "1";
3
+
4
+ export const ENV_FLAG = {
5
+ noLspTools: "TSFORGE_NO_LSP_TOOLS",
6
+ legacyFeedback: "TSFORGE_LEGACY_FEEDBACK",
7
+ noAstgrep: "TSFORGE_NO_ASTGREP",
8
+ forceTools: "TSFORGE_FORCE_TOOLS",
9
+ } as const;
@@ -0,0 +1,32 @@
1
+ import { FLAG_ON, ENV_FLAG } from "./config.constants";
2
+
3
+ /**
4
+ * The ONLY module that reads `process.env` for runtime flags. Read LIVE (on each
5
+ * call, not at import) so per-run/test env changes take effect. (Scripts read
6
+ * their own env directly — they're operational entry points, not library code.)
7
+ */
8
+ function isOn(name: string): boolean {
9
+ return process.env[name] === FLAG_ON;
10
+ }
11
+
12
+ export const flags = {
13
+ /** Withhold the LSP nav tool set even on existing-code runs (A/B control). */
14
+ noLspTools: (): boolean => isOn(ENV_FLAG.noLspTools),
15
+ /** Force the legacy (mis-selected) gate-feedback parser (A/B control). */
16
+ legacyFeedback: (): boolean => isOn(ENV_FLAG.legacyFeedback),
17
+ /** Disable the ast-grep safe-idiom rewrite pass in settleGate (A/B control). */
18
+ noAstgrep: (): boolean => isOn(ENV_FLAG.noAstgrep),
19
+ /** FORCED-TOOLS experiment (A/B, default off): every gated-build turn runs
20
+ * with tool_choice "required" + a `yield_status` stop tool, so output is
21
+ * always grammar-constrained — the malformed-tool-call class can't occur. */
22
+ forceTools: (): boolean => isOn(ENV_FLAG.forceTools),
23
+ /** Hashline edit tool (content-hash-anchored line edits) with snapshot recovery
24
+ * (A/B control, default ON — set to "0" to disable). */
25
+ hashlineEditTool: (): boolean => process.env.TSFORGE_HASHLINE !== "0",
26
+ /** TTSR stream-interrupting rules (A/B control, default ON — set to "0" to disable). */
27
+ ttsr: (): boolean => process.env.TSFORGE_TTSR !== "0",
28
+ /** Instant per-file type diagnostics appended to edit/create tool results
29
+ * (A/B control, default ON — set to "0" to disable). */
30
+ lspWriteFeedback: (): boolean =>
31
+ process.env.TSFORGE_LSP_WRITE_FEEDBACK !== "0",
32
+ };
@@ -0,0 +1,8 @@
1
+ export * from "./config.constants";
2
+ export { flags } from "./flags";
3
+ export {
4
+ loadTsforgeConfig,
5
+ resolveActivePacks,
6
+ normalizeRuleOverrides,
7
+ type ITsforgeProjectConfig,
8
+ } from "./tsforge-config";
@@ -0,0 +1,301 @@
1
+ import { join } from "node:path";
2
+ import { isRecord } from "../lib/guards";
3
+ import { PACK_REGISTRY } from "../stack-detection";
4
+
5
+ /**
6
+ * User-defined configuration from tsforge.config.json
7
+ * Allows users to tune the opinionated guardrails — override detected packs,
8
+ * include/exclude packs, and tune rule severities (eslint packs + meta-rules).
9
+ */
10
+ export interface ITsforgeProjectConfig {
11
+ /** Force-enable a stack by name (skip detection heuristics, force-add its packs). */
12
+ readonly stack?: string;
13
+
14
+ /** Pack include/exclude — applied AFTER detection. */
15
+ readonly packs?: {
16
+ readonly include?: readonly string[];
17
+ readonly exclude?: readonly string[];
18
+ };
19
+
20
+ /**
21
+ * ESLint rule + meta-rule severity overrides.
22
+ * Keys: bare rule name ("timestamp-must-specify-mode") or tsforge-prefixed ("tsforge/timestamp-must-specify-mode").
23
+ * Values: "error" | "warn" | "off" (off silences the rule).
24
+ */
25
+ readonly rules?: Readonly<Record<string, "error" | "warn" | "off">>;
26
+ }
27
+
28
+ function warnConfig(msg: string): void {
29
+ process.stderr.write(`${msg}\n`);
30
+ }
31
+
32
+ function warnInvalidStackType(stackValue: unknown): void {
33
+ const msg = `tsforge.config.json: "stack" must be a string, got ${typeof stackValue}`;
34
+
35
+ warnConfig(msg);
36
+ }
37
+
38
+ function warnInvalidPacksType(packsValue: unknown): void {
39
+ const msg = `tsforge.config.json: "packs" must be an object, got ${typeof packsValue}`;
40
+
41
+ warnConfig(msg);
42
+ }
43
+
44
+ function warnInvalidPacksInclude(): void {
45
+ warnConfig("tsforge.config.json: packs.include must be an array of strings");
46
+ }
47
+
48
+ function warnInvalidPacksExclude(): void {
49
+ warnConfig("tsforge.config.json: packs.exclude must be an array of strings");
50
+ }
51
+
52
+ function warnInvalidRulesType(rulesValue: unknown): void {
53
+ const msg = `tsforge.config.json: "rules" must be an object, got ${typeof rulesValue}`;
54
+
55
+ warnConfig(msg);
56
+ }
57
+
58
+ function warnInvalidRuleSeverity(key: string, value: unknown): void {
59
+ const msg = `tsforge.config.json: rule "${key}" severity must be "error", "warn", or "off", got "${String(value)}"`;
60
+
61
+ warnConfig(msg);
62
+ }
63
+
64
+ function warnInvalidJsonRoot(rootValue: unknown): void {
65
+ const msg = `tsforge.config.json: expected object root, got ${typeof rootValue}`;
66
+
67
+ warnConfig(msg);
68
+ }
69
+
70
+ function warnInvalidJson(msg: string | undefined): void {
71
+ const displayMsg =
72
+ msg !== undefined && msg.length > 0 ? msg : "parsing failed";
73
+
74
+ warnConfig(`tsforge.config.json: invalid JSON — ${displayMsg}`);
75
+ }
76
+
77
+ function warnReadError(msg: string): void {
78
+ warnConfig(`tsforge.config.json: read error — ${msg}`);
79
+ }
80
+
81
+ function warnUnknownPackInInclude(packId: string): void {
82
+ const msg = `tsforge.config.json: unknown pack in packs.include: "${packId}" (will be ignored)`;
83
+
84
+ warnConfig(msg);
85
+ }
86
+
87
+ /** Validate and extract stack field. */
88
+ function validateStack(parsed: unknown): string | undefined {
89
+ if (typeof parsed === "string") {
90
+ return parsed;
91
+ }
92
+
93
+ warnInvalidStackType(parsed);
94
+
95
+ return undefined;
96
+ }
97
+
98
+ /** Validate and extract packs field. */
99
+ function validatePacks(
100
+ parsed: unknown
101
+ ): { include?: readonly string[]; exclude?: readonly string[] } | undefined {
102
+ if (!isRecord(parsed)) {
103
+ warnInvalidPacksType(parsed);
104
+
105
+ return undefined;
106
+ }
107
+
108
+ const packFields: {
109
+ include?: readonly string[];
110
+ exclude?: readonly string[];
111
+ } = {};
112
+
113
+ if (parsed.include !== undefined) {
114
+ if (
115
+ Array.isArray(parsed.include) &&
116
+ parsed.include.every((x) => typeof x === "string")
117
+ ) {
118
+ packFields.include = parsed.include;
119
+ } else {
120
+ warnInvalidPacksInclude();
121
+ }
122
+ }
123
+
124
+ if (parsed.exclude !== undefined) {
125
+ if (
126
+ Array.isArray(parsed.exclude) &&
127
+ parsed.exclude.every((x) => typeof x === "string")
128
+ ) {
129
+ packFields.exclude = parsed.exclude;
130
+ } else {
131
+ warnInvalidPacksExclude();
132
+ }
133
+ }
134
+
135
+ return Object.keys(packFields).length > 0 ? packFields : undefined;
136
+ }
137
+
138
+ /** Validate and extract rules field. */
139
+ function validateRules(
140
+ parsed: unknown
141
+ ): Record<string, "error" | "warn" | "off"> | undefined {
142
+ if (!isRecord(parsed)) {
143
+ warnInvalidRulesType(parsed);
144
+
145
+ return undefined;
146
+ }
147
+
148
+ const rulesFields: Record<string, "error" | "warn" | "off"> = {};
149
+
150
+ for (const [key, value] of Object.entries(parsed)) {
151
+ if (value === "error" || value === "warn" || value === "off") {
152
+ rulesFields[key] = value;
153
+ } else {
154
+ warnInvalidRuleSeverity(key, value);
155
+ }
156
+ }
157
+
158
+ return Object.keys(rulesFields).length > 0 ? rulesFields : undefined;
159
+ }
160
+
161
+ /** Load tsforge.config.json from cwd, defaulting to empty config on missing/invalid files. */
162
+ export async function loadTsforgeConfig(
163
+ cwd: string
164
+ ): Promise<ITsforgeProjectConfig> {
165
+ const configPath = join(cwd, "tsforge.config.json");
166
+ const file = Bun.file(configPath);
167
+
168
+ const exists = await file.exists();
169
+
170
+ if (!exists) {
171
+ return {};
172
+ }
173
+
174
+ try {
175
+ const text = await file.text();
176
+ const parsed: unknown = JSON.parse(text);
177
+
178
+ if (!isRecord(parsed)) {
179
+ warnInvalidJsonRoot(parsed);
180
+
181
+ return {};
182
+ }
183
+
184
+ const configFields: {
185
+ stack?: string;
186
+ packs?: { include?: readonly string[]; exclude?: readonly string[] };
187
+ rules?: Record<string, "error" | "warn" | "off">;
188
+ } = {};
189
+
190
+ if (parsed.stack !== undefined) {
191
+ const stack = validateStack(parsed.stack);
192
+
193
+ if (stack !== undefined) {
194
+ configFields.stack = stack;
195
+ }
196
+ }
197
+
198
+ if (parsed.packs !== undefined) {
199
+ const packs = validatePacks(parsed.packs);
200
+
201
+ if (packs !== undefined) {
202
+ configFields.packs = packs;
203
+ }
204
+ }
205
+
206
+ if (parsed.rules !== undefined) {
207
+ const rules = validateRules(parsed.rules);
208
+
209
+ if (rules !== undefined) {
210
+ configFields.rules = rules;
211
+ }
212
+ }
213
+
214
+ return configFields;
215
+ } catch (err) {
216
+ if (err instanceof SyntaxError) {
217
+ const firstLine = err.message.split("\n")[0];
218
+
219
+ warnInvalidJson(firstLine);
220
+ } else {
221
+ const msg = err instanceof Error ? err.message : "unknown error";
222
+
223
+ warnReadError(msg);
224
+ }
225
+
226
+ return {};
227
+ }
228
+ }
229
+
230
+ /**
231
+ * Resolve the active packs after applying config overrides.
232
+ * Rules:
233
+ * 1. Start with detected packs (from stack detection)
234
+ * 2. If config.stack is set, force-add its packs (as if detected)
235
+ * 3. Apply packs.include (add unknown packs with warning)
236
+ * 4. Apply packs.exclude (remove known and unknown packs silently)
237
+ * 5. Return deduplicated pack list
238
+ */
239
+ export function resolveActivePacks(
240
+ detectedPacks: readonly string[],
241
+ config: ITsforgeProjectConfig
242
+ ): readonly string[] {
243
+ const packs = new Set(detectedPacks);
244
+
245
+ // Force-add packs for config.stack if set
246
+ if (config.stack !== undefined && config.stack.length > 0) {
247
+ packs.add(config.stack);
248
+ }
249
+
250
+ // Include: add packs (unknown ids are kept out of the registry lookup warning only)
251
+ for (const packId of config.packs?.include ?? []) {
252
+ if (packId.length === 0) {
253
+ continue;
254
+ }
255
+
256
+ if (!(packId in PACK_REGISTRY)) {
257
+ warnUnknownPackInInclude(packId);
258
+ }
259
+
260
+ packs.add(packId);
261
+ }
262
+
263
+ // Exclude: remove packs
264
+ for (const packId of config.packs?.exclude ?? []) {
265
+ packs.delete(packId);
266
+ }
267
+
268
+ // Return as sorted array for determinism
269
+ return Array.from(packs).sort();
270
+ }
271
+
272
+ /**
273
+ * Normalize rule name keys: accept both bare ("timestamp-must-specify-mode")
274
+ * and tsforge-prefixed ("tsforge/timestamp-must-specify-mode").
275
+ * Returns a map keyed by the BARE rule name.
276
+ */
277
+ function isSeverityOverride(value: unknown): value is "error" | "warn" | "off" {
278
+ return value === "error" || value === "warn" || value === "off";
279
+ }
280
+
281
+ export function normalizeRuleOverrides(
282
+ config: ITsforgeProjectConfig
283
+ ): Record<string, "error" | "warn" | "off"> {
284
+ const normalized: Record<string, "error" | "warn" | "off"> = {};
285
+
286
+ for (const [key, severity] of Object.entries(config.rules ?? {})) {
287
+ // Runtime data can violate the declared union (hand-built configs in tests,
288
+ // partially validated JSON) — re-check before trusting it.
289
+ if (!isSeverityOverride(severity)) {
290
+ continue;
291
+ }
292
+
293
+ const bareKey = key.startsWith("tsforge/") ? key.slice(8) : key;
294
+
295
+ if (bareKey.length > 0) {
296
+ normalized[bareKey] = severity;
297
+ }
298
+ }
299
+
300
+ return normalized;
301
+ }
@@ -0,0 +1,257 @@
1
+ import { join } from "node:path";
2
+
3
+ /**
4
+ * The baseline constitution — the deterministic gate floor tsforge brings to an
5
+ * under-gated TypeScript repo so "correct" has a meaning even where the repo
6
+ * ships none. Mirrors tsforge's OWN gate (strict `tsc` + the boringstack-derived
7
+ * eslint house rules + prettier), so a target fails the same way tsforge does.
8
+ *
9
+ * Bringing it is NON-DESTRUCTIVE: a repo's own config for a tool is authority,
10
+ * and we only write a file when nothing equivalent exists. We never install
11
+ * dependencies — `dependencies` lists what the written configs need so the
12
+ * caller can ensure them separately.
13
+ */
14
+
15
+ const TSCONFIG = `{
16
+ "compilerOptions": {
17
+ "target": "ES2022",
18
+ "module": "ES2022",
19
+ "moduleResolution": "bundler",
20
+ "esModuleInterop": true,
21
+ "forceConsistentCasingInFileNames": true,
22
+ "noEmit": true,
23
+
24
+ "strict": true,
25
+ "useUnknownInCatchVariables": true,
26
+ "noUnusedLocals": true,
27
+ "noUnusedParameters": true,
28
+ "noImplicitReturns": true,
29
+ "noFallthroughCasesInSwitch": true,
30
+ "noUncheckedIndexedAccess": true,
31
+ "noImplicitOverride": true,
32
+
33
+ "skipLibCheck": true
34
+ },
35
+ "include": ["**/*.ts", "**/*.tsx"],
36
+ "exclude": ["node_modules", "dist", "scratch"]
37
+ }
38
+ `;
39
+
40
+ const PRETTIER = `{
41
+ "semi": true,
42
+ "trailingComma": "es5",
43
+ "singleQuote": false,
44
+ "printWidth": 80,
45
+ "tabWidth": 2
46
+ }
47
+ `;
48
+
49
+ const ESLINT = `import pluginJs from "@eslint/js";
50
+ import tseslint from "typescript-eslint";
51
+ import configPrettier from "eslint-config-prettier";
52
+ import pluginPrettier from "eslint-plugin-prettier";
53
+
54
+ // Brought by tsforge: the general TypeScript quality rules (type-safety, async
55
+ // correctness, strict-boolean, I-prefixed interfaces, no-enum, prettier). Every
56
+ // rule is \`error\` — local green == CI green. Delete or relax in your own config.
57
+ export default tseslint.config(
58
+ { ignores: ["**/node_modules/**", "**/dist/**"] },
59
+ {
60
+ files: ["**/*.ts", "**/*.tsx"],
61
+ extends: [
62
+ pluginJs.configs.recommended,
63
+ ...tseslint.configs.strictTypeChecked,
64
+ ...tseslint.configs.stylisticTypeChecked,
65
+ configPrettier,
66
+ ],
67
+ languageOptions: {
68
+ parser: tseslint.parser,
69
+ parserOptions: {
70
+ projectService: true,
71
+ tsconfigRootDir: import.meta.dirname,
72
+ },
73
+ },
74
+ plugins: {
75
+ "@typescript-eslint": tseslint.plugin,
76
+ prettier: pluginPrettier,
77
+ },
78
+ rules: {
79
+ "prettier/prettier": "error",
80
+
81
+ // Idiomatic-output rules promoted to the GATE: over-annotation and
82
+ // string concatenation become deterministic errors, not reviewer notes.
83
+ "@typescript-eslint/no-inferrable-types": "error",
84
+ "prefer-template": "error",
85
+
86
+ "@typescript-eslint/no-explicit-any": "error",
87
+ "@typescript-eslint/no-non-null-assertion": "error",
88
+ "@typescript-eslint/consistent-type-assertions": [
89
+ "error",
90
+ { assertionStyle: "never" },
91
+ ],
92
+
93
+ "@typescript-eslint/no-floating-promises": "error",
94
+ "@typescript-eslint/no-misused-promises": "error",
95
+
96
+ "@typescript-eslint/strict-boolean-expressions": [
97
+ "error",
98
+ {
99
+ allowString: false,
100
+ allowNumber: false,
101
+ allowNullableObject: true,
102
+ allowNullableBoolean: false,
103
+ allowNullableString: false,
104
+ allowNullableNumber: false,
105
+ allowAny: false,
106
+ },
107
+ ],
108
+
109
+ "@typescript-eslint/naming-convention": [
110
+ "error",
111
+ { selector: "interface", format: ["PascalCase"], prefix: ["I"] },
112
+ { selector: "typeAlias", format: ["PascalCase"] },
113
+ { selector: "typeParameter", format: ["PascalCase"] },
114
+ {
115
+ selector: "variable",
116
+ format: ["camelCase", "UPPER_CASE", "PascalCase"],
117
+ leadingUnderscore: "allow",
118
+ },
119
+ { selector: "function", format: ["camelCase", "PascalCase"] },
120
+ { selector: ["objectLiteralProperty", "typeProperty"], format: null },
121
+ ],
122
+
123
+ eqeqeq: ["error", "always"],
124
+ curly: ["error", "all"],
125
+ "no-var": "error",
126
+ "prefer-const": "error",
127
+ "no-restricted-syntax": [
128
+ "error",
129
+ {
130
+ selector: "TSEnumDeclaration",
131
+ message: "Use 'as const' object literals instead of enums.",
132
+ },
133
+ ],
134
+ },
135
+ }
136
+ );
137
+ `;
138
+
139
+ interface IBaselineFile {
140
+ /** The file we write when nothing equivalent is present. */
141
+ path: string;
142
+ /** Any of these existing means the repo already has this tool's config. */
143
+ satisfiedBy: string[];
144
+ content: string;
145
+ }
146
+
147
+ const FILES: IBaselineFile[] = [
148
+ { path: "tsconfig.json", satisfiedBy: ["tsconfig.json"], content: TSCONFIG },
149
+ {
150
+ path: "eslint.config.js",
151
+ satisfiedBy: [
152
+ "eslint.config.js",
153
+ "eslint.config.mjs",
154
+ "eslint.config.cjs",
155
+ ".eslintrc",
156
+ ".eslintrc.js",
157
+ ".eslintrc.cjs",
158
+ ".eslintrc.json",
159
+ ],
160
+ content: ESLINT,
161
+ },
162
+ {
163
+ path: ".prettierrc.json",
164
+ satisfiedBy: [
165
+ ".prettierrc",
166
+ ".prettierrc.json",
167
+ ".prettierrc.js",
168
+ ".prettierrc.cjs",
169
+ "prettier.config.js",
170
+ "prettier.config.mjs",
171
+ ],
172
+ content: PRETTIER,
173
+ },
174
+ ];
175
+
176
+ /** What the brought configs need installed (we never install them ourselves). */
177
+ const DEPENDENCIES = [
178
+ "typescript",
179
+ "@eslint/js",
180
+ "eslint",
181
+ "typescript-eslint",
182
+ "eslint-config-prettier",
183
+ "eslint-plugin-prettier",
184
+ "prettier",
185
+ ];
186
+
187
+ export interface IConstitutionState {
188
+ tsconfig: boolean;
189
+ eslint: boolean;
190
+ prettier: boolean;
191
+ }
192
+
193
+ export interface IBroughtConstitution {
194
+ /** Config files written (were absent). */
195
+ written: string[];
196
+ /** Config files left alone (the repo already had one). */
197
+ skipped: string[];
198
+ /** npm dependencies the written configs require. */
199
+ dependencies: string[];
200
+ /** The gate command that runs the constitution. */
201
+ verify: string;
202
+ }
203
+
204
+ export interface IBringOptions {
205
+ /** Test command appended to the gate (default `bun test`). */
206
+ testCommand?: string;
207
+ }
208
+
209
+ /** Report which parts of the constitution the repo already has — writes nothing. */
210
+ export async function analyzeConstitution(
211
+ cwd: string
212
+ ): Promise<IConstitutionState> {
213
+ return {
214
+ tsconfig: await anyExists(cwd, FILES[0]?.satisfiedBy ?? []),
215
+ eslint: await anyExists(cwd, FILES[1]?.satisfiedBy ?? []),
216
+ prettier: await anyExists(cwd, FILES[2]?.satisfiedBy ?? []),
217
+ };
218
+ }
219
+
220
+ /** Materialize the missing parts of the gate floor into `cwd`, non-destructively. */
221
+ export async function bringConstitution(
222
+ cwd: string,
223
+ opts: IBringOptions = {}
224
+ ): Promise<IBroughtConstitution> {
225
+ const written: string[] = [];
226
+ const skipped: string[] = [];
227
+
228
+ for (const file of FILES) {
229
+ if (await anyExists(cwd, file.satisfiedBy)) {
230
+ skipped.push(file.path);
231
+ continue;
232
+ }
233
+
234
+ await Bun.write(join(cwd, file.path), file.content);
235
+ written.push(file.path);
236
+ }
237
+
238
+ const testCommand = opts.testCommand ?? "bun test";
239
+ const verify = [
240
+ "tsc --noEmit -p tsconfig.json",
241
+ "eslint .",
242
+ 'prettier --check "**/*.{ts,tsx}"',
243
+ testCommand,
244
+ ].join(" && ");
245
+
246
+ return { written, skipped, dependencies: DEPENDENCIES, verify };
247
+ }
248
+
249
+ async function anyExists(cwd: string, names: string[]): Promise<boolean> {
250
+ for (const name of names) {
251
+ if (await Bun.file(join(cwd, name)).exists()) {
252
+ return true;
253
+ }
254
+ }
255
+
256
+ return false;
257
+ }