@levironexe/architect 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (210) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/CONTRIBUTING.md +55 -0
  3. package/README.md +341 -0
  4. package/dist/analyzers/ast-parser.d.ts +3 -0
  5. package/dist/analyzers/ast-parser.js +305 -0
  6. package/dist/analyzers/ast-parser.js.map +1 -0
  7. package/dist/analyzers/dependency-graph.d.ts +2 -0
  8. package/dist/analyzers/dependency-graph.js +67 -0
  9. package/dist/analyzers/dependency-graph.js.map +1 -0
  10. package/dist/analyzers/duplication.d.ts +2 -0
  11. package/dist/analyzers/duplication.js +56 -0
  12. package/dist/analyzers/duplication.js.map +1 -0
  13. package/dist/analyzers/file-walker.d.ts +3 -0
  14. package/dist/analyzers/file-walker.js +80 -0
  15. package/dist/analyzers/file-walker.js.map +1 -0
  16. package/dist/cli/context-runner.d.ts +1 -0
  17. package/dist/cli/context-runner.js +16 -0
  18. package/dist/cli/context-runner.js.map +1 -0
  19. package/dist/cli/index.d.ts +24 -0
  20. package/dist/cli/index.js +217 -0
  21. package/dist/cli/index.js.map +1 -0
  22. package/dist/cli/init-runner.d.ts +25 -0
  23. package/dist/cli/init-runner.js +152 -0
  24. package/dist/cli/init-runner.js.map +1 -0
  25. package/dist/cli/scan-runner.d.ts +8 -0
  26. package/dist/cli/scan-runner.js +133 -0
  27. package/dist/cli/scan-runner.js.map +1 -0
  28. package/dist/formatters/plan-json.d.ts +2 -0
  29. package/dist/formatters/plan-json.js +4 -0
  30. package/dist/formatters/plan-json.js.map +1 -0
  31. package/dist/formatters/plan-markdown.d.ts +2 -0
  32. package/dist/formatters/plan-markdown.js +42 -0
  33. package/dist/formatters/plan-markdown.js.map +1 -0
  34. package/dist/formatters/plan-prompt.d.ts +4 -0
  35. package/dist/formatters/plan-prompt.js +5 -0
  36. package/dist/formatters/plan-prompt.js.map +1 -0
  37. package/dist/formatters/plan-terminal.d.ts +5 -0
  38. package/dist/formatters/plan-terminal.js +62 -0
  39. package/dist/formatters/plan-terminal.js.map +1 -0
  40. package/dist/generators/blueprint-renderer.d.ts +3 -0
  41. package/dist/generators/blueprint-renderer.js +27 -0
  42. package/dist/generators/blueprint-renderer.js.map +1 -0
  43. package/dist/generators/claudeWriter.d.ts +3 -0
  44. package/dist/generators/claudeWriter.js +9 -0
  45. package/dist/generators/claudeWriter.js.map +1 -0
  46. package/dist/generators/copilotWriter.d.ts +3 -0
  47. package/dist/generators/copilotWriter.js +11 -0
  48. package/dist/generators/copilotWriter.js.map +1 -0
  49. package/dist/generators/cursorWriter.d.ts +3 -0
  50. package/dist/generators/cursorWriter.js +14 -0
  51. package/dist/generators/cursorWriter.js.map +1 -0
  52. package/dist/generators/genericWriter.d.ts +3 -0
  53. package/dist/generators/genericWriter.js +9 -0
  54. package/dist/generators/genericWriter.js.map +1 -0
  55. package/dist/generators/template-context.d.ts +18 -0
  56. package/dist/generators/template-context.js +126 -0
  57. package/dist/generators/template-context.js.map +1 -0
  58. package/dist/generators/templateRenderer.d.ts +2 -0
  59. package/dist/generators/templateRenderer.js +19 -0
  60. package/dist/generators/templateRenderer.js.map +1 -0
  61. package/dist/generators/windsurfWriter.d.ts +3 -0
  62. package/dist/generators/windsurfWriter.js +14 -0
  63. package/dist/generators/windsurfWriter.js.map +1 -0
  64. package/dist/generators/writer-types.d.ts +11 -0
  65. package/dist/generators/writer-types.js +40 -0
  66. package/dist/generators/writer-types.js.map +1 -0
  67. package/dist/llm/claude-provider.d.ts +8 -0
  68. package/dist/llm/claude-provider.js +22 -0
  69. package/dist/llm/claude-provider.js.map +1 -0
  70. package/dist/llm/concern-classifier.d.ts +15 -0
  71. package/dist/llm/concern-classifier.js +61 -0
  72. package/dist/llm/concern-classifier.js.map +1 -0
  73. package/dist/llm/config.d.ts +11 -0
  74. package/dist/llm/config.js +120 -0
  75. package/dist/llm/config.js.map +1 -0
  76. package/dist/llm/ollama-provider.d.ts +8 -0
  77. package/dist/llm/ollama-provider.js +27 -0
  78. package/dist/llm/ollama-provider.js.map +1 -0
  79. package/dist/llm/openai-provider.d.ts +8 -0
  80. package/dist/llm/openai-provider.js +19 -0
  81. package/dist/llm/openai-provider.js.map +1 -0
  82. package/dist/llm/prompt-builder.d.ts +12 -0
  83. package/dist/llm/prompt-builder.js +132 -0
  84. package/dist/llm/prompt-builder.js.map +1 -0
  85. package/dist/llm/provider.d.ts +17 -0
  86. package/dist/llm/provider.js +2 -0
  87. package/dist/llm/provider.js.map +1 -0
  88. package/dist/llm/response-parser.d.ts +6 -0
  89. package/dist/llm/response-parser.js +128 -0
  90. package/dist/llm/response-parser.js.map +1 -0
  91. package/dist/planner/plan-generator.d.ts +7 -0
  92. package/dist/planner/plan-generator.js +275 -0
  93. package/dist/planner/plan-generator.js.map +1 -0
  94. package/dist/planner/plan-prompt-builder.d.ts +9 -0
  95. package/dist/planner/plan-prompt-builder.js +92 -0
  96. package/dist/planner/plan-prompt-builder.js.map +1 -0
  97. package/dist/planner/plan-response-parser.d.ts +7 -0
  98. package/dist/planner/plan-response-parser.js +21 -0
  99. package/dist/planner/plan-response-parser.js.map +1 -0
  100. package/dist/planner/plan-validator.d.ts +3 -0
  101. package/dist/planner/plan-validator.js +49 -0
  102. package/dist/planner/plan-validator.js.map +1 -0
  103. package/dist/reporters/scan-json.d.ts +13 -0
  104. package/dist/reporters/scan-json.js +26 -0
  105. package/dist/reporters/scan-json.js.map +1 -0
  106. package/dist/reporters/terminal.d.ts +6 -0
  107. package/dist/reporters/terminal.js +224 -0
  108. package/dist/reporters/terminal.js.map +1 -0
  109. package/dist/scoring/consistency-score.d.ts +3 -0
  110. package/dist/scoring/consistency-score.js +23 -0
  111. package/dist/scoring/consistency-score.js.map +1 -0
  112. package/dist/scoring/duplication-score.d.ts +3 -0
  113. package/dist/scoring/duplication-score.js +16 -0
  114. package/dist/scoring/duplication-score.js.map +1 -0
  115. package/dist/scoring/health-score.d.ts +4 -0
  116. package/dist/scoring/health-score.js +20 -0
  117. package/dist/scoring/health-score.js.map +1 -0
  118. package/dist/scoring/issue-builder.d.ts +4 -0
  119. package/dist/scoring/issue-builder.js +62 -0
  120. package/dist/scoring/issue-builder.js.map +1 -0
  121. package/dist/scoring/modularity-score.d.ts +3 -0
  122. package/dist/scoring/modularity-score.js +56 -0
  123. package/dist/scoring/modularity-score.js.map +1 -0
  124. package/dist/scoring/pattern-analysis.d.ts +3 -0
  125. package/dist/scoring/pattern-analysis.js +74 -0
  126. package/dist/scoring/pattern-analysis.js.map +1 -0
  127. package/dist/scoring/separation-score.d.ts +3 -0
  128. package/dist/scoring/separation-score.js +35 -0
  129. package/dist/scoring/separation-score.js.map +1 -0
  130. package/dist/skills/detector.d.ts +4 -0
  131. package/dist/skills/detector.js +104 -0
  132. package/dist/skills/detector.js.map +1 -0
  133. package/dist/skills/lister.d.ts +9 -0
  134. package/dist/skills/lister.js +35 -0
  135. package/dist/skills/lister.js.map +1 -0
  136. package/dist/skills/loader.d.ts +6 -0
  137. package/dist/skills/loader.js +76 -0
  138. package/dist/skills/loader.js.map +1 -0
  139. package/dist/skills/structure-check.d.ts +2 -0
  140. package/dist/skills/structure-check.js +37 -0
  141. package/dist/skills/structure-check.js.map +1 -0
  142. package/dist/skills/validator.d.ts +6 -0
  143. package/dist/skills/validator.js +229 -0
  144. package/dist/skills/validator.js.map +1 -0
  145. package/dist/types/analysis.d.ts +130 -0
  146. package/dist/types/analysis.js +41 -0
  147. package/dist/types/analysis.js.map +1 -0
  148. package/dist/types/concern.d.ts +48 -0
  149. package/dist/types/concern.js +16 -0
  150. package/dist/types/concern.js.map +1 -0
  151. package/dist/types/generation.d.ts +32 -0
  152. package/dist/types/generation.js +2 -0
  153. package/dist/types/generation.js.map +1 -0
  154. package/dist/types/issue.d.ts +12 -0
  155. package/dist/types/issue.js +2 -0
  156. package/dist/types/issue.js.map +1 -0
  157. package/dist/types/pattern.d.ts +15 -0
  158. package/dist/types/pattern.js +2 -0
  159. package/dist/types/pattern.js.map +1 -0
  160. package/dist/types/plan.d.ts +56 -0
  161. package/dist/types/plan.js +2 -0
  162. package/dist/types/plan.js.map +1 -0
  163. package/dist/types/scan-output.d.ts +84 -0
  164. package/dist/types/scan-output.js +2 -0
  165. package/dist/types/scan-output.js.map +1 -0
  166. package/dist/types/scoring.d.ts +15 -0
  167. package/dist/types/scoring.js +2 -0
  168. package/dist/types/scoring.js.map +1 -0
  169. package/dist/types/skill.d.ts +97 -0
  170. package/dist/types/skill.js +2 -0
  171. package/dist/types/skill.js.map +1 -0
  172. package/dist/utils/agent-detector.d.ts +2 -0
  173. package/dist/utils/agent-detector.js +22 -0
  174. package/dist/utils/agent-detector.js.map +1 -0
  175. package/dist/utils/interactive.d.ts +6 -0
  176. package/dist/utils/interactive.js +15 -0
  177. package/dist/utils/interactive.js.map +1 -0
  178. package/dist/utils/path.d.ts +5 -0
  179. package/dist/utils/path.js +31 -0
  180. package/dist/utils/path.js.map +1 -0
  181. package/dist/utils/progress.d.ts +17 -0
  182. package/dist/utils/progress.js +48 -0
  183. package/dist/utils/progress.js.map +1 -0
  184. package/dist/utils/thresholds.d.ts +6 -0
  185. package/dist/utils/thresholds.js +48 -0
  186. package/dist/utils/thresholds.js.map +1 -0
  187. package/package.json +63 -0
  188. package/skills/meta/general-js.skill.yaml +131 -0
  189. package/skills/patterns/clerk-auth.skill.yaml +349 -0
  190. package/skills/patterns/docker-deploy.skill.yaml +214 -0
  191. package/skills/patterns/drizzle.skill.yaml +277 -0
  192. package/skills/patterns/mongoose.skill.yaml +290 -0
  193. package/skills/patterns/nextauth.skill.yaml +308 -0
  194. package/skills/patterns/playwright-e2e.skill.yaml +265 -0
  195. package/skills/patterns/prisma.skill.yaml +255 -0
  196. package/skills/patterns/s3-storage.skill.yaml +235 -0
  197. package/skills/patterns/selenium-e2e.skill.yaml +276 -0
  198. package/skills/patterns/supabase-auth.skill.yaml +298 -0
  199. package/skills/patterns/supabase.skill.yaml +304 -0
  200. package/skills/patterns/vercel-deploy.skill.yaml +219 -0
  201. package/skills/patterns/vitest-testing.skill.yaml +262 -0
  202. package/skills/stacks/express-api.skill.yaml +155 -0
  203. package/skills/stacks/fastify-api.skill.yaml +119 -0
  204. package/skills/stacks/hono-api.skill.yaml +130 -0
  205. package/skills/stacks/nestjs.skill.yaml +135 -0
  206. package/skills/stacks/nextjs-app-router.skill.yaml +176 -0
  207. package/skills/stacks/react-spa.skill.yaml +153 -0
  208. package/skills/stacks/vue-nuxt.skill.yaml +115 -0
  209. package/templates/architect-plan.md +139 -0
  210. package/templates/architect-refactor.md +119 -0
