activate-agentmd 2.4.0 → 2.5.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/README.md +11 -1
- package/bin/agentmd.js +9 -2
- package/package.json +1 -1
- package/src/commands/bundle.js +151 -0
- package/src/commands/ci.js +6 -1
- package/src/commands/lint.js +84 -0
- package/src/commands/pins.js +43 -0
- package/src/commands/review.js +4 -2
- package/src/commands/test.js +37 -17
- package/src/detect.json +1 -0
- package/src/lib/agents.js +18 -55
- package/src/lib/analytics.js +6 -0
- package/src/lib/bundle.js +169 -0
- package/src/lib/enterprise.js +7 -1
- package/src/lib/license.js +15 -2
- package/src/lib/lint-text.js +150 -0
- package/src/lib/lint.js +213 -0
- package/src/lib/manifest.js +8 -0
- package/src/lib/patterns.js +11 -14
- package/src/lib/pro.js +1 -1
- package/src/lib/telemetry.js +1 -1
- package/src/lib/versions.js +56 -103
- package/src/pins.json +403 -0
package/src/lib/patterns.js
CHANGED
|
@@ -26,12 +26,6 @@ const PATTERNS = [
|
|
|
26
26
|
test: /\bSELECT\s+\*\s+FROM\b/i,
|
|
27
27
|
message: "`SELECT *` — name the columns you need.",
|
|
28
28
|
},
|
|
29
|
-
{
|
|
30
|
-
id: "pydantic-v1-api", standard: "Backend/pydantic", severity: "medium", files: [".py"],
|
|
31
|
-
test: /\.(dict|json|parse_obj|parse_raw)\(|@(validator|root_validator)\b|class Config:/,
|
|
32
|
-
not: /model_dump|model_validate/,
|
|
33
|
-
message: "Pydantic v1 API (`.dict()`, `.json()`, `@validator`, `class Config`) — use `model_dump`, `model_validate`, `field_validator`, `ConfigDict`.",
|
|
34
|
-
},
|
|
35
29
|
{
|
|
36
30
|
id: "eval-exec", standard: "Security/command-injection", severity: "high",
|
|
37
31
|
files: [".js", ".ts", ".tsx", ".py", ".rb", ".php"],
|
|
@@ -82,11 +76,6 @@ const PATTERNS = [
|
|
|
82
76
|
not: /process\.env|os\.environ|getenv|\$\{|<[A-Z_]+>|example|placeholder|changeme|xxx/i,
|
|
83
77
|
message: "Looks like a hard-coded secret — read it from the environment or a secret store.",
|
|
84
78
|
},
|
|
85
|
-
{
|
|
86
|
-
id: "next-sync-request-api", standard: "Frontend/nextjs", severity: "medium", files: [".ts", ".tsx"],
|
|
87
|
-
test: /\b(cookies|headers)\(\)\.(get|set|getAll|has)\(/,
|
|
88
|
-
message: "Next.js 15+: `cookies()` / `headers()` are async — `(await cookies()).get(...)`.",
|
|
89
|
-
},
|
|
90
79
|
{
|
|
91
80
|
id: "console-log-in-server", standard: "Backend/logging", severity: "low", files: [".ts", ".js"],
|
|
92
81
|
test: /^\s*console\.log\(/,
|
|
@@ -94,12 +83,17 @@ const PATTERNS = [
|
|
|
94
83
|
},
|
|
95
84
|
];
|
|
96
85
|
|
|
86
|
+
// `pydantic-v1-api` and `next-sync-request-api` used to live here and fired
|
|
87
|
+
// on every project — flagging `.dict()` in a repo pinned to Pydantic v1, where
|
|
88
|
+
// it is the correct call. They are version checks now, in pins.json, and run
|
|
89
|
+
// only when that major is detected (see versions.js pinPatterns).
|
|
90
|
+
|
|
97
91
|
/**
|
|
98
92
|
* Run the patterns over a unified diff. Only added lines are checked, and the
|
|
99
93
|
* file's extension must be in the pattern's list. Returns review-shaped
|
|
100
94
|
* findings: { file, line, severity, standard, rule, issue, fix, pattern }.
|
|
101
95
|
*/
|
|
102
|
-
function runPatterns(diff, installedStandardIds) {
|
|
96
|
+
function runPatterns(diff, installedStandardIds, patterns = PATTERNS) {
|
|
103
97
|
const findings = [];
|
|
104
98
|
let file = null, ext = "", line = 0;
|
|
105
99
|
for (const raw of diff.split("\n")) {
|
|
@@ -119,13 +113,16 @@ function runPatterns(diff, installedStandardIds) {
|
|
|
119
113
|
if (!raw.startsWith("+")) continue;
|
|
120
114
|
const code = raw.slice(1);
|
|
121
115
|
if (/^\s*(\/\/|#|\*|<!--)/.test(code)) continue; // comments
|
|
122
|
-
for (const p of
|
|
116
|
+
for (const p of patterns) {
|
|
123
117
|
if (!p.files.includes(ext)) continue;
|
|
124
118
|
if (!p.test.test(code)) continue;
|
|
125
119
|
if (p.not && p.not.test(code)) continue;
|
|
126
120
|
// Only report against standards the project actually installed, unless
|
|
127
121
|
// the caller passes null to mean "everything".
|
|
128
|
-
|
|
122
|
+
// Pin checks are scoped by the detected version instead: whether the
|
|
123
|
+
// project installed a Next.js standard says nothing about whether it
|
|
124
|
+
// runs Next 16.
|
|
125
|
+
if (!p.pin && installedStandardIds && !installedStandardIds.has(p.standard)) continue;
|
|
129
126
|
findings.push({ file, line, severity: p.severity, standard: p.standard, rule: p.id, issue: p.message, fix: null, pattern: true });
|
|
130
127
|
}
|
|
131
128
|
}
|
package/src/lib/pro.js
CHANGED
|
@@ -21,7 +21,7 @@ const { verify, fingerprint, daysLeft } = require("./license");
|
|
|
21
21
|
const { resolveKey } = require("./credentials");
|
|
22
22
|
const { cmd } = require("./invocation");
|
|
23
23
|
|
|
24
|
-
const UPGRADE_URL = process.env.AGENTMD_PRO_URL || "https://
|
|
24
|
+
const UPGRADE_URL = process.env.AGENTMD_PRO_URL || "https://agentmd.pages.dev/pro";
|
|
25
25
|
|
|
26
26
|
/**
|
|
27
27
|
* Every gated capability, with the sentence shown when it is reached without
|
package/src/lib/telemetry.js
CHANGED
|
@@ -28,7 +28,7 @@ const crypto = require("crypto");
|
|
|
28
28
|
const { configDir } = require("./credentials");
|
|
29
29
|
|
|
30
30
|
const FILE = "telemetry.json";
|
|
31
|
-
const ENDPOINT = process.env.AGENTMD_TELEMETRY_URL || "https://
|
|
31
|
+
const ENDPOINT = process.env.AGENTMD_TELEMETRY_URL || "https://agentmd.pages.dev/api/telemetry";
|
|
32
32
|
const settingsPath = () => path.join(configDir(), FILE);
|
|
33
33
|
|
|
34
34
|
/** Exactly what may be sent. A field not in here cannot reach the wire. */
|
package/src/lib/versions.js
CHANGED
|
@@ -20,112 +20,31 @@
|
|
|
20
20
|
const fs = require("fs");
|
|
21
21
|
const path = require("path");
|
|
22
22
|
|
|
23
|
-
/**
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
16: [
|
|
35
|
-
"Turbopack is the default bundler; do not add webpack-only config without checking it is still needed.",
|
|
36
|
-
"Request APIs are async only — `params`, `searchParams`, `cookies()`, `headers()` must be awaited; synchronous access was removed.",
|
|
37
|
-
"`middleware.ts` is `proxy.ts`; the old filename is not picked up.",
|
|
38
|
-
"Ships React 19: `ref` is a prop, `use()` and `useActionState` are available.",
|
|
39
|
-
],
|
|
40
|
-
},
|
|
41
|
-
react: {
|
|
42
|
-
label: "React",
|
|
43
|
-
18: ["Mount with `createRoot` (not `ReactDOM.render`). Effects double-invoke in StrictMode during development — write them idempotently."],
|
|
44
|
-
19: [
|
|
45
|
-
"`ref` is a regular prop on function components — do not use `forwardRef`.",
|
|
46
|
-
"Use `use()` to read promises/context, `useActionState` and `useFormStatus` for forms; Actions can be passed to `<form action>`.",
|
|
47
|
-
"`propTypes` and `defaultProps` on function components are ignored; use TypeScript and default parameters.",
|
|
48
|
-
],
|
|
49
|
-
},
|
|
50
|
-
pydantic: {
|
|
51
|
-
label: "Pydantic",
|
|
52
|
-
1: ["Pydantic v1 API: `.dict()`, `.json()`, `@validator`, `class Config`. Do not use v2 names (`model_dump`, `field_validator`)."],
|
|
53
|
-
2: [
|
|
54
|
-
"Use `.model_dump()` / `.model_dump_json()` / `.model_validate()` — `.dict()`, `.json()`, `.parse_obj()` are deprecated v1 names.",
|
|
55
|
-
"Validators are `@field_validator` and `@model_validator`; `@validator` and `@root_validator` are deprecated.",
|
|
56
|
-
"Configure with `model_config = ConfigDict(...)`, not an inner `class Config`. Use `TypeAdapter` for non-model types.",
|
|
57
|
-
],
|
|
58
|
-
},
|
|
59
|
-
sqlalchemy: {
|
|
60
|
-
label: "SQLAlchemy",
|
|
61
|
-
2: [
|
|
62
|
-
"Use 2.0 style: `select()` with `session.execute()` / `session.scalars()`; `session.query()` is legacy.",
|
|
63
|
-
"Declare models with `DeclarativeBase`, `Mapped[...]` and `mapped_column()`; `declarative_base()` and `Column` are legacy.",
|
|
64
|
-
],
|
|
65
|
-
},
|
|
66
|
-
django: {
|
|
67
|
-
label: "Django",
|
|
68
|
-
5: ["Django 5: `db_default` and `GeneratedField` exist; `USE_L10N` was removed; async ORM methods (`aget`, `acreate`, `afirst`) are available."],
|
|
69
|
-
},
|
|
70
|
-
prisma: {
|
|
71
|
-
label: "Prisma",
|
|
72
|
-
5: ["Prisma 5: `prisma.$extends` for extensions; `@prisma/client` generated in `node_modules` — run `prisma generate` after schema changes."],
|
|
73
|
-
6: ["Prisma 6: same API as 5; `omit` in queries is stable. Run `prisma generate` after schema changes."],
|
|
74
|
-
7: [
|
|
75
|
-
"Prisma 7 has no Rust query engine by default: instantiate the client with a driver adapter (for example `@prisma/adapter-pg`), not a bare `new PrismaClient()`.",
|
|
76
|
-
"Connection config lives in `prisma.config.ts`, not in the schema's `datasource url`.",
|
|
77
|
-
],
|
|
78
|
-
},
|
|
79
|
-
tailwindcss: {
|
|
80
|
-
label: "Tailwind CSS",
|
|
81
|
-
3: ["Tailwind v3: `tailwind.config.js` with `content` globs, `@tailwind base/components/utilities` in CSS."],
|
|
82
|
-
4: [
|
|
83
|
-
"Tailwind v4 is CSS-first: `@import \"tailwindcss\"`, design tokens in `@theme`, `@custom-variant` — there is no `tailwind.config.js` unless the project added `@config`.",
|
|
84
|
-
"`@apply` in a separate CSS file needs `@reference`. Renamed utilities: `shadow-sm`→`shadow-xs`, `outline-none`→`outline-hidden`, `ring`→`ring-3`, `rounded-sm`→`rounded-xs`.",
|
|
85
|
-
],
|
|
86
|
-
},
|
|
87
|
-
express: {
|
|
88
|
-
label: "Express",
|
|
89
|
-
5: [
|
|
90
|
-
"Express 5: rejected promises from async handlers reach error middleware automatically — no `next(err)` wrappers needed.",
|
|
91
|
-
"`req.query` is a read-only getter; route syntax is path-to-regexp v8 (`/{*splat}` instead of `*`, optional segments with `{}`); `res.redirect('back')` is gone.",
|
|
92
|
-
],
|
|
93
|
-
},
|
|
94
|
-
zod: {
|
|
95
|
-
label: "Zod",
|
|
96
|
-
4: [
|
|
97
|
-
"Zod 4: use top-level formats `z.email()`, `z.uuid()`, `z.url()` — `z.string().email()` is deprecated.",
|
|
98
|
-
"Error details are `.issues` (not `.errors`); the `message` option is `error`; use `z.strictObject` / `z.looseObject` instead of `.strict()` / `.passthrough()`.",
|
|
99
|
-
],
|
|
100
|
-
},
|
|
101
|
-
eslint: {
|
|
102
|
-
label: "ESLint",
|
|
103
|
-
9: ["ESLint 9 uses flat config (`eslint.config.js`); `.eslintrc*` is ignored unless `ESLINT_USE_FLAT_CONFIG=false`."],
|
|
104
|
-
},
|
|
105
|
-
vue: {
|
|
106
|
-
label: "Vue",
|
|
107
|
-
3: ["Vue 3: Composition API with `<script setup>`, `defineProps` / `defineEmits`; Options API still works. Vue 2 is end-of-life — do not add Vue 2 patterns like `Vue.extend`."],
|
|
108
|
-
},
|
|
109
|
-
go: {
|
|
110
|
-
label: "Go",
|
|
111
|
-
// keyed by "1.NN" from the go directive
|
|
112
|
-
"1.22": ["Go 1.22+: loop variables are per-iteration; `for i := range 10` works; `net/http` ServeMux supports methods and wildcards (`mux.HandleFunc(\"GET /items/{id}\", …)`)."],
|
|
113
|
-
"1.23": ["Go 1.23+: range-over-func iterators (`iter.Seq`); everything from 1.22 (per-iteration loop vars, method patterns in ServeMux)."],
|
|
114
|
-
"1.24": ["Go 1.24+: generic type aliases; `testing.B.Loop` for benchmarks; `go tool` directives in go.mod; everything from 1.22/1.23."],
|
|
115
|
-
"1.25": ["Go 1.25+: `testing/synctest` for concurrent tests; container-aware GOMAXPROCS; everything from 1.22–1.24."],
|
|
116
|
-
},
|
|
117
|
-
python: {
|
|
118
|
-
label: "Python",
|
|
119
|
-
"3.12": ["Python 3.12+: `type` statement and `class C[T]` generics (PEP 695), `typing.override`, f-strings may nest quotes; `distutils` is gone."],
|
|
120
|
-
"3.13": ["Python 3.13+: `warnings.deprecated`; everything from 3.12 (PEP 695 generics, `typing.override`)."],
|
|
121
|
-
},
|
|
122
|
-
};
|
|
23
|
+
/**
|
|
24
|
+
* The table lives in pins.json, not here, because two writers need it:
|
|
25
|
+
* people adding a pin, and scripts/measure-pins.js writing back the measured
|
|
26
|
+
* effect. `pins` keeps the lib -> label + major -> rules shape that link,
|
|
27
|
+
* the website and the score already read; `checks` and `proof` sit beside it
|
|
28
|
+
* rather than inside, so no consumer mistakes them for a major.
|
|
29
|
+
*/
|
|
30
|
+
const DATA = require("../pins.json");
|
|
31
|
+
const PINS = DATA.pins;
|
|
32
|
+
const CHECKS = DATA.checks || {};
|
|
33
|
+
const PROOF = DATA.proof || {};
|
|
123
34
|
|
|
124
35
|
const major = (range) => {
|
|
125
36
|
const m = /(\d+)/.exec(String(range || ""));
|
|
126
37
|
return m ? m[1] : null;
|
|
127
38
|
};
|
|
128
39
|
|
|
40
|
+
function installedVersion(cwd, name) {
|
|
41
|
+
try {
|
|
42
|
+
return JSON.parse(fs.readFileSync(path.join(cwd, "node_modules", name, "package.json"), "utf-8")).version || null;
|
|
43
|
+
} catch {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
129
48
|
/** Read the majors this project runs, from whichever manifests exist. */
|
|
130
49
|
function detectVersions(cwd) {
|
|
131
50
|
const found = [];
|
|
@@ -142,8 +61,14 @@ function detectVersions(cwd) {
|
|
|
142
61
|
try { pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8")); } catch {}
|
|
143
62
|
const deps = pkg ? { ...pkg.dependencies, ...pkg.devDependencies } : {};
|
|
144
63
|
for (const lib of ["next", "react", "prisma", "tailwindcss", "express", "zod", "eslint", "vue"]) {
|
|
145
|
-
const
|
|
146
|
-
if (
|
|
64
|
+
const name = deps[lib] !== undefined ? lib : lib === "prisma" && deps["@prisma/client"] !== undefined ? "@prisma/client" : null;
|
|
65
|
+
if (!name) continue;
|
|
66
|
+
// What is installed beats what is declared: `^15` says nothing about
|
|
67
|
+
// whether a lockfile bump already put 16 on disk. Works for npm, pnpm,
|
|
68
|
+
// yarn and bun alike, without parsing four lockfile formats.
|
|
69
|
+
const installed = installedVersion(cwd, name);
|
|
70
|
+
if (installed) push(lib, major(installed), `node_modules (${name} ${installed})`);
|
|
71
|
+
else push(lib, major(deps[name]), `package.json (${name} ${deps[name]})`);
|
|
147
72
|
}
|
|
148
73
|
}
|
|
149
74
|
|
|
@@ -187,4 +112,32 @@ function renderPins(pins) {
|
|
|
187
112
|
return lines.join("\n");
|
|
188
113
|
}
|
|
189
114
|
|
|
190
|
-
|
|
115
|
+
/**
|
|
116
|
+
* The detectors for the *old* API of each detected pin, in the shape
|
|
117
|
+
* runPatterns consumes. Scoped by version, not by installed standard: a
|
|
118
|
+
* Next 13 project must not be told `cookies()` is async, and a Next 16
|
|
119
|
+
* project must be told whether or not it installed a Next.js standard.
|
|
120
|
+
*/
|
|
121
|
+
function pinPatterns(pins) {
|
|
122
|
+
const out = [];
|
|
123
|
+
for (const p of pins) {
|
|
124
|
+
for (const c of (CHECKS[p.lib] && CHECKS[p.lib][p.version]) || []) {
|
|
125
|
+
out.push({
|
|
126
|
+
id: c.id,
|
|
127
|
+
standard: `${p.label} ${p.version}`,
|
|
128
|
+
severity: c.severity || "medium",
|
|
129
|
+
files: c.files,
|
|
130
|
+
test: new RegExp(c.test, "m"),
|
|
131
|
+
not: c.not ? new RegExp(c.not, "m") : null,
|
|
132
|
+
message: c.message,
|
|
133
|
+
pin: true,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return out;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Measured effect of a pin, if scripts/measure-pins.js has run for it. */
|
|
141
|
+
const proofOf = (lib, version) => (PROOF[lib] && PROOF[lib][version]) || null;
|
|
142
|
+
|
|
143
|
+
module.exports = { PINS, CHECKS, PROOF, detectVersions, renderPins, pinPatterns, proofOf };
|
package/src/pins.json
ADDED
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$comment": "Version pins: breaking changes per dependency major that a model trained on the previous major gets wrong. `pins` is what link writes (keep its shape: lib -> label + major -> rules). `checks` are deterministic detectors for the OLD api, used by `review --fast`, `lint` and as the judge in scripts/measure-pins.js. `proof` is written only by scripts/measure-pins.js — never by hand.",
|
|
3
|
+
"pins": {
|
|
4
|
+
"next": {
|
|
5
|
+
"13": [
|
|
6
|
+
"App Router (`app/`) exists but `pages/` is still common; check which this project uses before adding routes."
|
|
7
|
+
],
|
|
8
|
+
"14": [
|
|
9
|
+
"App Router with Server Actions (stable). Navigate with `next/navigation`, not `next/router`, inside `app/`."
|
|
10
|
+
],
|
|
11
|
+
"15": [
|
|
12
|
+
"Request APIs are async: `await params`, `await searchParams`, `await cookies()`, `await headers()` — synchronous access is deprecated.",
|
|
13
|
+
"`fetch` is no longer cached by default; opt in with `cache: 'force-cache'` or `revalidate`.",
|
|
14
|
+
"Ships React 19: `ref` is a prop, `use()` and `useActionState` are available."
|
|
15
|
+
],
|
|
16
|
+
"16": [
|
|
17
|
+
"Turbopack is the default bundler; do not add webpack-only config without checking it is still needed.",
|
|
18
|
+
"Request APIs are async only — `params`, `searchParams`, `cookies()`, `headers()` must be awaited; synchronous access was removed.",
|
|
19
|
+
"`middleware.ts` is `proxy.ts`; the old filename is not picked up.",
|
|
20
|
+
"Ships React 19: `ref` is a prop, `use()` and `useActionState` are available."
|
|
21
|
+
],
|
|
22
|
+
"label": "Next.js"
|
|
23
|
+
},
|
|
24
|
+
"react": {
|
|
25
|
+
"18": [
|
|
26
|
+
"Mount with `createRoot` (not `ReactDOM.render`). Effects double-invoke in StrictMode during development — write them idempotently."
|
|
27
|
+
],
|
|
28
|
+
"19": [
|
|
29
|
+
"`ref` is a regular prop on function components — do not use `forwardRef`.",
|
|
30
|
+
"Use `use()` to read promises/context, `useActionState` and `useFormStatus` for forms; Actions can be passed to `<form action>`.",
|
|
31
|
+
"`propTypes` and `defaultProps` on function components are ignored; use TypeScript and default parameters."
|
|
32
|
+
],
|
|
33
|
+
"label": "React"
|
|
34
|
+
},
|
|
35
|
+
"pydantic": {
|
|
36
|
+
"1": [
|
|
37
|
+
"Pydantic v1 API: `.dict()`, `.json()`, `@validator`, `class Config`. Do not use v2 names (`model_dump`, `field_validator`)."
|
|
38
|
+
],
|
|
39
|
+
"2": [
|
|
40
|
+
"Use `.model_dump()` / `.model_dump_json()` / `.model_validate()` — `.dict()`, `.json()`, `.parse_obj()` are deprecated v1 names.",
|
|
41
|
+
"Validators are `@field_validator` and `@model_validator`; `@validator` and `@root_validator` are deprecated.",
|
|
42
|
+
"Configure with `model_config = ConfigDict(...)`, not an inner `class Config`. Use `TypeAdapter` for non-model types."
|
|
43
|
+
],
|
|
44
|
+
"label": "Pydantic"
|
|
45
|
+
},
|
|
46
|
+
"sqlalchemy": {
|
|
47
|
+
"2": [
|
|
48
|
+
"Use 2.0 style: `select()` with `session.execute()` / `session.scalars()`; `session.query()` is legacy.",
|
|
49
|
+
"Declare models with `DeclarativeBase`, `Mapped[...]` and `mapped_column()`; `declarative_base()` and `Column` are legacy."
|
|
50
|
+
],
|
|
51
|
+
"label": "SQLAlchemy"
|
|
52
|
+
},
|
|
53
|
+
"django": {
|
|
54
|
+
"5": [
|
|
55
|
+
"Django 5: `db_default` and `GeneratedField` exist; `USE_L10N` was removed; async ORM methods (`aget`, `acreate`, `afirst`) are available."
|
|
56
|
+
],
|
|
57
|
+
"label": "Django"
|
|
58
|
+
},
|
|
59
|
+
"prisma": {
|
|
60
|
+
"5": [
|
|
61
|
+
"Prisma 5: `prisma.$extends` for extensions; `@prisma/client` generated in `node_modules` — run `prisma generate` after schema changes."
|
|
62
|
+
],
|
|
63
|
+
"6": [
|
|
64
|
+
"Prisma 6: same API as 5; `omit` in queries is stable. Run `prisma generate` after schema changes."
|
|
65
|
+
],
|
|
66
|
+
"7": [
|
|
67
|
+
"Prisma 7 has no Rust query engine by default: instantiate the client with a driver adapter (for example `@prisma/adapter-pg`), not a bare `new PrismaClient()`.",
|
|
68
|
+
"Connection config lives in `prisma.config.ts`, not in the schema's `datasource url`."
|
|
69
|
+
],
|
|
70
|
+
"label": "Prisma"
|
|
71
|
+
},
|
|
72
|
+
"tailwindcss": {
|
|
73
|
+
"3": [
|
|
74
|
+
"Tailwind v3: `tailwind.config.js` with `content` globs, `@tailwind base/components/utilities` in CSS."
|
|
75
|
+
],
|
|
76
|
+
"4": [
|
|
77
|
+
"Tailwind v4 is CSS-first: `@import \"tailwindcss\"`, design tokens in `@theme`, `@custom-variant` — there is no `tailwind.config.js` unless the project added `@config`.",
|
|
78
|
+
"`@apply` in a separate CSS file needs `@reference`. Renamed utilities: `shadow-sm`→`shadow-xs`, `outline-none`→`outline-hidden`, `ring`→`ring-3`, `rounded-sm`→`rounded-xs`."
|
|
79
|
+
],
|
|
80
|
+
"label": "Tailwind CSS"
|
|
81
|
+
},
|
|
82
|
+
"express": {
|
|
83
|
+
"5": [
|
|
84
|
+
"Express 5: rejected promises from async handlers reach error middleware automatically — no `next(err)` wrappers needed.",
|
|
85
|
+
"`req.query` is a read-only getter; route syntax is path-to-regexp v8 (`/{*splat}` instead of `*`, optional segments with `{}`); `res.redirect('back')` is gone."
|
|
86
|
+
],
|
|
87
|
+
"label": "Express"
|
|
88
|
+
},
|
|
89
|
+
"zod": {
|
|
90
|
+
"4": [
|
|
91
|
+
"Zod 4: use top-level formats `z.email()`, `z.uuid()`, `z.url()` — `z.string().email()` is deprecated.",
|
|
92
|
+
"Error details are `.issues` (not `.errors`); the `message` option is `error`; use `z.strictObject` / `z.looseObject` instead of `.strict()` / `.passthrough()`."
|
|
93
|
+
],
|
|
94
|
+
"label": "Zod"
|
|
95
|
+
},
|
|
96
|
+
"eslint": {
|
|
97
|
+
"9": [
|
|
98
|
+
"ESLint 9 uses flat config (`eslint.config.js`); `.eslintrc*` is ignored unless `ESLINT_USE_FLAT_CONFIG=false`."
|
|
99
|
+
],
|
|
100
|
+
"label": "ESLint"
|
|
101
|
+
},
|
|
102
|
+
"vue": {
|
|
103
|
+
"3": [
|
|
104
|
+
"Vue 3: Composition API with `<script setup>`, `defineProps` / `defineEmits`; Options API still works. Vue 2 is end-of-life — do not add Vue 2 patterns like `Vue.extend`."
|
|
105
|
+
],
|
|
106
|
+
"label": "Vue"
|
|
107
|
+
},
|
|
108
|
+
"go": {
|
|
109
|
+
"label": "Go",
|
|
110
|
+
"1.22": [
|
|
111
|
+
"Go 1.22+: loop variables are per-iteration; `for i := range 10` works; `net/http` ServeMux supports methods and wildcards (`mux.HandleFunc(\"GET /items/{id}\", …)`)."
|
|
112
|
+
],
|
|
113
|
+
"1.23": [
|
|
114
|
+
"Go 1.23+: range-over-func iterators (`iter.Seq`); everything from 1.22 (per-iteration loop vars, method patterns in ServeMux)."
|
|
115
|
+
],
|
|
116
|
+
"1.24": [
|
|
117
|
+
"Go 1.24+: generic type aliases; `testing.B.Loop` for benchmarks; `go tool` directives in go.mod; everything from 1.22/1.23."
|
|
118
|
+
],
|
|
119
|
+
"1.25": [
|
|
120
|
+
"Go 1.25+: `testing/synctest` for concurrent tests; container-aware GOMAXPROCS; everything from 1.22–1.24."
|
|
121
|
+
]
|
|
122
|
+
},
|
|
123
|
+
"python": {
|
|
124
|
+
"label": "Python",
|
|
125
|
+
"3.12": [
|
|
126
|
+
"Python 3.12+: `type` statement and `class C[T]` generics (PEP 695), `typing.override`, f-strings may nest quotes; `distutils` is gone."
|
|
127
|
+
],
|
|
128
|
+
"3.13": [
|
|
129
|
+
"Python 3.13+: `warnings.deprecated`; everything from 3.12 (PEP 695 generics, `typing.override`)."
|
|
130
|
+
]
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
"checks": {
|
|
134
|
+
"next": {
|
|
135
|
+
"15": [
|
|
136
|
+
{
|
|
137
|
+
"id": "next-sync-request-api",
|
|
138
|
+
"files": [
|
|
139
|
+
".ts",
|
|
140
|
+
".tsx",
|
|
141
|
+
".js",
|
|
142
|
+
".jsx",
|
|
143
|
+
".mjs"
|
|
144
|
+
],
|
|
145
|
+
"test": "\\b(cookies|headers)\\(\\)\\.(get|set|getAll|has|delete)\\(",
|
|
146
|
+
"message": "Next.js 15+: `cookies()` / `headers()` are async — `(await cookies()).get(...)`."
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
"id": "next-sync-params",
|
|
150
|
+
"files": [
|
|
151
|
+
".ts",
|
|
152
|
+
".tsx"
|
|
153
|
+
],
|
|
154
|
+
"test": "\\{\\s*(params|searchParams)\\s*[,}][^)]*\\)?\\s*:\\s*\\{\\s*(params|searchParams)\\s*:\\s*\\{",
|
|
155
|
+
"message": "Next.js 15+: `params` / `searchParams` are Promises — type them `Promise<{...}>` and `await` them."
|
|
156
|
+
}
|
|
157
|
+
],
|
|
158
|
+
"16": [
|
|
159
|
+
{
|
|
160
|
+
"id": "next-sync-request-api",
|
|
161
|
+
"files": [
|
|
162
|
+
".ts",
|
|
163
|
+
".tsx",
|
|
164
|
+
".js",
|
|
165
|
+
".jsx",
|
|
166
|
+
".mjs"
|
|
167
|
+
],
|
|
168
|
+
"test": "\\b(cookies|headers)\\(\\)\\.(get|set|getAll|has|delete)\\(",
|
|
169
|
+
"message": "Next.js 16: `cookies()` / `headers()` must be awaited — synchronous access was removed."
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
"id": "next-sync-params",
|
|
173
|
+
"files": [
|
|
174
|
+
".ts",
|
|
175
|
+
".tsx"
|
|
176
|
+
],
|
|
177
|
+
"test": "\\{\\s*(params|searchParams)\\s*[,}][^)]*\\)?\\s*:\\s*\\{\\s*(params|searchParams)\\s*:\\s*\\{",
|
|
178
|
+
"message": "Next.js 16: `params` / `searchParams` are Promises — type them `Promise<{...}>` and `await` them."
|
|
179
|
+
}
|
|
180
|
+
]
|
|
181
|
+
},
|
|
182
|
+
"react": {
|
|
183
|
+
"18": [
|
|
184
|
+
{
|
|
185
|
+
"id": "react-legacy-render",
|
|
186
|
+
"files": [
|
|
187
|
+
".ts",
|
|
188
|
+
".tsx",
|
|
189
|
+
".js",
|
|
190
|
+
".jsx",
|
|
191
|
+
".mjs"
|
|
192
|
+
],
|
|
193
|
+
"test": "\\bReactDOM\\.render\\(",
|
|
194
|
+
"message": "React 18+: mount with `createRoot(el).render(...)`, not `ReactDOM.render`."
|
|
195
|
+
}
|
|
196
|
+
],
|
|
197
|
+
"19": [
|
|
198
|
+
{
|
|
199
|
+
"id": "react-forward-ref",
|
|
200
|
+
"files": [
|
|
201
|
+
".ts",
|
|
202
|
+
".tsx",
|
|
203
|
+
".js",
|
|
204
|
+
".jsx",
|
|
205
|
+
".mjs"
|
|
206
|
+
],
|
|
207
|
+
"test": "\\bforwardRef\\s*[<(]",
|
|
208
|
+
"message": "React 19: `ref` is a regular prop — drop `forwardRef`."
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
"id": "react-function-proptypes",
|
|
212
|
+
"files": [
|
|
213
|
+
".ts",
|
|
214
|
+
".tsx",
|
|
215
|
+
".js",
|
|
216
|
+
".jsx",
|
|
217
|
+
".mjs"
|
|
218
|
+
],
|
|
219
|
+
"test": "^\\s*[A-Z]\\w*\\.(propTypes|defaultProps)\\s*=",
|
|
220
|
+
"message": "React 19: `propTypes` / `defaultProps` on function components are ignored — use TypeScript and default parameters."
|
|
221
|
+
}
|
|
222
|
+
]
|
|
223
|
+
},
|
|
224
|
+
"pydantic": {
|
|
225
|
+
"1": [
|
|
226
|
+
{
|
|
227
|
+
"id": "pydantic-v2-api-on-v1",
|
|
228
|
+
"files": [
|
|
229
|
+
".py"
|
|
230
|
+
],
|
|
231
|
+
"test": "\\.model_dump\\(|\\.model_validate\\(|@field_validator\\b|ConfigDict\\(",
|
|
232
|
+
"message": "This project pins Pydantic v1 — `model_dump`, `field_validator`, `ConfigDict` do not exist there."
|
|
233
|
+
}
|
|
234
|
+
],
|
|
235
|
+
"2": [
|
|
236
|
+
{
|
|
237
|
+
"id": "pydantic-v1-api",
|
|
238
|
+
"files": [
|
|
239
|
+
".py"
|
|
240
|
+
],
|
|
241
|
+
"test": "\\.(dict|json|parse_obj|parse_raw)\\(\\s*\\)|@(validator|root_validator)\\b|^\\s*class Config\\s*:",
|
|
242
|
+
"not": "model_dump|model_validate",
|
|
243
|
+
"message": "Pydantic v2: use `model_dump`, `model_validate`, `field_validator`, `model_config = ConfigDict(...)`."
|
|
244
|
+
}
|
|
245
|
+
]
|
|
246
|
+
},
|
|
247
|
+
"sqlalchemy": {
|
|
248
|
+
"2": [
|
|
249
|
+
{
|
|
250
|
+
"id": "sqlalchemy-legacy-query",
|
|
251
|
+
"files": [
|
|
252
|
+
".py"
|
|
253
|
+
],
|
|
254
|
+
"test": "\\b(session|db\\.session|db)\\.query\\(\\s*[A-Z]",
|
|
255
|
+
"message": "SQLAlchemy 2.0: `session.execute(select(Model))` / `session.scalars(...)`, not `session.query(Model)`."
|
|
256
|
+
},
|
|
257
|
+
{
|
|
258
|
+
"id": "sqlalchemy-declarative-base",
|
|
259
|
+
"files": [
|
|
260
|
+
".py"
|
|
261
|
+
],
|
|
262
|
+
"test": "\\bdeclarative_base\\(\\)",
|
|
263
|
+
"message": "SQLAlchemy 2.0: subclass `DeclarativeBase` and use `Mapped[...]` / `mapped_column()`."
|
|
264
|
+
}
|
|
265
|
+
]
|
|
266
|
+
},
|
|
267
|
+
"django": {
|
|
268
|
+
"5": [
|
|
269
|
+
{
|
|
270
|
+
"id": "django-use-l10n",
|
|
271
|
+
"files": [
|
|
272
|
+
".py"
|
|
273
|
+
],
|
|
274
|
+
"test": "^\\s*USE_L10N\\s*=",
|
|
275
|
+
"message": "Django 5: `USE_L10N` was removed."
|
|
276
|
+
}
|
|
277
|
+
]
|
|
278
|
+
},
|
|
279
|
+
"prisma": {
|
|
280
|
+
"7": [
|
|
281
|
+
{
|
|
282
|
+
"id": "prisma-bare-client",
|
|
283
|
+
"files": [
|
|
284
|
+
".ts",
|
|
285
|
+
".tsx",
|
|
286
|
+
".js",
|
|
287
|
+
".jsx",
|
|
288
|
+
".mjs"
|
|
289
|
+
],
|
|
290
|
+
"test": "new\\s+PrismaClient\\(\\s*\\)",
|
|
291
|
+
"message": "Prisma 7: no Rust engine by default — pass a driver adapter, `new PrismaClient({ adapter })`."
|
|
292
|
+
}
|
|
293
|
+
]
|
|
294
|
+
},
|
|
295
|
+
"tailwindcss": {
|
|
296
|
+
"4": [
|
|
297
|
+
{
|
|
298
|
+
"id": "tailwind-v3-directives",
|
|
299
|
+
"files": [
|
|
300
|
+
".css",
|
|
301
|
+
".scss",
|
|
302
|
+
".pcss"
|
|
303
|
+
],
|
|
304
|
+
"test": "^\\s*@tailwind\\s+(base|components|utilities)\\b",
|
|
305
|
+
"message": "Tailwind v4: `@import \"tailwindcss\";` replaces the three `@tailwind` directives."
|
|
306
|
+
}
|
|
307
|
+
]
|
|
308
|
+
},
|
|
309
|
+
"express": {
|
|
310
|
+
"5": [
|
|
311
|
+
{
|
|
312
|
+
"id": "express-bare-wildcard",
|
|
313
|
+
"files": [
|
|
314
|
+
".ts",
|
|
315
|
+
".tsx",
|
|
316
|
+
".js",
|
|
317
|
+
".jsx",
|
|
318
|
+
".mjs"
|
|
319
|
+
],
|
|
320
|
+
"test": "\\.(get|post|put|patch|delete|all|use)\\(\\s*[\\x27\"][^\\x27\"]*\\/\\*[\\x27\"]",
|
|
321
|
+
"message": "Express 5: path-to-regexp v8 — name the wildcard, `/{*splat}`, not `*`."
|
|
322
|
+
},
|
|
323
|
+
{
|
|
324
|
+
"id": "express-redirect-back",
|
|
325
|
+
"files": [
|
|
326
|
+
".ts",
|
|
327
|
+
".tsx",
|
|
328
|
+
".js",
|
|
329
|
+
".jsx",
|
|
330
|
+
".mjs"
|
|
331
|
+
],
|
|
332
|
+
"test": "res\\.redirect\\(\\s*[\\x27\"]back[\\x27\"]",
|
|
333
|
+
"message": "Express 5: `res.redirect(\"back\")` was removed — use `req.get(\"Referrer\") || \"/\"`."
|
|
334
|
+
}
|
|
335
|
+
]
|
|
336
|
+
},
|
|
337
|
+
"zod": {
|
|
338
|
+
"4": [
|
|
339
|
+
{
|
|
340
|
+
"id": "zod-string-format",
|
|
341
|
+
"files": [
|
|
342
|
+
".ts",
|
|
343
|
+
".tsx",
|
|
344
|
+
".js",
|
|
345
|
+
".jsx",
|
|
346
|
+
".mjs"
|
|
347
|
+
],
|
|
348
|
+
"test": "z\\.string\\(\\)\\.(email|uuid|url|cuid|ulid|ipv4|ipv6|datetime)\\(",
|
|
349
|
+
"message": "Zod 4: use the top-level formats `z.email()`, `z.uuid()`, `z.url()`."
|
|
350
|
+
},
|
|
351
|
+
{
|
|
352
|
+
"id": "zod-error-errors",
|
|
353
|
+
"files": [
|
|
354
|
+
".ts",
|
|
355
|
+
".tsx",
|
|
356
|
+
".js",
|
|
357
|
+
".jsx",
|
|
358
|
+
".mjs"
|
|
359
|
+
],
|
|
360
|
+
"test": "\\.error\\.errors\\b",
|
|
361
|
+
"message": "Zod 4: error details are `.issues`, not `.errors`."
|
|
362
|
+
}
|
|
363
|
+
]
|
|
364
|
+
},
|
|
365
|
+
"vue": {
|
|
366
|
+
"3": [
|
|
367
|
+
{
|
|
368
|
+
"id": "vue2-global-api",
|
|
369
|
+
"files": [
|
|
370
|
+
".js",
|
|
371
|
+
".ts",
|
|
372
|
+
".vue"
|
|
373
|
+
],
|
|
374
|
+
"test": "\\bVue\\.extend\\(|\\bnew\\s+Vue\\(",
|
|
375
|
+
"message": "Vue 3: `createApp()` and `defineComponent()` — `new Vue` / `Vue.extend` are Vue 2."
|
|
376
|
+
}
|
|
377
|
+
]
|
|
378
|
+
},
|
|
379
|
+
"python": {
|
|
380
|
+
"3.12": [
|
|
381
|
+
{
|
|
382
|
+
"id": "python-distutils",
|
|
383
|
+
"files": [
|
|
384
|
+
".py"
|
|
385
|
+
],
|
|
386
|
+
"test": "^\\s*(from|import)\\s+distutils\\b",
|
|
387
|
+
"message": "Python 3.12: `distutils` is gone — use `setuptools` or `sysconfig`."
|
|
388
|
+
}
|
|
389
|
+
],
|
|
390
|
+
"3.13": [
|
|
391
|
+
{
|
|
392
|
+
"id": "python-distutils",
|
|
393
|
+
"files": [
|
|
394
|
+
".py"
|
|
395
|
+
],
|
|
396
|
+
"test": "^\\s*(from|import)\\s+distutils\\b",
|
|
397
|
+
"message": "Python 3.13: `distutils` is gone — use `setuptools` or `sysconfig`."
|
|
398
|
+
}
|
|
399
|
+
]
|
|
400
|
+
}
|
|
401
|
+
},
|
|
402
|
+
"proof": {}
|
|
403
|
+
}
|