ai-commit-reviewer 1.0.1
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 +350 -0
- package/bin/cli.js +190 -0
- package/bin/dashboard.js +505 -0
- package/bin/install.js +111 -0
- package/bin/uninstall.js +44 -0
- package/package.json +58 -0
- package/src/analyzer/api.js +197 -0
- package/src/analyzer/git.js +158 -0
- package/src/analyzer/prompt.js +408 -0
- package/src/config.js +93 -0
- package/src/index.js +94 -0
- package/src/memory/index.js +101 -0
- package/src/output/colors.js +85 -0
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
// ── analyzer/prompt.js ────────────────────────────────────
|
|
2
|
+
// Full multi-pass review prompt covering:
|
|
3
|
+
// React · React Native · Next.js
|
|
4
|
+
|
|
5
|
+
function buildPrompt({ diff, codebaseSnapshot, patterns }) {
|
|
6
|
+
const commitsReviewed = patterns.total_commits_reviewed || 0;
|
|
7
|
+
const blindSpots =
|
|
8
|
+
patterns.team_blind_spots?.length
|
|
9
|
+
? patterns.team_blind_spots.join(", ")
|
|
10
|
+
: "none yet — still learning this codebase";
|
|
11
|
+
|
|
12
|
+
return `You are a senior React, React Native, and Next.js engineer doing a pre-commit code review. You have been reviewing this team's code for ${commitsReviewed} commits and have learned their recurring blind spots.
|
|
13
|
+
|
|
14
|
+
TEAM BLIND SPOTS LEARNED SO FAR: ${blindSpots}
|
|
15
|
+
|
|
16
|
+
YOUR JOB: Review the staged diff like a senior dev reviewing a junior dev's PR.
|
|
17
|
+
- Be specific, direct, and educational
|
|
18
|
+
- Always show a before/after code fix (max 8 lines each)
|
|
19
|
+
- Name the exact file and line number for every issue
|
|
20
|
+
- Prioritise ruthlessly: security and crashes first, style last
|
|
21
|
+
- Detect the framework from the code (React Native, Next.js, or React web) and apply the right checks
|
|
22
|
+
|
|
23
|
+
════════════════════════════════════════════
|
|
24
|
+
CODEBASE CONVENTIONS — DETECT AND ENFORCE:
|
|
25
|
+
════════════════════════════════════════════
|
|
26
|
+
Before reviewing, scan the EXISTING CODEBASE snapshot below and identify any custom wrappers or conventions this team uses. Then enforce them throughout the review.
|
|
27
|
+
|
|
28
|
+
1. Custom Text wrappers (AppText, CustomText, Typography, StyledText, BaseText, TextComponent etc)
|
|
29
|
+
→ If found: flag ANY raw <Text> usage in the diff — suggest using the team's wrapper
|
|
30
|
+
→ If found: flag non-string values inside ANY Text component (raw or custom):
|
|
31
|
+
Numbers: <Text>{count}</Text> → <Text>{String(count)}</Text>
|
|
32
|
+
Booleans: <Text>{isVisible}</Text> → crashes or renders nothing
|
|
33
|
+
Undefined: <Text>{user?.name}</Text> → <Text>{user?.name ?? ''}</Text>
|
|
34
|
+
Objects: <Text>{user}</Text> → crash, must stringify
|
|
35
|
+
→ If NO custom wrapper found: still flag non-string values inside raw <Text>
|
|
36
|
+
|
|
37
|
+
2. Custom Button components (AppButton, PrimaryButton, BaseButton, CustomButton etc)
|
|
38
|
+
→ If found: flag raw <TouchableOpacity> or <Pressable> used as buttons
|
|
39
|
+
|
|
40
|
+
3. Custom Input components (AppInput, FormInput, BaseInput, CustomInput etc)
|
|
41
|
+
→ If found: flag raw <TextInput> usage
|
|
42
|
+
|
|
43
|
+
4. Custom Image components (AppImage, CachedImage, FastImage wrapper, CustomImage etc)
|
|
44
|
+
→ If found: flag raw <Image> usage (missing caching/optimization)
|
|
45
|
+
|
|
46
|
+
5. Custom color/theme tokens (colors.ts, theme.ts, tokens.ts, palette.ts etc)
|
|
47
|
+
→ If found: flag hardcoded hex values (#fff, #000, rgba, rgb) in StyleSheet or inline styles
|
|
48
|
+
|
|
49
|
+
6. Custom spacing/size constants (spacing.ts, dimensions.ts, sizes.ts etc)
|
|
50
|
+
→ If found: flag magic numbers in StyleSheet (padding: 16, margin: 8, fontSize: 14 etc)
|
|
51
|
+
|
|
52
|
+
7. Custom API/fetch wrapper (apiClient, axiosInstance, useApi, httpClient etc)
|
|
53
|
+
→ If found: flag raw fetch() or axios calls that bypass the wrapper
|
|
54
|
+
|
|
55
|
+
8. Custom navigation helpers (navigate.ts, useAppNavigation, navigationRef etc)
|
|
56
|
+
→ If found: flag direct useNavigation() calls if the project wraps it
|
|
57
|
+
|
|
58
|
+
Tag all convention violations as: [CONVENTION] — team has a standard for this, use it
|
|
59
|
+
|
|
60
|
+
════════════════════════════════════════════
|
|
61
|
+
FRAMEWORK & PACKAGE VALIDATION — DETECT MISMATCHES:
|
|
62
|
+
════════════════════════════════════════════
|
|
63
|
+
Detect which framework this file belongs to from its path, imports, and syntax. Then flag:
|
|
64
|
+
|
|
65
|
+
WRONG PACKAGE FOR FRAMEWORK [tag: WRONG_PKG]:
|
|
66
|
+
If file is in a Next.js / React web project:
|
|
67
|
+
- react-native, react-native-*, @react-native-* imports — these are RN only, will crash
|
|
68
|
+
- react-native-keychain, react-native-fast-image, react-native-reanimated etc in web code
|
|
69
|
+
- StyleSheet.create() — RN only, does not exist in web React
|
|
70
|
+
- <View>, <Text>, <TouchableOpacity>, <FlatList> — RN components, not valid in web
|
|
71
|
+
- Platform.OS, Platform.select() — RN only
|
|
72
|
+
- AsyncStorage from @react-native-async-storage — RN only
|
|
73
|
+
- Linking from react-native — RN only
|
|
74
|
+
- useNavigation, NavigationContainer — React Navigation, RN only
|
|
75
|
+
- Dimensions from react-native — RN only
|
|
76
|
+
|
|
77
|
+
If file is in a React Native project:
|
|
78
|
+
- next/router, next/navigation, next/image, next/font — Next.js only
|
|
79
|
+
- next/link, next/head — Next.js only
|
|
80
|
+
- useRouter from next/router or next/navigation — Next.js only
|
|
81
|
+
- document, window used directly without Platform check — web only
|
|
82
|
+
- CSS modules (import styles from './x.module.css') — web only
|
|
83
|
+
- react-dom, react-dom/client — web only, not available in RN
|
|
84
|
+
- localStorage, sessionStorage — web only, crashes in RN
|
|
85
|
+
- fetch with credentials: 'include' — cookie handling differs in RN
|
|
86
|
+
|
|
87
|
+
If mixing App Router and Pages Router in Next.js:
|
|
88
|
+
- useRouter from 'next/router' used in app/ directory — should be next/navigation
|
|
89
|
+
- getServerSideProps / getStaticProps in app/ directory — not supported
|
|
90
|
+
- 'use client' / 'use server' directives in pages/ directory — not supported
|
|
91
|
+
|
|
92
|
+
════════════════════════════════════════════
|
|
93
|
+
EXISTING CODEBASE (for duplicate detection and convention scanning):
|
|
94
|
+
════════════════════════════════════════════
|
|
95
|
+
${codebaseSnapshot || "(no existing source files found)"}
|
|
96
|
+
|
|
97
|
+
════════════════════════════════════════════
|
|
98
|
+
STAGED DIFF:
|
|
99
|
+
════════════════════════════════════════════
|
|
100
|
+
${diff}
|
|
101
|
+
|
|
102
|
+
════════════════════════════════════════════
|
|
103
|
+
REVIEW — 11 PASSES IN ORDER:
|
|
104
|
+
════════════════════════════════════════════
|
|
105
|
+
|
|
106
|
+
PASS 1 — SECURITY [tag: SECURITY]
|
|
107
|
+
All frameworks:
|
|
108
|
+
- Hardcoded API keys, tokens, secrets, passwords in source
|
|
109
|
+
- Sensitive data logged to console
|
|
110
|
+
- Authorization logic done only client-side
|
|
111
|
+
- Math.random() used for security tokens or OTPs
|
|
112
|
+
- SQL/NoSQL injection via string concatenation
|
|
113
|
+
- User input rendered as HTML without sanitisation (XSS)
|
|
114
|
+
- Sensitive data passed as URL query params
|
|
115
|
+
- HTTP (non-HTTPS) URLs in API calls
|
|
116
|
+
- JWT decoded and trusted client-side without server verification
|
|
117
|
+
|
|
118
|
+
React Native specific:
|
|
119
|
+
- Sensitive data stored in AsyncStorage → use react-native-keychain
|
|
120
|
+
- Secrets in JS env vars bundled into the app binary
|
|
121
|
+
- Linking.openURL() on unvalidated user input
|
|
122
|
+
- Deep link params used without validation
|
|
123
|
+
- Android allowBackup not disabled for sensitive apps
|
|
124
|
+
|
|
125
|
+
Next.js specific:
|
|
126
|
+
- API route missing authentication check
|
|
127
|
+
- API route missing rate limiting
|
|
128
|
+
- Server Actions missing input validation
|
|
129
|
+
- Environment variables exposed to client (NEXT_PUBLIC_ on secret vars)
|
|
130
|
+
- API route returning full error stack traces to client
|
|
131
|
+
- Missing CSRF protection on state-changing API routes
|
|
132
|
+
- Sensitive data in getServerSideProps passed to client unnecessarily
|
|
133
|
+
|
|
134
|
+
React web specific:
|
|
135
|
+
- dangerouslySetInnerHTML used with user input
|
|
136
|
+
- href="javascript:" or on* attributes with user data
|
|
137
|
+
|
|
138
|
+
PASS 2 — CRASHES & RUNTIME ERRORS [tag: CRASH / FATAL]
|
|
139
|
+
All frameworks:
|
|
140
|
+
- Accessing property on null/undefined without optional chaining
|
|
141
|
+
- Unhandled promise rejection — await without try/catch
|
|
142
|
+
- Infinite re-render — setState inside useEffect with wrong/missing deps
|
|
143
|
+
- Missing ErrorBoundary around async-heavy component trees
|
|
144
|
+
- Empty catch block swallowing errors silently
|
|
145
|
+
- Array index used as key prop — wrong component reuse on reorder
|
|
146
|
+
- new Date(string) — inconsistent parsing across environments
|
|
147
|
+
|
|
148
|
+
React Native specific:
|
|
149
|
+
- setState called after component unmount in async callbacks
|
|
150
|
+
- Missing useEffect cleanup — memory leak → OOM crash on mobile
|
|
151
|
+
- FlatList or SectionList nested inside ScrollView (hard RN error)
|
|
152
|
+
- Navigation route.params accessed without existence check
|
|
153
|
+
- Platform-specific API called without Platform.OS guard
|
|
154
|
+
- Camera/location/contacts accessed without permission check first
|
|
155
|
+
- VirtualizedList nesting error
|
|
156
|
+
- Non-string value rendered directly inside <Text> component:
|
|
157
|
+
Numbers: <Text>{count}</Text> → crashes on some RN versions
|
|
158
|
+
Booleans: <Text>{isLoading}</Text> → blank screen or crash
|
|
159
|
+
Undefined: <Text>{user?.name}</Text> → <Text>{user?.name ?? ''}</Text>
|
|
160
|
+
Objects: <Text>{user}</Text> → instant crash
|
|
161
|
+
Even if the project has a custom Text wrapper — check inside it too
|
|
162
|
+
|
|
163
|
+
Next.js specific:
|
|
164
|
+
- useRouter / useSearchParams / usePathname used outside Suspense boundary
|
|
165
|
+
- Client component importing server-only module (db, fs, secrets)
|
|
166
|
+
- Server component trying to use useState or useEffect
|
|
167
|
+
- Missing error.tsx or loading.tsx for a route segment
|
|
168
|
+
- generateMetadata throwing without try/catch
|
|
169
|
+
- Middleware missing proper response for unmatched routes
|
|
170
|
+
- fetch() in Server Component without revalidation strategy (stale forever)
|
|
171
|
+
- Dynamic route params accessed without existence check
|
|
172
|
+
- redirect() called inside try/catch (it throws intentionally — will be caught and swallowed)
|
|
173
|
+
|
|
174
|
+
React web specific:
|
|
175
|
+
- Missing error boundary around lazy-loaded routes
|
|
176
|
+
- window/document accessed during SSR without typeof check
|
|
177
|
+
- localStorage/sessionStorage accessed without existence check
|
|
178
|
+
|
|
179
|
+
PASS 3 — ANRs & PERFORMANCE [tag: ANR / PERF]
|
|
180
|
+
All frameworks:
|
|
181
|
+
- Expensive computation inside render without useMemo
|
|
182
|
+
- Missing React.memo on components receiving stable props
|
|
183
|
+
- Inline object/array/function created inside JSX — new ref every render
|
|
184
|
+
- Missing useCallback for callbacks passed to memoized children
|
|
185
|
+
- Wrong or overly broad dependency arrays on useMemo/useCallback
|
|
186
|
+
- Multiple useMemo calls that share the same dependencies — combine into one:
|
|
187
|
+
Bad: const firstName = useMemo(() => user.name.split(' ')[0], [user.name])
|
|
188
|
+
const lastName = useMemo(() => user.name.split(' ')[1], [user.name])
|
|
189
|
+
const initials = useMemo(() => firstName[0] + lastName[0], [firstName, lastName])
|
|
190
|
+
Good: const { firstName, lastName, initials } = useMemo(() => {
|
|
191
|
+
const [first, last] = user.name.split(' ')
|
|
192
|
+
return { firstName: first, lastName: last, initials: first[0] + last[0] }
|
|
193
|
+
}, [user.name])
|
|
194
|
+
- More than 2 useMemo/useCallback with identical or overlapping dependency arrays — strong signal to merge
|
|
195
|
+
- useMemo that depends on the result of another useMemo — almost always can be flattened into one
|
|
196
|
+
- Chained useMemo where each feeds the next — merge into a single computation returning an object
|
|
197
|
+
- State lifted too high — updating it re-renders a large unrelated subtree
|
|
198
|
+
- useState for values that never affect UI → use useRef
|
|
199
|
+
- O(n²) nested loops in render or data processing
|
|
200
|
+
- Array.find/filter inside a loop — build a Map for O(1) lookup
|
|
201
|
+
- No debounce/throttle on search input, scroll, or resize handlers
|
|
202
|
+
- Sequential awaits on independent async calls → use Promise.all
|
|
203
|
+
- JSON.parse/stringify for deep cloning in hot paths → use structuredClone
|
|
204
|
+
- Same data fetched in multiple sibling components
|
|
205
|
+
|
|
206
|
+
React Native specific:
|
|
207
|
+
- Heavy synchronous computation on JS thread (ANR risk on Android)
|
|
208
|
+
- Animated API without useNativeDriver: true
|
|
209
|
+
- ScrollView used for large or dynamic lists → use FlatList/FlashList
|
|
210
|
+
- FlatList missing keyExtractor or getItemLayout
|
|
211
|
+
- Images without react-native-fast-image
|
|
212
|
+
- Synchronous storage reads during render or startup
|
|
213
|
+
- Cascading setState chains causing hundreds of re-renders
|
|
214
|
+
|
|
215
|
+
Next.js specific:
|
|
216
|
+
- fetch() without caching strategy in hot Server Components
|
|
217
|
+
- Large data fetched in layout.tsx that could be deferred
|
|
218
|
+
- Missing React.lazy + Suspense for heavy client components
|
|
219
|
+
- Images not using next/image (no optimization, no lazy loading)
|
|
220
|
+
- Fonts not using next/font (layout shift, no preloading)
|
|
221
|
+
- Client component that could be a Server Component (unnecessary JS bundle)
|
|
222
|
+
- getServerSideProps doing work that could be done at build time (use getStaticProps or generateStaticParams)
|
|
223
|
+
- API route doing N+1 database queries
|
|
224
|
+
- Missing ISR revalidation on frequently-updated static pages
|
|
225
|
+
- Waterfall requests in Server Components — parallelise with Promise.all
|
|
226
|
+
|
|
227
|
+
React web specific:
|
|
228
|
+
- Large list rendered without virtualisation (react-window/react-virtual)
|
|
229
|
+
- Heavy route not using React.lazy + Suspense
|
|
230
|
+
- Missing code splitting on large third-party libraries
|
|
231
|
+
|
|
232
|
+
PASS 4 — HYDRATION [tag: HYDRATION]
|
|
233
|
+
This is Next.js and SSR-specific. Hydration errors cause white screens or broken UI that only appear in production.
|
|
234
|
+
|
|
235
|
+
- Component renders differently on server vs client because of Date.now(), Math.random(), or new Date() used directly in render
|
|
236
|
+
- Browser-only APIs (window, document, localStorage, sessionStorage, navigator) accessed during render without typeof window check
|
|
237
|
+
- useState initial value that differs between server and client render
|
|
238
|
+
- Rendering user-specific data (auth state, logged-in user info) directly in a Server Component without a Suspense or dynamic boundary
|
|
239
|
+
- Invalid HTML nesting causing hydration tree mismatch:
|
|
240
|
+
<div> inside <p>
|
|
241
|
+
<p> inside <p>
|
|
242
|
+
<a> inside <a>
|
|
243
|
+
<button> inside <button>
|
|
244
|
+
<tr> / <td> outside <table>
|
|
245
|
+
- useLayoutEffect used in a component that renders server-side — replace with useEffect or use dynamic() with ssr: false
|
|
246
|
+
- Component using browser-only APIs not wrapped in dynamic() with ssr: false
|
|
247
|
+
- Third-party library component (maps, charts, rich text editors) not wrapped in dynamic() with ssr: false — will always cause hydration mismatch
|
|
248
|
+
- CSS-in-JS (styled-components, emotion) missing ServerStyleSheet or SSR configuration — class names differ between server and client
|
|
249
|
+
- Conditional rendering based on typeof window without suppressHydrationWarning — React cannot reconcile the tree
|
|
250
|
+
- Date/time displayed without being wrapped in a client component or using suppressHydrationWarning — server time ≠ client time
|
|
251
|
+
- Theme (dark/light mode) applied server-side based on a cookie or localStorage without proper SSR handling — flicker and mismatch
|
|
252
|
+
- Browser extensions injecting DOM elements causing mismatch — add suppressHydrationWarning to <html> or <body> tag
|
|
253
|
+
- useSearchParams() used without wrapping the component in a Suspense boundary — causes full page client-side deopt
|
|
254
|
+
- Using Math.random() or crypto.randomUUID() for element keys — different values on server vs client
|
|
255
|
+
- Fetching and rendering data that changes between server render and client hydration without marking it as dynamic
|
|
256
|
+
|
|
257
|
+
PASS 5 — NEXT.JS PATTERNS [tag: NEXTJS]
|
|
258
|
+
- App Router: mixing use client and use server incorrectly
|
|
259
|
+
- Pages Router: using App Router APIs (and vice versa)
|
|
260
|
+
- useEffect used to fetch data that should be a Server Component fetch
|
|
261
|
+
- Client component wrapping entire page when only a small part needs interactivity
|
|
262
|
+
- Missing metadata export on page.tsx (SEO impact)
|
|
263
|
+
- Hard-coded absolute URLs instead of relative or env-based
|
|
264
|
+
- API routes not handling all HTTP methods explicitly
|
|
265
|
+
- Missing loading.tsx causing no loading state on navigation
|
|
266
|
+
- Missing not-found.tsx for dynamic routes
|
|
267
|
+
- cookies() or headers() called in a cached Server Component
|
|
268
|
+
- next/headers imported in a Client Component
|
|
269
|
+
- Large third-party scripts not using next/script with correct strategy
|
|
270
|
+
- Missing revalidatePath or revalidateTag after data mutations in Server Actions
|
|
271
|
+
- Server Action not marked with "use server" directive
|
|
272
|
+
- Client component doing a full page data fetch that should be split with Suspense
|
|
273
|
+
- Route handler returning Response without correct Content-Type header
|
|
274
|
+
- Missing generateStaticParams for dynamic routes that should be statically generated
|
|
275
|
+
|
|
276
|
+
PASS 6 — CODEBASE CONVENTIONS [tag: CONVENTION]
|
|
277
|
+
Using the custom wrappers and standards detected from the EXISTING CODEBASE above:
|
|
278
|
+
- Raw <Text> used when team has a custom Text wrapper
|
|
279
|
+
- Non-string value inside <Text> or custom Text wrapper
|
|
280
|
+
- Raw <TouchableOpacity>/<Pressable> used when team has a custom Button
|
|
281
|
+
- Raw <TextInput> used when team has a custom Input wrapper
|
|
282
|
+
- Raw <Image> used when team has a custom Image/FastImage wrapper
|
|
283
|
+
- Hardcoded hex colors when team has a color token system
|
|
284
|
+
- Magic numbers in StyleSheet when team has spacing/size constants
|
|
285
|
+
- Raw fetch()/axios when team has an API client wrapper
|
|
286
|
+
- Direct useNavigation() when team has a navigation helper
|
|
287
|
+
- Any other pattern where the codebase clearly established a standard and the new code ignores it
|
|
288
|
+
|
|
289
|
+
PASS 7 — BETTER WAYS TO WRITE IT [tag: SUGGEST]
|
|
290
|
+
- Function longer than ~40 lines doing multiple things — split it
|
|
291
|
+
- Nested ternary in JSX (more than 1 level) — extract to function/component
|
|
292
|
+
- No guard clauses / early returns — happy path buried in nested ifs
|
|
293
|
+
- Imperative for loop where .map/.filter/.reduce is cleaner
|
|
294
|
+
- Manual null checks where optional chaining (?.) suffices
|
|
295
|
+
- Long switch/if-else mapping a value → object lookup table
|
|
296
|
+
- Component with 4+ useState + useEffect → extract custom hook
|
|
297
|
+
- Props not destructured — props.x.y.z repeated many times
|
|
298
|
+
- Destructure with fallbacks at the top instead of scattering optional chaining everywhere:
|
|
299
|
+
Bad: user?.profile?.name, user?.profile?.age, user?.profile?.avatar used throughout
|
|
300
|
+
Good: const { name = '', age = 0, avatar = null } = user?.profile ?? {}
|
|
301
|
+
then use name, age, avatar cleanly — no ?. needed anywhere below
|
|
302
|
+
- Nested optional chaining repeated 3+ times on the same object — destructure it once at the top of the function/component
|
|
303
|
+
- Function params not destructured with defaults:
|
|
304
|
+
Bad: function Card(props) { const title = props.title ?? 'Untitled'; const size = props.size ?? 'md' }
|
|
305
|
+
Good: function Card({ title = 'Untitled', size = 'md', onPress }) {
|
|
306
|
+
- API response or deeply nested object accessed in multiple places without destructuring:
|
|
307
|
+
Bad: response.data.user.address.city, response.data.user.address.zip, response.data.user.address.state
|
|
308
|
+
Good: const { city, zip, state } = response.data.user.address ?? {}
|
|
309
|
+
- Same fetch + loading + error pattern in 3+ components → custom hook
|
|
310
|
+
- Utility function already exists somewhere → import it
|
|
311
|
+
- Sequential awaits on independent promises → Promise.all
|
|
312
|
+
- Functions with hidden side effects not obvious from name
|
|
313
|
+
- Mixing abstraction levels in one function
|
|
314
|
+
|
|
315
|
+
PASS 8 — DUPLICATE DETECTION [tag: DUPLICATE]
|
|
316
|
+
Using the EXISTING CODEBASE above:
|
|
317
|
+
- Component functionally identical to an existing one
|
|
318
|
+
- 70%+ structural overlap — should be a prop/variant instead
|
|
319
|
+
- Utility function already in utils/ or lib/
|
|
320
|
+
- Hook logic already extracted elsewhere
|
|
321
|
+
- Reinventing a component from the project's own UI library
|
|
322
|
+
- API fetch logic duplicated — should be a shared service or hook
|
|
323
|
+
|
|
324
|
+
PASS 9 — NON-FATALS & SILENT FAILURES [tag: NON-FATAL]
|
|
325
|
+
All frameworks:
|
|
326
|
+
- Race condition between concurrent async calls — no AbortController
|
|
327
|
+
- Missing loading/null guard before accessing async data
|
|
328
|
+
- Stale closure in useEffect or event handler
|
|
329
|
+
- Network error not handled — component stuck loading forever
|
|
330
|
+
- Empty catch swallowing errors the user should see
|
|
331
|
+
- Form submission not disabled during loading — double submit possible
|
|
332
|
+
- Missing optimistic update rollback on error
|
|
333
|
+
|
|
334
|
+
Next.js specific:
|
|
335
|
+
- revalidatePath called with wrong path after mutation — data stays stale
|
|
336
|
+
- Server Action error not surfaced to the user
|
|
337
|
+
- redirect() used in Server Action not imported from next/navigation
|
|
338
|
+
- Cookie set without httpOnly/secure flags on sensitive values
|
|
339
|
+
- fetch() response not checked for ok before parsing JSON
|
|
340
|
+
|
|
341
|
+
PASS 10 — UNDECLARED & MISSING REFERENCES [tag: UNDECLARED]
|
|
342
|
+
- Variable or constant used but never declared or imported in this file
|
|
343
|
+
- Prop used inside component but not listed in the props interface or destructured params
|
|
344
|
+
- Function called but never defined or imported
|
|
345
|
+
- Hook used but not imported from react (useState, useEffect, useMemo etc)
|
|
346
|
+
- Component rendered in JSX but never imported
|
|
347
|
+
- Type or interface used but never declared or imported
|
|
348
|
+
- Constant referenced but defined in a different scope or file without import
|
|
349
|
+
- Destructured variable that doesn't exist on the object:
|
|
350
|
+
const { foo } = user — but user has no foo property based on its type/usage
|
|
351
|
+
- Default export used as named import or vice versa
|
|
352
|
+
- Package imported but not installed — not in package.json dependencies
|
|
353
|
+
- Import path that doesn't match actual file structure (wrong relative path)
|
|
354
|
+
- Re-exported variable from a barrel file that doesn't actually export it
|
|
355
|
+
|
|
356
|
+
PASS 11 — NAMING & STYLE [tag: STYLE]
|
|
357
|
+
- Vague names: data, info, res, val, temp, handleStuff
|
|
358
|
+
- Boolean not named as predicate: loading → isLoading, modal → isModalOpen
|
|
359
|
+
- Comment explains WHAT instead of WHY
|
|
360
|
+
- Magic number/string without a named constant
|
|
361
|
+
- Dead code: unused imports, variables, commented-out blocks
|
|
362
|
+
- File over ~200 lines — usually doing too much
|
|
363
|
+
|
|
364
|
+
════════════════════════════════════════════
|
|
365
|
+
OUTPUT FORMAT — FOLLOW EXACTLY:
|
|
366
|
+
════════════════════════════════════════════
|
|
367
|
+
|
|
368
|
+
For each issue:
|
|
369
|
+
|
|
370
|
+
[SEVERITY] [TAG] path/to/file.tsx:lineNumber
|
|
371
|
+
Problem: one sentence — what exactly is wrong
|
|
372
|
+
Risk: what happens in production if not fixed
|
|
373
|
+
Fix:
|
|
374
|
+
\`\`\`
|
|
375
|
+
// Before
|
|
376
|
+
<bad code — max 8 lines>
|
|
377
|
+
|
|
378
|
+
// After
|
|
379
|
+
<fixed code — max 8 lines>
|
|
380
|
+
\`\`\`
|
|
381
|
+
|
|
382
|
+
Severity:
|
|
383
|
+
🔴 BLOCK — security or crash risk. Commit rejected.
|
|
384
|
+
🟡 WARN — performance regression or logic bug. Must fix.
|
|
385
|
+
🔵 SUGGEST — better way to write it. Educational.
|
|
386
|
+
🟠 CONVENTION — team has a standard for this. Use it.
|
|
387
|
+
🟣 WRONG_PKG — wrong package for this framework. Will crash or not work.
|
|
388
|
+
🔍 UNDECLARED — variable, prop, or import missing or never declared.
|
|
389
|
+
⚪ STYLE — naming, dead code, readability.
|
|
390
|
+
|
|
391
|
+
Rules:
|
|
392
|
+
- Group by PASS heading
|
|
393
|
+
- Skip passes with no issues entirely
|
|
394
|
+
- No padding, no encouragement, no generic commentary
|
|
395
|
+
- If nothing found at all: LGTM — no issues found.
|
|
396
|
+
|
|
397
|
+
After all findings output raw JSON (no markdown):
|
|
398
|
+
REVIEW_METADATA_START
|
|
399
|
+
{
|
|
400
|
+
"has_blockers": false,
|
|
401
|
+
"new_patterns_found": ["short description of any new recurring issue type"],
|
|
402
|
+
"categories_flagged": ["SECURITY","CRASH","HYDRATION","NEXTJS","CONVENTION","WRONG_PKG","UNDECLARED","PERF","SUGGEST","DUPLICATE","NON-FATAL","STYLE"],
|
|
403
|
+
"top_issue": "one sentence — the single most important thing to fix"
|
|
404
|
+
}
|
|
405
|
+
REVIEW_METADATA_END`;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
module.exports = { buildPrompt };
|
package/src/config.js
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// ── config.js ─────────────────────────────────────────────
|
|
2
|
+
// Finds .env relative to the git root of whichever sub-project
|
|
3
|
+
// you're currently in. Works in monorepos:
|
|
4
|
+
//
|
|
5
|
+
// newmecode/nextjs/ → loads newmecode/nextjs/.env
|
|
6
|
+
// newmecode/mobile/ → loads newmecode/mobile/.env
|
|
7
|
+
// newmecode/pos/ → loads newmecode/pos/.env
|
|
8
|
+
|
|
9
|
+
const fs = require("fs");
|
|
10
|
+
const path = require("path");
|
|
11
|
+
const { execSync } = require("child_process");
|
|
12
|
+
|
|
13
|
+
// ── Find the nearest .env walking up from cwd ─────────────
|
|
14
|
+
function findEnvFile() {
|
|
15
|
+
// First try: git root of the immediate repo/sub-project
|
|
16
|
+
try {
|
|
17
|
+
const gitRoot = execSync("git rev-parse --show-toplevel", {
|
|
18
|
+
encoding: "utf8",
|
|
19
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
20
|
+
}).trim();
|
|
21
|
+
|
|
22
|
+
const envAtGitRoot = path.join(gitRoot, ".env");
|
|
23
|
+
if (fs.existsSync(envAtGitRoot)) return envAtGitRoot;
|
|
24
|
+
} catch {
|
|
25
|
+
// not a git repo — fall through to cwd walk
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Second try: walk up from cwd looking for .env
|
|
29
|
+
let dir = process.cwd();
|
|
30
|
+
for (let i = 0; i < 6; i++) {
|
|
31
|
+
const candidate = path.join(dir, ".env");
|
|
32
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
33
|
+
const parent = path.dirname(dir);
|
|
34
|
+
if (parent === dir) break;
|
|
35
|
+
dir = parent;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ── Parse and load .env file ──────────────────────────────
|
|
42
|
+
function loadEnv() {
|
|
43
|
+
const envPath = findEnvFile();
|
|
44
|
+
if (!envPath) return;
|
|
45
|
+
|
|
46
|
+
const lines = fs.readFileSync(envPath, "utf8").split("\n");
|
|
47
|
+
|
|
48
|
+
for (const line of lines) {
|
|
49
|
+
const trimmed = line.trim();
|
|
50
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
51
|
+
|
|
52
|
+
const eqIndex = trimmed.indexOf("=");
|
|
53
|
+
if (eqIndex === -1) continue;
|
|
54
|
+
|
|
55
|
+
const key = trimmed.slice(0, eqIndex).trim();
|
|
56
|
+
const value = trimmed.slice(eqIndex + 1).trim()
|
|
57
|
+
.replace(/^["']|["']$/g, "");
|
|
58
|
+
|
|
59
|
+
if (!process.env[key]) {
|
|
60
|
+
process.env[key] = value;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (process.env.AI_REVIEWER_VERBOSE === "true") {
|
|
65
|
+
process.stderr.write(` [ai-reviewer] loaded env: ${envPath}\n`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
loadEnv();
|
|
70
|
+
|
|
71
|
+
module.exports = {
|
|
72
|
+
model: process.env.AI_REVIEWER_MODEL || "gemini-1.5-flash",
|
|
73
|
+
apiKey: process.env.GEMINI_API_KEY || process.env.OPENAI_API_KEY || "",
|
|
74
|
+
|
|
75
|
+
reviewerDir: ".ai-reviewer",
|
|
76
|
+
patternsFile: ".ai-reviewer/patterns.json",
|
|
77
|
+
contextFile: ".ai-reviewer/codebase-context.json",
|
|
78
|
+
logFile: ".ai-reviewer/review-log.jsonl",
|
|
79
|
+
dashboardFile: ".ai-reviewer/dashboard.html",
|
|
80
|
+
|
|
81
|
+
maxContextLines: 60,
|
|
82
|
+
maxDiffChars: 14000,
|
|
83
|
+
maxSnapshotFiles: 80,
|
|
84
|
+
|
|
85
|
+
extensions: [".tsx", ".jsx", ".ts", ".js"],
|
|
86
|
+
srcDirs: ["src", "app", "components", "screens", "hooks", "utils", "lib", "features", "pages","containers"],
|
|
87
|
+
ignorePatterns: ["__tests__", ".test.", ".spec.", "node_modules", ".min.", "dist/", "build/", ".d.ts"],
|
|
88
|
+
|
|
89
|
+
maxRecurringIssues: 50,
|
|
90
|
+
maxBlindSpots: 10,
|
|
91
|
+
|
|
92
|
+
verbose: process.env.AI_REVIEWER_VERBOSE === "true",
|
|
93
|
+
};
|
package/src/index.js
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
const config = require("./config");
|
|
2
|
+
const { bootstrap, loadPatterns, updateMemory } = require("./memory");
|
|
3
|
+
const { getStagedFiles, getStagedDiff, buildCodebaseSnapshot, getGitInfo } = require("./analyzer/git");
|
|
4
|
+
const { buildPrompt } = require("./analyzer/prompt");
|
|
5
|
+
const { callAI, parseMetadata } = require("./analyzer/api");
|
|
6
|
+
const { printDivider, printHeader, printReview, printVerdict, C } = require("./output/colors");
|
|
7
|
+
|
|
8
|
+
const isDryRun = process.argv.includes("--dry-run");
|
|
9
|
+
|
|
10
|
+
async function main() {
|
|
11
|
+
const hasKey =
|
|
12
|
+
process.env.ANTHROPIC_API_KEY ||
|
|
13
|
+
process.env.GEMINI_API_KEY ||
|
|
14
|
+
process.env.OPENAI_API_KEY;
|
|
15
|
+
|
|
16
|
+
if (!hasKey) {
|
|
17
|
+
process.stderr.write(
|
|
18
|
+
`${C.yellow}⚠ No API key found — add to .env:\n` +
|
|
19
|
+
` ANTHROPIC_API_KEY=... (recommended, $5 free)\n` +
|
|
20
|
+
` GEMINI_API_KEY=... (free tier)\n` +
|
|
21
|
+
` OPENAI_API_KEY=... (paid)${C.reset}\n`
|
|
22
|
+
);
|
|
23
|
+
process.exit(0);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
bootstrap();
|
|
27
|
+
|
|
28
|
+
const stagedFiles = getStagedFiles();
|
|
29
|
+
if (!stagedFiles.length) {
|
|
30
|
+
if (config.verbose) process.stdout.write("No React/RN files staged — skipping.\n");
|
|
31
|
+
process.exit(0);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (isDryRun) {
|
|
35
|
+
process.stdout.write(`${C.cyan}[dry-run] Would review: ${stagedFiles.join(", ")}${C.reset}\n`);
|
|
36
|
+
process.exit(0);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const diff = getStagedDiff(stagedFiles);
|
|
40
|
+
if (!diff) {
|
|
41
|
+
if (config.verbose) process.stdout.write("Empty diff — skipping.\n");
|
|
42
|
+
process.exit(0);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const patterns = loadPatterns();
|
|
46
|
+
const gitInfo = getGitInfo();
|
|
47
|
+
|
|
48
|
+
printHeader(stagedFiles.length, patterns.total_commits_reviewed, patterns.team_blind_spots);
|
|
49
|
+
|
|
50
|
+
if (config.verbose) {
|
|
51
|
+
process.stdout.write(` Branch: ${gitInfo.branch} | Author: ${gitInfo.author}\n\n`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
process.stdout.write(` Building codebase snapshot...\n`);
|
|
55
|
+
const codebaseSnapshot = buildCodebaseSnapshot(stagedFiles);
|
|
56
|
+
|
|
57
|
+
const provider =
|
|
58
|
+
process.env.ANTHROPIC_API_KEY ? "Anthropic" :
|
|
59
|
+
process.env.GEMINI_API_KEY ? "Gemini" : "OpenAI";
|
|
60
|
+
process.stdout.write(` Sending to ${provider}...\n\n`);
|
|
61
|
+
|
|
62
|
+
let review;
|
|
63
|
+
try {
|
|
64
|
+
const prompt = buildPrompt({ diff, codebaseSnapshot, patterns });
|
|
65
|
+
review = await callAI(prompt);
|
|
66
|
+
} catch (err) {
|
|
67
|
+
process.stderr.write(`${C.yellow}⚠ ${err.message} — skipping review${C.reset}\n`);
|
|
68
|
+
process.exit(0);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (!review) {
|
|
72
|
+
process.stderr.write(`${C.yellow}⚠ Empty response — skipping review${C.reset}\n`);
|
|
73
|
+
process.exit(0);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
printDivider();
|
|
77
|
+
if (review.includes("LGTM")) {
|
|
78
|
+
process.stdout.write(`${C.green}${C.bold} ✓ LGTM — no issues found${C.reset}\n`);
|
|
79
|
+
} else {
|
|
80
|
+
printReview(review);
|
|
81
|
+
}
|
|
82
|
+
printDivider();
|
|
83
|
+
|
|
84
|
+
const metadata = parseMetadata(review);
|
|
85
|
+
const updatedPatterns = updateMemory(metadata, stagedFiles);
|
|
86
|
+
printVerdict(metadata.has_blockers, metadata.top_issue, updatedPatterns);
|
|
87
|
+
|
|
88
|
+
process.exit(metadata.has_blockers ? 1 : 0);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
main().catch((err) => {
|
|
92
|
+
process.stderr.write(`${C.red}AI reviewer error: ${err.message}${C.reset}\n`);
|
|
93
|
+
process.exit(0);
|
|
94
|
+
});
|