@@ -0,0 +1,308 @@
1
+ schema_version: "2.0.0"
2
+ id: nextauth
3
+ name: "NextAuth.js / Auth.js"
4
+ version: "2.0.0"
5
+ description: "Self-hosted authentication with NextAuth.js v4 / Auth.js v5 — provider configuration, JWT/database session strategies, role-based access, and middleware route protection."
6
+ category: pattern
7
+ language: javascript
8
+ frameworks:
9
+ - next-auth
10
+ dependencies:
11
+ none:
12
+ - "@clerk/nextjs"
13
+ - "@supabase/ssr"
14
+ - lucia
15
+ detection:
16
+ dependencies:
17
+ any:
18
+ - next-auth
19
+ - "@auth/nextjs"
20
+ - "@auth/core"
21
+ source_indicators:
22
+ - "NextAuth("
23
+ - "authOptions"
24
+ - "getServerSession"
25
+ - "useSession"
26
+ - "SessionProvider"
27
+ - "auth()"
28
+ structure:
29
+ required_dirs:
30
+ - path: app/api/auth
31
+ purpose: "NextAuth.js catch-all route directory — must contain [...nextauth]/route.ts which exports both GET and POST handlers. All sign-in, sign-out, OAuth callbacks, CSRF protection, and provider redirects run through this single route. Never add business logic here."
32
+ recommended_dirs:
33
+ - path: src/lib
34
+ purpose: "Single authOptions export in auth.ts — provider configuration, session strategy, adapter wiring, jwt and session callbacks. Imported by the route handler, getServerSession calls, and middleware. Keeping authOptions in one file ensures all tokens are signed with the same secret."
35
+ - path: components
36
+ purpose: "Client Component wrappers that depend on next-auth/react hooks — primarily a Providers.tsx that wraps SessionProvider so the root layout stays a Server Component."
37
+ separation:
38
+ rules:
39
+ - concern: route_handler
40
+ belongs_in: app/api/auth
41
+ rule_text: "Create the NextAuth route handler at app/api/auth/[...nextauth]/route.ts. Import authOptions from src/lib/auth.ts — never inline provider config in the route file. The handler must export both GET and POST or sign-in and CSRF flows will break."
42
+ example: |
43
+ // app/api/auth/[...nextauth]/route.ts
44
+ import NextAuth from 'next-auth';
45
+ import { authOptions } from '@/lib/auth';
46
+
47
+ const handler = NextAuth(authOptions);
48
+ export { handler as GET, handler as POST };
49
+ indicators:
50
+ - "NextAuth("
51
+ - "[...nextauth]"
52
+ - "handler as GET"
53
+ - "handler as POST"
54
+ - concern: auth_config
55
+ belongs_in: src/lib
56
+ rule_text: "Define all providers, callbacks, adapter, and session strategy in src/lib/auth.ts as a single exported NextAuthOptions object. Every file that needs session data imports authOptions from this one location. The jwt and session callbacks must both be present to persist custom fields like user.id or user.role."
57
+ example: |
58
+ // src/lib/auth.ts — single authoritative NextAuth configuration
59
+ import { type NextAuthOptions } from 'next-auth';
60
+ import GithubProvider from 'next-auth/providers/github';
61
+ import { PrismaAdapter } from '@auth/prisma-adapter';
62
+ import { prisma } from '@/lib/db';
63
+
64
+ export const authOptions: NextAuthOptions = {
65
+ adapter: PrismaAdapter(prisma),
66
+ providers: [
67
+ GithubProvider({
68
+ clientId: process.env.GITHUB_ID!,
69
+ clientSecret: process.env.GITHUB_SECRET!,
70
+ }),
71
+ ],
72
+ session: { strategy: 'database' },
73
+ secret: process.env.NEXTAUTH_SECRET,
74
+ callbacks: {
75
+ // Thread custom fields from DB user into session
76
+ session({ session, user }) {
77
+ if (session.user) {
78
+ session.user.id = user.id;
79
+ (session.user as any).role = (user as any).role;
80
+ }
81
+ return session;
82
+ },
83
+ },
84
+ };
85
+ indicators:
86
+ - "authOptions"
87
+ - "NextAuthOptions"
88
+ - "providers:"
89
+ - "callbacks:"
90
+ - "NEXTAUTH_SECRET"
91
+ - concern: session_access
92
+ belongs_in: app
93
+ rule_text: "In Server Components and API routes, use getServerSession(authOptions) to read the session. In Client Components, use useSession() from next-auth/react. Never read session cookies or JWT tokens manually — getServerSession reads the cookie but handles decryption and expiry."
94
+ example: |
95
+ // Server Component — no loading state, data available on first render
96
+ import { getServerSession } from 'next-auth';
97
+ import { authOptions } from '@/lib/auth';
98
+ import { redirect } from 'next/navigation';
99
+
100
+ export default async function ProtectedPage() {
101
+ const session = await getServerSession(authOptions);
102
+ if (!session) redirect('/api/auth/signin');
103
+ return <Dashboard user={session.user} />;
104
+ }
105
+
106
+ // Client Component — shows loading state during hydration
107
+ 'use client';
108
+ import { useSession } from 'next-auth/react';
109
+ export function UserMenu() {
110
+ const { data: session, status } = useSession();
111
+ if (status === 'loading') return <Spinner />;
112
+ if (!session) return <SignInButton />;
113
+ return <UserAvatar user={session.user} />;
114
+ }
115
+ indicators:
116
+ - "getServerSession"
117
+ - "useSession()"
118
+ - "from 'next-auth/react'"
119
+ - "authOptions"
120
+ - concern: middleware_protection
121
+ belongs_in: middleware.ts
122
+ rule_text: "Export next-auth/middleware as the default export to protect matching routes. Configure the matcher pattern to exclude /api/auth (the NextAuth handler itself), static assets, and public pages. Use the authorized callback in authOptions for role-based access control."
123
+ example: |
124
+ // middleware.ts — blanket protection for dashboard and protected API routes
125
+ export { default } from 'next-auth/middleware';
126
+
127
+ export const config = {
128
+ matcher: [
129
+ '/dashboard/:path*',
130
+ '/admin/:path*',
131
+ '/api/protected/:path*',
132
+ // Does NOT match: /api/auth (NextAuth handler), /sign-in, static files
133
+ ],
134
+ };
135
+
136
+ // For role-based access, add authorized callback in authOptions:
137
+ // callbacks: {
138
+ // authorized({ token, req }) {
139
+ // if (req.nextUrl.pathname.startsWith('/admin')) return token?.role === 'admin';
140
+ // return !!token;
141
+ // },
142
+ // }
143
+ indicators:
144
+ - "next-auth/middleware"
145
+ - "matcher:"
146
+ - "authorized("
147
+ - concern: session_provider
148
+ belongs_in: components
149
+ rule_text: "Wrap SessionProvider in a dedicated 'use client' component so the root layout stays a Server Component. Pass the server-fetched session as a prop to SessionProvider to eliminate the loading flicker that occurs when SessionProvider fetches the session client-side."
150
+ example: |
151
+ // components/providers.tsx — isolates the 'use client' boundary
152
+ 'use client';
153
+ import { SessionProvider } from 'next-auth/react';
154
+ import type { Session } from 'next-auth';
155
+
156
+ export function Providers({
157
+ children,
158
+ session,
159
+ }: {
160
+ children: React.ReactNode;
161
+ session: Session | null;
162
+ }) {
163
+ return <SessionProvider session={session}>{children}</SessionProvider>;
164
+ }
165
+
166
+ // app/layout.tsx — Server Component, fetches session for SSR hydration
167
+ import { getServerSession } from 'next-auth';
168
+ import { authOptions } from '@/lib/auth';
169
+ import { Providers } from '@/components/providers';
170
+
171
+ export default async function RootLayout({ children }: { children: React.ReactNode }) {
172
+ const session = await getServerSession(authOptions);
173
+ return (
174
+ <html lang="en">
175
+ <body>
176
+ <Providers session={session}>{children}</Providers>
177
+ </body>
178
+ </html>
179
+ );
180
+ }
181
+ indicators:
182
+ - "SessionProvider"
183
+ - "session={session}"
184
+ - "from 'next-auth/react'"
185
+ patterns:
186
+ data_flow:
187
+ direction: "Request → Middleware (withAuth) → Server Component (getServerSession) → Service → DB"
188
+ rules:
189
+ - "Middleware protects routes before any rendering — pages don't need to re-check for middleware-covered paths."
190
+ - "Server Components call getServerSession(authOptions) — reads the encrypted cookie without an extra network call."
191
+ - "Client Components call useSession() — reads from SessionProvider context already in the page, zero extra requests."
192
+ - "authOptions is defined once in src/lib/auth.ts — imported by route handler, middleware, and all session accessors so they all use the same secret and config."
193
+ - "Custom session fields (user.id, user.role) must flow through both jwt callback (token enrichment) and session callback (token → session mapping)."
194
+ - "Database sessions (strategy: 'database') store the session in your DB via the adapter — JWT sessions store everything in the encrypted cookie."
195
+ error_handling:
196
+ recommended: "Check if session is null before accessing session.user properties — getServerSession() returns null for unauthenticated requests, not an error. Redirect to /api/auth/signin to let NextAuth handle the sign-in flow rather than a custom page."
197
+ naming:
198
+ config: "src/lib/auth.ts — single authOptions export, imported by all session consumers"
199
+ route_handler: "app/api/auth/[...nextauth]/route.ts — exports GET and POST"
200
+ session_provider: "components/providers.tsx — 'use client' wrapper for SessionProvider"
201
+ session_helper: "getServerSession(authOptions) in Server Components; useSession() in Client Components"
202
+ anti_patterns:
203
+ - id: missing_secret
204
+ severity: critical
205
+ description: "Running NextAuth without NEXTAUTH_SECRET set to a strong random value. In development, NextAuth uses an insecure default and issues a warning. In production, it throws a hard startup error. Without a strong secret, session tokens can be forged by brute-force."
206
+ bad_example: |
207
+ # .env — weak or missing secret
208
+ NEXTAUTH_SECRET=secret # too short, too predictable
209
+ # Or: NEXTAUTH_SECRET not set at all — uses insecure default in dev
210
+ good_example: |
211
+ # .env.local — strong random secret, 32+ characters
212
+ NEXTAUTH_SECRET=your-32-char-or-longer-random-secret-here
213
+ # Generate with: openssl rand -base64 32
214
+ # Or: node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"
215
+ - id: session_not_checked_in_api
216
+ severity: warning
217
+ description: "API routes outside the middleware matcher that return sensitive data without calling getServerSession(). The middleware only protects matched paths — any API route not in the matcher must check the session itself."
218
+ bad_example: |
219
+ // ❌ No session check — any request (including unauthenticated) gets the data
220
+ export async function GET() {
221
+ const users = await db.user.findMany({ select: { email: true, salary: true } });
222
+ return Response.json(users);
223
+ }
224
+ good_example: |
225
+ // ✓ Explicit session check before accessing protected data
226
+ import { getServerSession } from 'next-auth';
227
+ import { authOptions } from '@/lib/auth';
228
+ export async function GET() {
229
+ const session = await getServerSession(authOptions);
230
+ if (!session) return new Response('Unauthorized', { status: 401 });
231
+ const users = await db.user.findMany({ select: { email: true } });
232
+ return Response.json(users);
233
+ }
234
+ - id: duplicated_auth_options
235
+ severity: warning
236
+ description: "Defining authOptions inline in multiple API routes instead of importing from src/lib/auth.ts. Each definition can have a different secret or provider config — tokens signed by one definition are rejected by another, causing intermittent auth failures that are hard to debug."
237
+ bad_example: |
238
+ // ❌ authOptions defined inline in the route file
239
+ // app/api/auth/[...nextauth]/route.ts
240
+ const handler = NextAuth({
241
+ providers: [GithubProvider({ clientId: '...', clientSecret: '...' })],
242
+ secret: process.env.NEXTAUTH_SECRET,
243
+ // Different callbacks or session strategy in another file → token mismatch
244
+ });
245
+ good_example: |
246
+ // ✓ Import from the single source of truth
247
+ import { authOptions } from '@/lib/auth';
248
+ const handler = NextAuth(authOptions);
249
+ export { handler as GET, handler as POST };
250
+ - id: custom_fields_not_in_callbacks
251
+ severity: warning
252
+ description: "Extending the Session type with custom fields (user.id, user.role) but not adding both a jwt callback and a session callback to populate them. The TypeScript types compile, but the fields are undefined at runtime because the values are never written into the token or session object."
253
+ bad_example: |
254
+ // next-auth.d.ts: Session.user.role declared as string
255
+ // authOptions — callbacks are missing entirely
256
+ export const authOptions: NextAuthOptions = {
257
+ providers: [...],
258
+ // session.user.role is always undefined at runtime
259
+ };
260
+ good_example: |
261
+ // ✓ Both callbacks required: jwt enriches the token, session maps token → session
262
+ callbacks: {
263
+ async jwt({ token, user }) {
264
+ if (user) token.role = (user as any).role; // runs on sign-in only
265
+ return token;
266
+ },
267
+ async session({ session, token }) {
268
+ if (session.user) session.user.role = token.role as string; // every request
269
+ return session;
270
+ },
271
+ },
272
+ - id: credentials_provider_plain_text
273
+ severity: critical
274
+ description: "Using CredentialsProvider and comparing passwords as plain text. If the database is breached, every user's password is immediately exposed. Passwords must always be stored as bcrypt or argon2 hashes."
275
+ bad_example: |
276
+ // ❌ Plain text comparison — catastrophic on any database breach
277
+ CredentialsProvider({
278
+ authorize: async (credentials) => {
279
+ const user = await db.user.findFirst({ where: { email: credentials?.email } });
280
+ if (user?.password === credentials?.password) return user; // raw string compare
281
+ return null;
282
+ },
283
+ })
284
+ good_example: |
285
+ // ✓ bcrypt — one-way hash, breach-safe
286
+ import bcrypt from 'bcryptjs';
287
+ CredentialsProvider({
288
+ authorize: async (credentials) => {
289
+ const user = await db.user.findFirst({ where: { email: credentials?.email } });
290
+ if (!user?.passwordHash) return null;
291
+ const valid = await bcrypt.compare(credentials?.password ?? '', user.passwordHash);
292
+ return valid ? { id: user.id, email: user.email } : null;
293
+ },
294
+ })
295
+ - id: adapter_with_jwt_strategy
296
+ severity: warning
297
+ description: "Configuring a database adapter (e.g., PrismaAdapter) with session: { strategy: 'jwt' }. With JWT strategy, NextAuth does not write sessions to the database — the adapter is wired but ignored for session storage, causing confusion and wasted DB connections."
298
+ bad_example: |
299
+ // ❌ Adapter installed but JWT strategy means sessions never hit the DB
300
+ export const authOptions: NextAuthOptions = {
301
+ adapter: PrismaAdapter(prisma),
302
+ session: { strategy: 'jwt' }, // adapter's Session table is never written
303
+ };
304
+ good_example: |
305
+ // ✓ Use database strategy when you have an adapter
306
+ adapter: PrismaAdapter(prisma),
307
+ session: { strategy: 'database' },
308
+ // — OR — remove the adapter entirely for JWT-only sessions (no DB table needed)
@@ -0,0 +1,265 @@
1
+ schema_version: "2.0.0"
2
+ id: playwright-e2e
3
+ name: "Playwright E2E"
4
+ version: "2.0.0"
5
+ description: "End-to-end testing with Playwright — Page Object Models, storageState-based auth fixtures, semantic selectors, baseURL from config, globalSetup for one-time login, and CI-optimized retries."
6
+ category: pattern
7
+ language: javascript
8
+ frameworks:
9
+ - playwright
10
+ dependencies:
11
+ none:
12
+ - "@jest/globals"
13
+ detection:
14
+ dependencies:
15
+ any:
16
+ - "@playwright/test"
17
+ source_indicators:
18
+ - "from '@playwright/test'"
19
+ - "test.describe("
20
+ - "page.goto("
21
+ - "expect(page)"
22
+ - "storageState"
23
+ structure:
24
+ required_dirs:
25
+ - path: tests/e2e
26
+ purpose: "E2E test specs organized by user flow or feature. Each spec file covers one major feature (e.g., auth.spec.ts, checkout.spec.ts). Test specs import Page Objects and fixtures — they never contain raw selectors or setup logic."
27
+ recommended_dirs:
28
+ - path: tests/e2e/pages
29
+ purpose: "Page Object Model classes — one class per page or major UI component. The POM encapsulates all selectors and user-facing actions. Test specs call POM methods (loginPage.login(), checkoutPage.placeOrder()) — they never call page.locator() or page.getByRole() directly."
30
+ - path: tests/e2e/fixtures
31
+ purpose: "Custom Playwright fixtures extending the base `test` — authenticated page fixtures that load storageState from globalSetup. Import this extended `test` in all specs that need auth instead of the default @playwright/test export."
32
+ - path: tests/e2e/global-setup
33
+ purpose: "globalSetup.ts — one-time setup that runs before all specs. Performs the login flow once, saves storageState to a file (e.g., .auth/user.json), and reuses it across all tests via fixtures. Eliminates repeated login in every spec file."
34
+ separation:
35
+ rules:
36
+ - concern: page_object_models
37
+ belongs_in: tests/e2e/pages
38
+ rule_text: "Encapsulate all page selectors and user-action sequences in Page Object Model classes. Each POM class receives a Playwright Page object in its constructor. Methods represent user intentions (login(), submitForm(), navigateToSettings()) — not individual click/fill operations. Tests call POM methods and assert on outcomes."
39
+ example: |
40
+ // tests/e2e/pages/login.page.ts
41
+ import type { Page } from '@playwright/test';
42
+
43
+ export class LoginPage {
44
+ constructor(private readonly page: Page) {}
45
+
46
+ async goto() {
47
+ await this.page.goto('/login');
48
+ }
49
+
50
+ async login(email: string, password: string) {
51
+ await this.page.getByLabel('Email').fill(email);
52
+ await this.page.getByLabel('Password').fill(password);
53
+ await this.page.getByRole('button', { name: 'Sign in' }).click();
54
+ // Wait for navigation to complete
55
+ await this.page.waitForURL('**/dashboard');
56
+ }
57
+
58
+ async getErrorMessage() {
59
+ return this.page.getByRole('alert').textContent();
60
+ }
61
+ }
62
+
63
+ // tests/e2e/auth.spec.ts — test calls POM, not selectors
64
+ const loginPage = new LoginPage(page);
65
+ await loginPage.goto();
66
+ await loginPage.login('user@test.com', 'password');
67
+ await expect(page).toHaveURL('/dashboard');
68
+ indicators:
69
+ - "class LoginPage"
70
+ - "class DashboardPage"
71
+ - "new LoginPage(page)"
72
+ - concern: shared_fixtures
73
+ belongs_in: tests/e2e/fixtures
74
+ rule_text: "Create custom Playwright fixtures that extend the base test with authenticated page contexts. Load storageState (saved by globalSetup) into the browser context — this skips the login UI entirely for specs that need auth. All authenticated specs import the custom test, not the default one from @playwright/test."
75
+ example: |
76
+ // tests/e2e/fixtures/auth.fixture.ts
77
+ import { test as base, type Page } from '@playwright/test';
78
+ import path from 'path';
79
+
80
+ type AuthFixtures = { authedPage: Page };
81
+
82
+ export const test = base.extend<AuthFixtures>({
83
+ authedPage: async ({ browser }, use) => {
84
+ const context = await browser.newContext({
85
+ storageState: path.resolve('.auth/user.json'), // saved by globalSetup
86
+ });
87
+ const page = await context.newPage();
88
+ await use(page);
89
+ await context.close();
90
+ },
91
+ });
92
+
93
+ export { expect } from '@playwright/test';
94
+
95
+ // tests/e2e/dashboard.spec.ts — uses auth fixture
96
+ import { test, expect } from '../fixtures/auth.fixture';
97
+ test('shows user data', async ({ authedPage }) => {
98
+ await authedPage.goto('/dashboard');
99
+ await expect(authedPage.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
100
+ });
101
+ indicators:
102
+ - "base.extend"
103
+ - "storageState"
104
+ - "authedPage"
105
+ - concern: selectors
106
+ belongs_in: tests/e2e/pages
107
+ rule_text: "Use semantic, user-facing selectors in this priority order: (1) getByRole with name — most resilient, matches what screen readers see; (2) getByLabel — for form inputs; (3) getByTestId — for complex interactive components where role/label is impractical; (4) getByText — for links and non-interactive text. Never use CSS class selectors or XPath."
108
+ example: |
109
+ // ✓ Role-based — matches semantic HTML, resilient to style changes
110
+ await page.getByRole('button', { name: 'Submit' }).click();
111
+ await page.getByRole('link', { name: 'View profile' }).click();
112
+
113
+ // ✓ Label-based — for form inputs
114
+ await page.getByLabel('Email address').fill('user@test.com');
115
+
116
+ // ✓ data-testid — for complex interactive widgets without clear role
117
+ await page.getByTestId('date-picker-input').click();
118
+
119
+ // ❌ CSS class — breaks on class rename
120
+ // await page.locator('.btn-primary').click();
121
+ // ❌ XPath — brittle to DOM structure changes
122
+ // await page.locator('//div[@class="form"]/button[1]').click();
123
+ indicators:
124
+ - "getByRole"
125
+ - "getByLabel"
126
+ - "getByTestId"
127
+ - concern: config
128
+ belongs_in: playwright.config.ts
129
+ rule_text: "Configure baseURL, retries, timeout, and reporter in playwright.config.ts. Never hardcode localhost URLs in test files — use page.goto('/path') which Playwright prepends with baseURL. Use environment variables for baseURL to support testing in different environments (local, staging, CI)."
130
+ example: |
131
+ // playwright.config.ts
132
+ import { defineConfig, devices } from '@playwright/test';
133
+
134
+ export default defineConfig({
135
+ testDir: './tests/e2e',
136
+ globalSetup: './tests/e2e/global-setup/auth.setup.ts',
137
+ fullyParallel: true,
138
+ forbidOnly: !!process.env.CI,
139
+ retries: process.env.CI ? 2 : 0,
140
+ reporter: process.env.CI ? 'github' : 'list',
141
+ use: {
142
+ baseURL: process.env.BASE_URL ?? 'http://localhost:3000',
143
+ trace: 'on-first-retry', // captures trace on flaky tests in CI
144
+ screenshot: 'only-on-failure',
145
+ },
146
+ projects: [
147
+ { name: 'chromium', use: { ...devices['Desktop Chrome'] } },
148
+ ],
149
+ });
150
+ indicators:
151
+ - "playwright.config.ts"
152
+ - "baseURL"
153
+ - "retries"
154
+ patterns:
155
+ data_flow:
156
+ direction: "globalSetup (login once) → storageState file → authedPage fixture → Test spec → POM methods → Browser → Application"
157
+ rules:
158
+ - "globalSetup logs in once and saves cookies/localStorage to .auth/user.json."
159
+ - "Auth fixture loads .auth/user.json — test specs get authenticated page with no login overhead."
160
+ - "Test specs call POM methods — never raw Playwright selectors."
161
+ - "baseURL in playwright.config.ts — page.goto('/path') works in all environments."
162
+ - "Retries and trace collection in CI — flaky tests get 2 retries before failing."
163
+ error_handling:
164
+ recommended: "Use trace: 'on-first-retry' to capture full browser trace on flaky tests. Use screenshot: 'only-on-failure' to get a visual snapshot when a test fails in CI."
165
+ naming:
166
+ specs: "tests/e2e/[feature].spec.ts"
167
+ page_objects: "tests/e2e/pages/[page].page.ts"
168
+ fixtures: "tests/e2e/fixtures/auth.fixture.ts"
169
+ global_setup: "tests/e2e/global-setup/auth.setup.ts"
170
+ storage_state: ".auth/user.json"
171
+ anti_patterns:
172
+ - id: raw_selectors_in_tests
173
+ severity: warning
174
+ description: "Using CSS selectors or XPath directly in test spec files instead of Page Object Model methods. When the UI changes (class rename, DOM restructure), every test that uses that selector breaks — with POMs, only one file needs updating."
175
+ bad_example: |
176
+ // ❌ Raw selectors in test spec — brittle, hard to maintain
177
+ test('user can log in', async ({ page }) => {
178
+ await page.goto('/login');
179
+ await page.locator('.login-form input[type="email"]').fill('user@test.com');
180
+ await page.locator('#submit-btn').click();
181
+ await expect(page.locator('.dashboard-header')).toBeVisible();
182
+ });
183
+ good_example: |
184
+ // ✓ POM method — one place to update when UI changes
185
+ test('user can log in', async ({ page }) => {
186
+ const loginPage = new LoginPage(page);
187
+ await loginPage.goto();
188
+ await loginPage.login('user@test.com', 'password');
189
+ await expect(page).toHaveURL('/dashboard');
190
+ });
191
+ - id: auth_in_every_test
192
+ severity: warning
193
+ description: "Performing the full login flow (goto /login, fill email, fill password, click submit) at the start of every authenticated test. A full login takes 1-3 seconds per test — 100 authenticated tests = 100-300 extra seconds. Use globalSetup + storageState to log in once."
194
+ bad_example: |
195
+ // ❌ Full login flow in every test — multiplies test execution time
196
+ test('view dashboard', async ({ page }) => {
197
+ await page.goto('/login');
198
+ await page.fill('#email', 'user@test.com');
199
+ await page.fill('#password', 'Password123');
200
+ await page.click('[type="submit"]');
201
+ await page.waitForURL('/dashboard');
202
+ // Now the actual test starts...
203
+ });
204
+ good_example: |
205
+ // ✓ Auth fixture provides pre-authenticated page — login ran once in globalSetup
206
+ test('view dashboard', async ({ authedPage }) => {
207
+ await authedPage.goto('/dashboard');
208
+ await expect(authedPage.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
209
+ });
210
+ - id: no_retries_in_ci
211
+ severity: warning
212
+ description: "Running Playwright in CI without retries — a single transient network condition, animation timing issue, or server cold start causes the entire suite to fail. Tests that pass locally may fail in CI due to slower execution."
213
+ bad_example: |
214
+ // playwright.config.ts — no CI configuration
215
+ export default defineConfig({
216
+ use: { baseURL: 'http://localhost:3000' },
217
+ // No retries, no CI reporter, no trace collection
218
+ });
219
+ good_example: |
220
+ export default defineConfig({
221
+ retries: process.env.CI ? 2 : 0,
222
+ reporter: process.env.CI ? 'github' : 'list',
223
+ use: {
224
+ baseURL: process.env.BASE_URL ?? 'http://localhost:3000',
225
+ trace: 'on-first-retry',
226
+ screenshot: 'only-on-failure',
227
+ },
228
+ });
229
+ - id: hardcoded_base_url_in_goto
230
+ severity: warning
231
+ description: "Using hardcoded localhost URLs in page.goto() calls instead of relative paths. Hardcoded URLs break when running tests against staging, CI, or any non-localhost environment. Configure baseURL once in playwright.config.ts."
232
+ bad_example: |
233
+ // ❌ Hardcoded URL — only works on localhost:3000
234
+ await page.goto('http://localhost:3000/dashboard');
235
+ await page.goto('http://localhost:3000/login');
236
+ good_example: |
237
+ // ✓ Relative path — Playwright prepends baseURL from playwright.config.ts
238
+ await page.goto('/dashboard');
239
+ await page.goto('/login');
240
+ // baseURL is configured per-environment: local=localhost:3000, CI=$BASE_URL
241
+ - id: missing_global_setup
242
+ severity: warning
243
+ description: "Re-logging in across every spec file instead of using globalSetup to create a storageState file once. For a test suite with 50 specs all needing authentication, this means 50 complete login flows instead of 1."
244
+ bad_example: |
245
+ // ❌ Each spec file logs in separately — N login flows for N specs
246
+ // auth.spec.ts: logs in at the top
247
+ // dashboard.spec.ts: logs in again
248
+ // settings.spec.ts: logs in again
249
+ // 50 spec files × 2 seconds login = 100 seconds wasted on login
250
+ good_example: |
251
+ // ✓ globalSetup logs in once, saves cookies to .auth/user.json
252
+ // tests/e2e/global-setup/auth.setup.ts
253
+ async function globalSetup() {
254
+ const { chromium } = require('@playwright/test');
255
+ const browser = await chromium.launch();
256
+ const page = await browser.newPage();
257
+ await page.goto('/login');
258
+ await page.getByLabel('Email').fill(process.env.TEST_USER_EMAIL!);
259
+ await page.getByLabel('Password').fill(process.env.TEST_USER_PASSWORD!);
260
+ await page.getByRole('button', { name: 'Sign in' }).click();
261
+ await page.waitForURL('**/dashboard');
262
+ await page.context().storageState({ path: '.auth/user.json' });
263
+ await browser.close();
264
+ }
265
+ export default globalSetup;