@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.
- package/bin/tsforge.js +2 -0
- package/package.json +35 -0
- package/src/agent/agent.constants.ts +382 -0
- package/src/agent/agent.types.ts +34 -0
- package/src/agent/index.ts +4 -0
- package/src/agent/model-agent.ts +297 -0
- package/src/agent/tool-repair.ts +194 -0
- package/src/agent/tools.ts +190 -0
- package/src/browser/checks.ts +96 -0
- package/src/browser/index.ts +8 -0
- package/src/browser/oracle.ts +303 -0
- package/src/classify.ts +48 -0
- package/src/cli.ts +1333 -0
- package/src/config/config.constants.ts +9 -0
- package/src/config/flags.ts +32 -0
- package/src/config/index.ts +8 -0
- package/src/config/tsforge-config.ts +301 -0
- package/src/constitution/baseline.ts +257 -0
- package/src/detect-gate.ts +498 -0
- package/src/eval/eval.types.ts +36 -0
- package/src/eval/index.ts +3 -0
- package/src/eval/judge.ts +62 -0
- package/src/eval/score.ts +39 -0
- package/src/files/create.ts +22 -0
- package/src/files/edit.ts +193 -0
- package/src/files/files.constants.ts +11 -0
- package/src/files/files.types.ts +81 -0
- package/src/files/hashline-format.ts +110 -0
- package/src/files/hashline.ts +689 -0
- package/src/files/index.ts +19 -0
- package/src/index.ts +8 -0
- package/src/inference/index.ts +6 -0
- package/src/inference/inference.constants.ts +34 -0
- package/src/inference/inference.types.ts +123 -0
- package/src/inference/openai-compatible.ts +113 -0
- package/src/inference/stream-guard.ts +161 -0
- package/src/inference/stream.ts +370 -0
- package/src/inference/transport.ts +78 -0
- package/src/inference/wire.ts +0 -0
- package/src/lib/fs/fs.ts +126 -0
- package/src/lib/fs/fs.types.ts +5 -0
- package/src/lib/fs/index.ts +3 -0
- package/src/lib/fs/process.ts +146 -0
- package/src/lib/guards/guards.ts +9 -0
- package/src/lib/guards/index.ts +1 -0
- package/src/lib/json/index.ts +1 -0
- package/src/lib/json/json.ts +12 -0
- package/src/lib/scope/index.ts +2 -0
- package/src/lib/scope/scope.constants.ts +3 -0
- package/src/lib/scope/scope.ts +40 -0
- package/src/loop/astgrep-fix.ts +228 -0
- package/src/loop/feedback/feedback.ts +138 -0
- package/src/loop/feedback/index.ts +8 -0
- package/src/loop/feedback/meta-rule-docs.ts +41 -0
- package/src/loop/feedback/meta-rule-feedback.ts +61 -0
- package/src/loop/feedback/rule-docs.generated.json +112 -0
- package/src/loop/feedback/rule-docs.ts +342 -0
- package/src/loop/index.ts +19 -0
- package/src/loop/loop.constants.ts +68 -0
- package/src/loop/loop.types.ts +99 -0
- package/src/loop/prompt/index.ts +2 -0
- package/src/loop/prompt/project-map.ts +69 -0
- package/src/loop/prompt/prompt.ts +107 -0
- package/src/loop/quality.ts +174 -0
- package/src/loop/rule-docs.generated.json +367 -0
- package/src/loop/run-spec.ts +88 -0
- package/src/loop/run.ts +400 -0
- package/src/loop/session.ts +1410 -0
- package/src/loop/tools/add-dependency.ts +71 -0
- package/src/loop/tools/condense.ts +498 -0
- package/src/loop/tools/edit-hashline.ts +80 -0
- package/src/loop/tools/execute-tool.ts +80 -0
- package/src/loop/tools/file-ops.ts +323 -0
- package/src/loop/tools/index.ts +2 -0
- package/src/loop/tools/lsp-ops.ts +222 -0
- package/src/loop/tools/scaffold-routes.ts +68 -0
- package/src/loop/tools/scaffold-ui.ts +62 -0
- package/src/loop/tools/scaffold-web.ts +35 -0
- package/src/loop/tools/tool-context.ts +126 -0
- package/src/loop/ttsr-defaults.ts +53 -0
- package/src/loop/ttsr.ts +322 -0
- package/src/loop/turn.ts +856 -0
- package/src/lsp/index.ts +2 -0
- package/src/lsp/lsp.types.ts +56 -0
- package/src/lsp/service.ts +500 -0
- package/src/meta-rules/context.ts +195 -0
- package/src/meta-rules/index.ts +9 -0
- package/src/meta-rules/meta-rules.types.ts +47 -0
- package/src/meta-rules/parsers/package-json-parser.ts +51 -0
- package/src/meta-rules/registry.ts +37 -0
- package/src/meta-rules/rules/ci/workflow-actions-pinned.ts +59 -0
- package/src/meta-rules/rules/ci/workflow-runner-pinned.ts +57 -0
- package/src/meta-rules/rules/ci/workflow-timeout-required.ts +114 -0
- package/src/meta-rules/rules/config/tsconfig-paths-exist.ts +117 -0
- package/src/meta-rules/rules/config/tsconfig-strict.ts +91 -0
- package/src/meta-rules/rules/source-text/no-eslint-disable-comments.ts +34 -0
- package/src/meta-rules/rules/source-text/no-ts-suppressions.ts +38 -0
- package/src/meta-rules/rules/supply-chain/no-overlapping-libs.ts +57 -0
- package/src/meta-rules/rules/supply-chain/package-exact-deps.ts +55 -0
- package/src/meta-rules/rules/testing/test-sibling-required.ts +110 -0
- package/src/meta-rules/runner.ts +64 -0
- package/src/models-config.ts +196 -0
- package/src/render/ansi.ts +289 -0
- package/src/render/banner.ts +113 -0
- package/src/render/box.ts +134 -0
- package/src/render/index.ts +7 -0
- package/src/render/markdown.ts +123 -0
- package/src/render/render.types.ts +21 -0
- package/src/render/stream-markdown.ts +128 -0
- package/src/render/style.ts +26 -0
- package/src/rule-packs/bullmq/index.ts +39 -0
- package/src/rule-packs/bullmq/rules/index.ts +7 -0
- package/src/rule-packs/bullmq/rules/job-name-must-be-constant.ts +141 -0
- package/src/rule-packs/bullmq/rules/job-options-must-set-attempts.ts +174 -0
- package/src/rule-packs/bullmq/rules/no-blocking-concurrency-zero.ts +103 -0
- package/src/rule-packs/bullmq/rules/queue-options-must-set-removeoncomplete.ts +130 -0
- package/src/rule-packs/bullmq/rules/queue-options-must-set-removeonfail.ts +130 -0
- package/src/rule-packs/bullmq/rules/worker-must-implement-close.ts +182 -0
- package/src/rule-packs/bullmq/rules/worker-must-listen-failed.ts +140 -0
- package/src/rule-packs/bullmq/utils.ts +334 -0
- package/src/rule-packs/code-flow/index.ts +25 -0
- package/src/rule-packs/code-flow/rules/index.ts +3 -0
- package/src/rule-packs/code-flow/rules/no-bare-date-now.ts +138 -0
- package/src/rule-packs/code-flow/rules/no-template-trim-empty-ternary.ts +87 -0
- package/src/rule-packs/code-flow/rules/prefer-early-return.ts +80 -0
- package/src/rule-packs/code-flow/utils/prefer-early-return.ts +132 -0
- package/src/rule-packs/comment-hygiene/index.ts +25 -0
- package/src/rule-packs/comment-hygiene/rules/index.ts +3 -0
- package/src/rule-packs/comment-hygiene/rules/no-historical-comments.ts +102 -0
- package/src/rule-packs/comment-hygiene/rules/no-narration-comments.ts +83 -0
- package/src/rule-packs/comment-hygiene/rules/no-pr-reference-comments.ts +90 -0
- package/src/rule-packs/create-rule.ts +9 -0
- package/src/rule-packs/drizzle/index.ts +41 -0
- package/src/rule-packs/drizzle/rules/account-scoped-tables-require-where.ts +371 -0
- package/src/rule-packs/drizzle/rules/index.ts +8 -0
- package/src/rule-packs/drizzle/rules/no-nested-db-transaction.ts +127 -0
- package/src/rule-packs/drizzle/rules/no-raw-sql-outside-allowlist.ts +100 -0
- package/src/rule-packs/drizzle/rules/relations-must-cover-fks.ts +209 -0
- package/src/rule-packs/drizzle/rules/schema-files-must-not-import-driver.ts +127 -0
- package/src/rule-packs/drizzle/rules/schema-files-must-only-export-schema.ts +149 -0
- package/src/rule-packs/drizzle/rules/tables-must-have-timestamps.ts +312 -0
- package/src/rule-packs/drizzle/rules/timestamp-must-specify-mode.ts +166 -0
- package/src/rule-packs/drizzle/utils.ts +115 -0
- package/src/rule-packs/elysia/index.ts +43 -0
- package/src/rule-packs/elysia/rules/consistent-status-via-set.ts +69 -0
- package/src/rule-packs/elysia/rules/no-decorate-state-collision.ts +276 -0
- package/src/rule-packs/elysia/rules/no-separate-model-interfaces.ts +144 -0
- package/src/rule-packs/elysia/rules/prefer-destructured-context.ts +155 -0
- package/src/rule-packs/elysia/rules/prefer-direct-return.ts +176 -0
- package/src/rule-packs/elysia/rules/prefer-static-services.ts +159 -0
- package/src/rule-packs/elysia/rules/prefer-throw-status.ts +151 -0
- package/src/rule-packs/elysia/rules/require-hooks-before-routes.ts +209 -0
- package/src/rule-packs/elysia/rules/require-plugin-name.ts +107 -0
- package/src/rule-packs/elysia/utils/elysiaChain.ts +306 -0
- package/src/rule-packs/env-access/index.ts +23 -0
- package/src/rule-packs/env-access/rules/index.ts +2 -0
- package/src/rule-packs/env-access/rules/no-direct-process-env.ts +133 -0
- package/src/rule-packs/env-access/rules/no-process-exit.ts +95 -0
- package/src/rule-packs/i18n-keys/index.ts +19 -0
- package/src/rule-packs/i18n-keys/rules/static-translation-key-exists.ts +173 -0
- package/src/rule-packs/index.ts +139 -0
- package/src/rule-packs/jwt-cookies/index.ts +25 -0
- package/src/rule-packs/jwt-cookies/rules/auth-cookie-must-be-httponly.ts +150 -0
- package/src/rule-packs/jwt-cookies/rules/auth-cookie-must-be-secure-in-prod.ts +149 -0
- package/src/rule-packs/jwt-cookies/rules/bcrypt-rounds-min.ts +195 -0
- package/src/rule-packs/jwt-cookies/utils.ts +188 -0
- package/src/rule-packs/oauth-security/index.ts +25 -0
- package/src/rule-packs/oauth-security/rules/pkce-required-for-oidc.ts +296 -0
- package/src/rule-packs/oauth-security/rules/state-must-be-redis-backed.ts +193 -0
- package/src/rule-packs/oauth-security/rules/state-ttl-bounded.ts +219 -0
- package/src/rule-packs/oauth-security/utils.ts +127 -0
- package/src/rule-packs/react-component-architecture/index.ts +35 -0
- package/src/rule-packs/react-component-architecture/rules/component-folder-structure.ts +123 -0
- package/src/rule-packs/react-component-architecture/rules/forwardref-display-name.ts +93 -0
- package/src/rule-packs/react-component-architecture/rules/index-must-reexport-default.ts +123 -0
- package/src/rule-packs/react-component-architecture/rules/max-hooks-per-file.ts +122 -0
- package/src/rule-packs/react-component-architecture/rules/no-cross-feature-imports.ts +170 -0
- package/src/rule-packs/react-component-architecture/rules/no-inline-jsx-functions.ts +66 -0
- package/src/rule-packs/react-component-architecture/utils.ts +47 -0
- package/src/rule-packs/rule-packs.types.ts +18 -0
- package/src/rule-packs/structured-logging/index.ts +26 -0
- package/src/rule-packs/structured-logging/rules/mask-pii-fields.ts +221 -0
- package/src/rule-packs/structured-logging/rules/no-error-stringify.ts +217 -0
- package/src/rule-packs/structured-logging/rules/require-event-field.ts +136 -0
- package/src/rule-packs/structured-logging/utils/logger.ts +104 -0
- package/src/rule-packs/tanstack-query/index.ts +20 -0
- package/src/rule-packs/tanstack-query/rules/prefix-query-key-must-use-set-queries-data.ts +321 -0
- package/src/rule-packs/test-conventions/index.ts +23 -0
- package/src/rule-packs/test-conventions/rules/index.ts +2 -0
- package/src/rule-packs/test-conventions/rules/no-focused-tests.ts +170 -0
- package/src/rule-packs/test-conventions/rules/test-file-mirrors-source.ts +127 -0
- package/src/rule-packs/utils.ts +142 -0
- package/src/session-store.ts +359 -0
- package/src/spec/generate-tests.ts +213 -0
- package/src/spec/index.ts +5 -0
- package/src/spec/parse.ts +152 -0
- package/src/spec/review-tests.ts +162 -0
- package/src/spec/spec.constants.ts +13 -0
- package/src/spec/spec.types.ts +79 -0
- package/src/stack-detection/detect.ts +246 -0
- package/src/stack-detection/index.ts +3 -0
- package/src/stack-detection/packs.ts +174 -0
- package/src/stack-detection/stack-detection.types.ts +47 -0
- package/src/validate/accept.ts +49 -0
- package/src/validate/errors.ts +35 -0
- package/src/validate/index.ts +12 -0
- package/src/validate/parse.ts +148 -0
- package/src/validate/run-tests.ts +59 -0
- package/src/validate/validate.ts +40 -0
- package/src/validate/validate.types.ts +52 -0
- package/src/web-components.ts +638 -0
- package/src/web-coverage.ts +89 -0
- package/src/web-routes.ts +151 -0
- package/src/web-templates.ts +1011 -0
- package/strict.eslint.config.mjs +84 -0
- package/strict.web.eslint.config.mjs +185 -0
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { AST_NODE_TYPES, type TSESTree } from "@typescript-eslint/utils";
|
|
2
|
+
import type { SourceCode } from "@typescript-eslint/utils/ts-eslint";
|
|
3
|
+
|
|
4
|
+
const FLIPPABLE_OPERATORS = new Map<string, string>([
|
|
5
|
+
["===", "!=="],
|
|
6
|
+
["!==", "==="],
|
|
7
|
+
["==", "!="],
|
|
8
|
+
["!=", "=="],
|
|
9
|
+
["<", ">="],
|
|
10
|
+
[">", "<="],
|
|
11
|
+
["<=", ">"],
|
|
12
|
+
[">=", "<"],
|
|
13
|
+
]);
|
|
14
|
+
|
|
15
|
+
const MIN_CONSEQUENT_STATEMENTS = 2;
|
|
16
|
+
|
|
17
|
+
export function getFunctionBlockBody(
|
|
18
|
+
node:
|
|
19
|
+
| TSESTree.FunctionDeclaration
|
|
20
|
+
| TSESTree.FunctionExpression
|
|
21
|
+
| TSESTree.ArrowFunctionExpression
|
|
22
|
+
): TSESTree.BlockStatement | null {
|
|
23
|
+
if (node.body.type !== AST_NODE_TYPES.BlockStatement) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return node.body;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function findWrappedHappyPathIf(
|
|
31
|
+
body: TSESTree.BlockStatement
|
|
32
|
+
): TSESTree.IfStatement | null {
|
|
33
|
+
if (body.body.length === 0) {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const lastStatement = body.body[body.body.length - 1];
|
|
38
|
+
|
|
39
|
+
if (lastStatement?.type !== AST_NODE_TYPES.IfStatement) {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (lastStatement.alternate !== null) {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (lastStatement.consequent.type !== AST_NODE_TYPES.BlockStatement) {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (lastStatement.consequent.body.length < MIN_CONSEQUENT_STATEMENTS) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return lastStatement;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function negateTestExpression(
|
|
59
|
+
sourceCode: SourceCode,
|
|
60
|
+
test: TSESTree.Expression
|
|
61
|
+
): string {
|
|
62
|
+
if (test.type === AST_NODE_TYPES.UnaryExpression && test.operator === "!") {
|
|
63
|
+
return sourceCode.getText(test.argument);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (
|
|
67
|
+
test.type === AST_NODE_TYPES.Identifier ||
|
|
68
|
+
test.type === AST_NODE_TYPES.MemberExpression ||
|
|
69
|
+
test.type === "ChainExpression"
|
|
70
|
+
) {
|
|
71
|
+
return `!${sourceCode.getText(test)}`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (test.type === AST_NODE_TYPES.BinaryExpression) {
|
|
75
|
+
const flipped = FLIPPABLE_OPERATORS.get(test.operator);
|
|
76
|
+
|
|
77
|
+
if (flipped !== undefined) {
|
|
78
|
+
return `${sourceCode.getText(test.left)} ${flipped} ${sourceCode.getText(test.right)}`;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (test.type === AST_NODE_TYPES.LogicalExpression) {
|
|
83
|
+
if (test.operator === "&&") {
|
|
84
|
+
return `${negateOperand(sourceCode, test.left)} || ${negateOperand(sourceCode, test.right)}`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (test.operator === "||") {
|
|
88
|
+
return `${negateOperand(sourceCode, test.left)} && ${negateOperand(sourceCode, test.right)}`;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return `!(${sourceCode.getText(test)})`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function negateOperand(
|
|
96
|
+
sourceCode: SourceCode,
|
|
97
|
+
node: TSESTree.Expression
|
|
98
|
+
): string {
|
|
99
|
+
const negated = negateTestExpression(sourceCode, node);
|
|
100
|
+
|
|
101
|
+
if (needsParentheses(node)) {
|
|
102
|
+
return `(${negated})`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return negated;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function needsParentheses(node: TSESTree.Expression): boolean {
|
|
109
|
+
return (
|
|
110
|
+
node.type === AST_NODE_TYPES.BinaryExpression ||
|
|
111
|
+
node.type === AST_NODE_TYPES.LogicalExpression ||
|
|
112
|
+
node.type === AST_NODE_TYPES.ConditionalExpression ||
|
|
113
|
+
node.type === AST_NODE_TYPES.AssignmentExpression
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function buildGuardClauseReplacement(
|
|
118
|
+
sourceCode: SourceCode,
|
|
119
|
+
ifStatement: TSESTree.IfStatement
|
|
120
|
+
): string | null {
|
|
121
|
+
if (ifStatement.consequent.type !== AST_NODE_TYPES.BlockStatement) {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const invertedTest = negateTestExpression(sourceCode, ifStatement.test);
|
|
126
|
+
const guardClause = `if (${invertedTest}) {\n return;\n}\n`;
|
|
127
|
+
const hoistedBody = ifStatement.consequent.body
|
|
128
|
+
.map((statement) => sourceCode.getText(statement))
|
|
129
|
+
.join("\n");
|
|
130
|
+
|
|
131
|
+
return `${guardClause}\n${hoistedBody}`;
|
|
132
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { TSESLint } from "@typescript-eslint/utils";
|
|
2
|
+
|
|
3
|
+
import { noHistoricalCommentsRule } from "./rules/no-historical-comments";
|
|
4
|
+
import { noNarrationCommentsRule } from "./rules/no-narration-comments";
|
|
5
|
+
import { noPrReferenceCommentsRule } from "./rules/no-pr-reference-comments";
|
|
6
|
+
import type { IRulePack } from "../rule-packs.types";
|
|
7
|
+
|
|
8
|
+
const rules: Record<string, TSESLint.RuleModule<string, readonly unknown[]>> = {
|
|
9
|
+
"no-historical-comments": noHistoricalCommentsRule,
|
|
10
|
+
"no-narration-comments": noNarrationCommentsRule,
|
|
11
|
+
"no-pr-reference-comments": noPrReferenceCommentsRule,
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const commentHygienePack: IRulePack = {
|
|
15
|
+
id: "comment-hygiene",
|
|
16
|
+
description: "Meaningful comments and documentation standards",
|
|
17
|
+
rules,
|
|
18
|
+
rulesConfig: {
|
|
19
|
+
"no-historical-comments": "error",
|
|
20
|
+
"no-narration-comments": "error",
|
|
21
|
+
"no-pr-reference-comments": "error",
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export default commentHygienePack;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import type { TSESTree } from "@typescript-eslint/utils";
|
|
2
|
+
|
|
3
|
+
import { createRule } from "../../create-rule";
|
|
4
|
+
|
|
5
|
+
export const RULE_NAME = "no-historical-comments";
|
|
6
|
+
|
|
7
|
+
type MessageIds = "historicalComment";
|
|
8
|
+
|
|
9
|
+
/*
|
|
10
|
+
* Patterns that frame code relative to what it USED TO be or to a past
|
|
11
|
+
* incident, instead of describing the current invariant. Each entry is
|
|
12
|
+
* deliberately narrow — bare "now" / "legacy" / "previously" appear in
|
|
13
|
+
* legitimate technical prose, so the rule only matches the specific
|
|
14
|
+
* historical-narration constructions that have to come out.
|
|
15
|
+
*/
|
|
16
|
+
const HISTORICAL_PATTERNS: readonly RegExp[] = [
|
|
17
|
+
/\bcodex\s+flagged\b/iu,
|
|
18
|
+
/\bbefore\s+the\s+fix\b/iu,
|
|
19
|
+
/\bafter\s+the\s+fix\b/iu,
|
|
20
|
+
/\bbefore\s+the\s+refactor\b/iu,
|
|
21
|
+
/\bafter\s+the\s+refactor\b/iu,
|
|
22
|
+
/\bthe\s+[a-z][\w-]*\s+refactor\b/iu,
|
|
23
|
+
/\bwe\s+used\s+to\b/iu,
|
|
24
|
+
/\bthis\s+used\s+to\b/iu,
|
|
25
|
+
/\bused\s+to\s+be\b/iu,
|
|
26
|
+
/\bno\s+longer\b/iu,
|
|
27
|
+
/\bkept\s+for\s+(?:backwards|backward|legacy|compat)\b/iu,
|
|
28
|
+
/\b(?:was|were)\s+a\s+(?:footgun|bug)\b/iu,
|
|
29
|
+
/\bhistorical(?:ly)?\b/iu,
|
|
30
|
+
/\balpine[-\s]?era\b/iu,
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
function commentText(comment: TSESTree.Comment): string {
|
|
34
|
+
if (comment.type === "Line") {
|
|
35
|
+
return comment.value.trim();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return comment.value
|
|
39
|
+
.split("\n")
|
|
40
|
+
.map((line) => line.replace(/^\s*\*?/u, ""))
|
|
41
|
+
.join(" ")
|
|
42
|
+
.trim();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function looksLikeJsDoc(comment: TSESTree.Comment): boolean {
|
|
46
|
+
return comment.type === "Block" && comment.value.startsWith("*");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export const noHistoricalCommentsRule = createRule<[], MessageIds>({
|
|
50
|
+
name: RULE_NAME,
|
|
51
|
+
meta: {
|
|
52
|
+
type: "suggestion",
|
|
53
|
+
docs: {
|
|
54
|
+
description:
|
|
55
|
+
"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.",
|
|
56
|
+
},
|
|
57
|
+
schema: [],
|
|
58
|
+
messages: {
|
|
59
|
+
historicalComment:
|
|
60
|
+
"Historical narration ({{snippet}}). Source comments describe the current invariant, not what the code used to do — move the history to the commit message or delete the comment.",
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
defaultOptions: [],
|
|
64
|
+
create(context) {
|
|
65
|
+
return {
|
|
66
|
+
Program() {
|
|
67
|
+
const comments = context.sourceCode.getAllComments();
|
|
68
|
+
|
|
69
|
+
for (const comment of comments) {
|
|
70
|
+
if (looksLikeJsDoc(comment)) {
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const text = commentText(comment);
|
|
75
|
+
|
|
76
|
+
if (text === "") {
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
for (const pattern of HISTORICAL_PATTERNS) {
|
|
81
|
+
const match = pattern.exec(text);
|
|
82
|
+
|
|
83
|
+
if (match !== null) {
|
|
84
|
+
const matchedPhrase = match[0];
|
|
85
|
+
const snippet =
|
|
86
|
+
matchedPhrase.length > 40
|
|
87
|
+
? `${matchedPhrase.slice(0, 40)}…`
|
|
88
|
+
: matchedPhrase;
|
|
89
|
+
|
|
90
|
+
context.report({
|
|
91
|
+
loc: comment.loc,
|
|
92
|
+
messageId: "historicalComment",
|
|
93
|
+
data: { snippet },
|
|
94
|
+
});
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
},
|
|
102
|
+
});
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { TSESTree } from "@typescript-eslint/utils";
|
|
2
|
+
|
|
3
|
+
import { createRule } from "../../create-rule";
|
|
4
|
+
|
|
5
|
+
export const RULE_NAME = "no-narration-comments";
|
|
6
|
+
|
|
7
|
+
type MessageIds = "narrationComment";
|
|
8
|
+
|
|
9
|
+
const NARRATION_PATTERNS: readonly RegExp[] = [
|
|
10
|
+
/^here\s+we\b/iu,
|
|
11
|
+
/^now\s+we\b/iu,
|
|
12
|
+
/^first[,]?\s+(?:we\b|[a-z])/iu,
|
|
13
|
+
/^then[,]?\s+(?:we\b|[a-z])/iu,
|
|
14
|
+
/^next[,]?\s+(?:we\b|[a-z])/iu,
|
|
15
|
+
/^finally[,]?\s+(?:we\b|[a-z])/iu,
|
|
16
|
+
/^let's\b/iu,
|
|
17
|
+
/^let\s+me\b/iu,
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
function commentText(comment: TSESTree.Comment): string {
|
|
21
|
+
if (comment.type === "Line") {
|
|
22
|
+
return comment.value.trim();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return comment.value
|
|
26
|
+
.split("\n")
|
|
27
|
+
.map((line) => line.replace(/^\s*\*?/u, ""))
|
|
28
|
+
.join(" ")
|
|
29
|
+
.trim();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function looksLikeJsDoc(comment: TSESTree.Comment): boolean {
|
|
33
|
+
return comment.type === "Block" && comment.value.startsWith("*");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const noNarrationCommentsRule = createRule<[], MessageIds>({
|
|
37
|
+
name: RULE_NAME,
|
|
38
|
+
meta: {
|
|
39
|
+
type: "suggestion",
|
|
40
|
+
docs: {
|
|
41
|
+
description:
|
|
42
|
+
"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.",
|
|
43
|
+
},
|
|
44
|
+
schema: [],
|
|
45
|
+
messages: {
|
|
46
|
+
narrationComment:
|
|
47
|
+
"Narrative comment ({{snippet}}). Describe the WHY, not the sequence of operations — or delete the comment.",
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
defaultOptions: [],
|
|
51
|
+
create(context) {
|
|
52
|
+
return {
|
|
53
|
+
Program() {
|
|
54
|
+
const comments = context.sourceCode.getAllComments();
|
|
55
|
+
|
|
56
|
+
for (const comment of comments) {
|
|
57
|
+
if (looksLikeJsDoc(comment)) {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const text = commentText(comment);
|
|
62
|
+
|
|
63
|
+
if (text === "") {
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
for (const pattern of NARRATION_PATTERNS) {
|
|
68
|
+
if (pattern.test(text)) {
|
|
69
|
+
const snippet = text.length > 40 ? `${text.slice(0, 40)}…` : text;
|
|
70
|
+
|
|
71
|
+
context.report({
|
|
72
|
+
loc: comment.loc,
|
|
73
|
+
messageId: "narrationComment",
|
|
74
|
+
data: { snippet },
|
|
75
|
+
});
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
},
|
|
83
|
+
});
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import type { TSESTree } from "@typescript-eslint/utils";
|
|
2
|
+
|
|
3
|
+
import { createRule } from "../../create-rule";
|
|
4
|
+
|
|
5
|
+
export const RULE_NAME = "no-pr-reference-comments";
|
|
6
|
+
|
|
7
|
+
type MessageIds = "prReferenceComment";
|
|
8
|
+
|
|
9
|
+
interface IPrPattern {
|
|
10
|
+
readonly pattern: RegExp;
|
|
11
|
+
readonly label: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const PR_PATTERNS: readonly IPrPattern[] = [
|
|
15
|
+
{
|
|
16
|
+
pattern: /https?:\/\/github\.com\/[^\s)]+\/(?:pull|issues)\/\d+/iu,
|
|
17
|
+
label: "GitHub PR/issue URL",
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
pattern: /\b(?:see|closes?|fixes|fixed|addresses|resolves?|refs?)\s+#\d+/iu,
|
|
21
|
+
label: "issue/PR reference",
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
pattern: /\bPRs?\s+#?\d+/iu,
|
|
25
|
+
label: "PR number",
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
pattern: /(?:^|[\s(])#\d+\b/u,
|
|
29
|
+
label: "issue/PR number",
|
|
30
|
+
},
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
function commentText(comment: TSESTree.Comment): string {
|
|
34
|
+
if (comment.type === "Line") {
|
|
35
|
+
return comment.value;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return comment.value;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export const noPrReferenceCommentsRule = createRule<[], MessageIds>({
|
|
42
|
+
name: RULE_NAME,
|
|
43
|
+
meta: {
|
|
44
|
+
type: "suggestion",
|
|
45
|
+
docs: {
|
|
46
|
+
description:
|
|
47
|
+
"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.",
|
|
48
|
+
},
|
|
49
|
+
schema: [],
|
|
50
|
+
messages: {
|
|
51
|
+
prReferenceComment:
|
|
52
|
+
"Comment contains {{label}} ({{snippet}}). Move it to the commit message or PR description — the git log is the canonical place for repo-history references.",
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
defaultOptions: [],
|
|
56
|
+
create(context) {
|
|
57
|
+
return {
|
|
58
|
+
Program() {
|
|
59
|
+
const comments = context.sourceCode.getAllComments();
|
|
60
|
+
|
|
61
|
+
for (const comment of comments) {
|
|
62
|
+
const text = commentText(comment);
|
|
63
|
+
|
|
64
|
+
if (text.trim() === "") {
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
for (const { pattern, label } of PR_PATTERNS) {
|
|
69
|
+
const match = pattern.exec(text);
|
|
70
|
+
|
|
71
|
+
if (match === null) {
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const matched = match[0].trim();
|
|
76
|
+
const snippet =
|
|
77
|
+
matched.length > 40 ? `${matched.slice(0, 40)}…` : matched;
|
|
78
|
+
|
|
79
|
+
context.report({
|
|
80
|
+
loc: comment.loc,
|
|
81
|
+
messageId: "prReferenceComment",
|
|
82
|
+
data: { label, snippet },
|
|
83
|
+
});
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
},
|
|
90
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { ESLintUtils } from "@typescript-eslint/utils";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* RuleCreator for tsforge rules. Generates docs URLs pointing to GitHub.
|
|
5
|
+
*/
|
|
6
|
+
export const createRule = ESLintUtils.RuleCreator(
|
|
7
|
+
(ruleName) =>
|
|
8
|
+
`https://github.com/tsforge/tsforge/blob/main/docs/rules/${ruleName}.md`
|
|
9
|
+
);
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { TSESLint } from "@typescript-eslint/utils";
|
|
2
|
+
|
|
3
|
+
import { accountScopedTablesRequireWhereRule } from "./rules/account-scoped-tables-require-where";
|
|
4
|
+
import { noNestedDbTransactionRule } from "./rules/no-nested-db-transaction";
|
|
5
|
+
import { noRawSqlOutsideAllowlistRule } from "./rules/no-raw-sql-outside-allowlist";
|
|
6
|
+
import { relationsMustCoverFksRule } from "./rules/relations-must-cover-fks";
|
|
7
|
+
import { schemaFilesMustNotImportDriverRule } from "./rules/schema-files-must-not-import-driver";
|
|
8
|
+
import { schemaFilesMustOnlyExportSchemaRule } from "./rules/schema-files-must-only-export-schema";
|
|
9
|
+
import { tablesMustHaveTimestampsRule } from "./rules/tables-must-have-timestamps";
|
|
10
|
+
import { timestampMustSpecifyModeRule } from "./rules/timestamp-must-specify-mode";
|
|
11
|
+
import type { IRulePack } from "../rule-packs.types";
|
|
12
|
+
|
|
13
|
+
const rules: Record<string, TSESLint.RuleModule<string, readonly unknown[]>> = {
|
|
14
|
+
"account-scoped-tables-require-where": accountScopedTablesRequireWhereRule,
|
|
15
|
+
"no-nested-db-transaction": noNestedDbTransactionRule,
|
|
16
|
+
"no-raw-sql-outside-allowlist": noRawSqlOutsideAllowlistRule,
|
|
17
|
+
"relations-must-cover-fks": relationsMustCoverFksRule,
|
|
18
|
+
"schema-files-must-not-import-driver": schemaFilesMustNotImportDriverRule,
|
|
19
|
+
"schema-files-must-only-export-schema": schemaFilesMustOnlyExportSchemaRule,
|
|
20
|
+
"tables-must-have-timestamps": tablesMustHaveTimestampsRule,
|
|
21
|
+
"timestamp-must-specify-mode": timestampMustSpecifyModeRule,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const drizzlePack: IRulePack = {
|
|
25
|
+
id: "drizzle",
|
|
26
|
+
description:
|
|
27
|
+
"Database access patterns using Drizzle ORM (schema safety, query scoping, transaction safety)",
|
|
28
|
+
rules,
|
|
29
|
+
rulesConfig: {
|
|
30
|
+
"account-scoped-tables-require-where": "error",
|
|
31
|
+
"no-nested-db-transaction": "error",
|
|
32
|
+
"no-raw-sql-outside-allowlist": "error",
|
|
33
|
+
"relations-must-cover-fks": "error",
|
|
34
|
+
"schema-files-must-not-import-driver": "error",
|
|
35
|
+
"schema-files-must-only-export-schema": "warn",
|
|
36
|
+
"tables-must-have-timestamps": "warn",
|
|
37
|
+
"timestamp-must-specify-mode": "error",
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export default drizzlePack;
|