@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,276 @@
1
+ schema_version: "2.0.0"
2
+ id: selenium-e2e
3
+ name: "Selenium E2E"
4
+ version: "2.0.0"
5
+ description: "End-to-end testing with Selenium WebDriver — Page Object Models with BasePage, explicit waits (no sleeps), driver factory, screenshot capture on failure, data-testid selectors, and driver.quit() in afterEach."
6
+ category: pattern
7
+ language: javascript
8
+ frameworks:
9
+ - selenium-webdriver
10
+ dependencies:
11
+ none: []
12
+ detection:
13
+ dependencies:
14
+ any:
15
+ - selenium-webdriver
16
+ - "@selenium-devtools/expect-webdriver"
17
+ source_indicators:
18
+ - "from 'selenium-webdriver'"
19
+ - "new Builder()"
20
+ - "driver.findElement("
21
+ - "By.css("
22
+ - "until.elementLocated"
23
+ structure:
24
+ required_dirs:
25
+ - path: tests/e2e
26
+ purpose: "E2E test specs organized by user flow or feature. Each spec file tests one major user journey. Specs import Page Objects from tests/e2e/pages/ and the driver factory from tests/e2e/fixtures/ — they never contain raw driver.findElement() calls or By selectors."
27
+ recommended_dirs:
28
+ - path: tests/e2e/pages
29
+ purpose: "Page Object Model classes — one class per page, extending BasePage. BasePage provides shared explicit wait helpers so individual POMs don't copy-paste wait logic. Example: LoginPage extends BasePage, uses this.waitAndFind('login-form') from BasePage."
30
+ - path: tests/e2e/fixtures
31
+ purpose: "Driver factory (createDriver.ts) and shared setup helpers. createDriver() is the only place new Builder() is called — tests import and call createDriver() rather than building the WebDriver themselves."
32
+ separation:
33
+ rules:
34
+ - concern: page_object_models
35
+ belongs_in: tests/e2e/pages
36
+ rule_text: "All locators and user-action sequences go in Page Object Model classes. POM classes extend BasePage which provides shared explicit wait helpers. Tests call high-level POM methods (login(), submitContactForm()) — they never call driver.findElement() or By directly."
37
+ example: |
38
+ // tests/e2e/pages/base.page.ts
39
+ import { WebDriver, By, until, WebElement } from 'selenium-webdriver';
40
+
41
+ export class BasePage {
42
+ constructor(protected driver: WebDriver) {}
43
+
44
+ protected async waitAndFind(testId: string, timeout = 5000): Promise<WebElement> {
45
+ const locator = By.css(`[data-testid="${testId}"]`);
46
+ await this.driver.wait(until.elementLocated(locator), timeout);
47
+ return this.driver.findElement(locator);
48
+ }
49
+
50
+ protected async waitForUrl(urlFragment: string, timeout = 5000) {
51
+ await this.driver.wait(
52
+ async () => (await this.driver.getCurrentUrl()).includes(urlFragment),
53
+ timeout,
54
+ `URL did not contain "${urlFragment}" within ${timeout}ms`
55
+ );
56
+ }
57
+ }
58
+
59
+ // tests/e2e/pages/login.page.ts
60
+ import { By } from 'selenium-webdriver';
61
+ import { BasePage } from './base.page';
62
+
63
+ export class LoginPage extends BasePage {
64
+ async goto() {
65
+ await this.driver.get(`${process.env.BASE_URL ?? 'http://localhost:3000'}/login`);
66
+ }
67
+
68
+ async login(email: string, password: string) {
69
+ const emailInput = await this.waitAndFind('email-input');
70
+ const passwordInput = await this.waitAndFind('password-input');
71
+ await emailInput.sendKeys(email);
72
+ await passwordInput.sendKeys(password);
73
+ const submitBtn = await this.waitAndFind('login-submit');
74
+ await submitBtn.click();
75
+ await this.waitForUrl('/dashboard');
76
+ }
77
+
78
+ async getErrorMessage() {
79
+ const alert = await this.waitAndFind('error-alert');
80
+ return alert.getText();
81
+ }
82
+ }
83
+ indicators:
84
+ - "class LoginPage"
85
+ - "extends BasePage"
86
+ - "new LoginPage(driver)"
87
+ - concern: explicit_waits
88
+ belongs_in: tests/e2e/pages
89
+ rule_text: "Always use explicit waits (driver.wait with until.*) for every element interaction. Never use driver.sleep() or setTimeout() — they add fixed delay even when the element is ready, making tests slow and still flaky. Set a consistent timeout (5-10s for most elements, longer for page transitions)."
90
+ example: |
91
+ import { until, By, WebDriver } from 'selenium-webdriver';
92
+
93
+ // ✓ Explicit wait — resolves immediately when element appears, fails after timeout
94
+ const locator = By.css('[data-testid="result-list"]');
95
+ await driver.wait(until.elementLocated(locator), 8000, 'Result list not found within 8s');
96
+ const el = await driver.findElement(locator);
97
+ await driver.wait(until.elementIsVisible(el), 3000);
98
+
99
+ // ✓ Wait for element to become clickable
100
+ await driver.wait(until.elementIsEnabled(el), 3000);
101
+ await el.click();
102
+
103
+ // ❌ Fixed sleep — arbitrary, slow, still flaky:
104
+ // await driver.sleep(3000);
105
+ // await driver.findElement(locator); // may still fail if 3s wasn't enough
106
+ indicators:
107
+ - "driver.wait("
108
+ - "until.elementLocated"
109
+ - "until.elementIsVisible"
110
+ - concern: driver_factory
111
+ belongs_in: tests/e2e/fixtures
112
+ rule_text: "Create and configure the WebDriver instance in a single factory function exported from tests/e2e/fixtures/create-driver.ts. Test files import createDriver() — they never call `new Builder()`. This centralizes browser configuration (headless mode in CI, device emulation, etc.) in one place."
113
+ example: |
114
+ // tests/e2e/fixtures/create-driver.ts
115
+ import { Builder, WebDriver, Capabilities } from 'selenium-webdriver';
116
+ import chrome from 'selenium-webdriver/chrome';
117
+
118
+ export async function createDriver(): Promise<WebDriver> {
119
+ const options = new chrome.Options();
120
+ if (process.env.CI) {
121
+ options.addArguments('--headless', '--no-sandbox', '--disable-dev-shm-usage');
122
+ }
123
+ return new Builder()
124
+ .forBrowser('chrome')
125
+ .setChromeOptions(options)
126
+ .build();
127
+ }
128
+
129
+ // tests/e2e/auth.spec.ts
130
+ import { createDriver } from '../fixtures/create-driver';
131
+ let driver: WebDriver;
132
+ beforeEach(async () => { driver = await createDriver(); });
133
+ afterEach(async () => {
134
+ try { await driver.quit(); } catch { /* already closed */ }
135
+ });
136
+ indicators:
137
+ - "createDriver"
138
+ - "new Builder()"
139
+ - "forBrowser("
140
+ - concern: screenshot_on_failure
141
+ belongs_in: tests/e2e
142
+ rule_text: "Capture a screenshot and save it to disk in the afterEach (or afterAll) error handler. In CI, screenshots are the primary debugging tool — without them, a failing test in a headless browser is nearly impossible to diagnose."
143
+ example: |
144
+ // tests/e2e/helpers/screenshot.ts
145
+ import { WebDriver } from 'selenium-webdriver';
146
+ import fs from 'fs';
147
+ import path from 'path';
148
+
149
+ export async function screenshotOnFailure(driver: WebDriver, testName: string) {
150
+ const screenshot = await driver.takeScreenshot();
151
+ const dir = path.resolve('test-screenshots');
152
+ fs.mkdirSync(dir, { recursive: true });
153
+ const filename = `${testName.replace(/\s+/g, '-')}-${Date.now()}.png`;
154
+ fs.writeFileSync(path.join(dir, filename), screenshot, 'base64');
155
+ console.log(`Screenshot saved: ${path.join(dir, filename)}`);
156
+ }
157
+
158
+ // In test file:
159
+ afterEach(async function() {
160
+ // Mocha: this.currentTest.state === 'failed'
161
+ if (this.currentTest?.state === 'failed') {
162
+ await screenshotOnFailure(driver, this.currentTest.fullTitle());
163
+ }
164
+ await driver.quit();
165
+ });
166
+ indicators:
167
+ - "takeScreenshot"
168
+ - "writeFileSync"
169
+ - "test-screenshots"
170
+ patterns:
171
+ data_flow:
172
+ direction: "Test → POM Methods (explicit waits) → WebDriver → Browser → Application"
173
+ rules:
174
+ - "createDriver() in tests/e2e/fixtures/ is the only place new Builder() is called."
175
+ - "BasePage provides shared waitAndFind() and waitForUrl() — no copy-pasted wait logic in POMs."
176
+ - "Tests call POM methods — never raw driver.findElement() or By selectors."
177
+ - "driver.quit() runs in afterEach/finally — no orphaned browser processes."
178
+ - "Screenshots are saved on test failure for CI debugging."
179
+ error_handling:
180
+ recommended: "Wrap driver.quit() in try/catch in afterEach — if quit fails (already closed), the test runner still continues. Never let a failed driver.quit() block reporting of the actual test failure."
181
+ naming:
182
+ specs: "tests/e2e/[feature].spec.ts"
183
+ base_page: "tests/e2e/pages/base.page.ts — BasePage class with shared wait helpers"
184
+ page_objects: "tests/e2e/pages/[page].page.ts — extends BasePage"
185
+ factory: "tests/e2e/fixtures/create-driver.ts — createDriver() function"
186
+ screenshots: "test-screenshots/[test-name]-[timestamp].png"
187
+ anti_patterns:
188
+ - id: raw_selectors_in_tests
189
+ severity: warning
190
+ description: "Using driver.findElement() and By selectors directly in test spec files instead of Page Object Model methods. When a selector changes, every test that uses it breaks — with POMs, only one file needs updating."
191
+ bad_example: |
192
+ // ❌ Raw locators in test file — breaks when selector changes
193
+ it('can log in', async () => {
194
+ await driver.findElement(By.css('#email-field')).sendKeys('user@test.com');
195
+ await driver.findElement(By.css('#password-field')).sendKeys('password');
196
+ await driver.findElement(By.css('[type="submit"]')).click();
197
+ });
198
+ good_example: |
199
+ // ✓ POM method — one place to update when UI changes
200
+ it('can log in', async () => {
201
+ await loginPage.login('user@test.com', 'password');
202
+ });
203
+ - id: implicit_or_fixed_waits
204
+ severity: critical
205
+ description: "Using driver.manage().setImplicitWaitTimeout() or driver.sleep() instead of explicit waits. Implicit waits apply globally and interact badly with explicit waits. Fixed sleeps are arbitrary — too short causes flakiness, too long slows CI."
206
+ bad_example: |
207
+ // ❌ Global implicit wait — interacts badly with explicit waits
208
+ await driver.manage().setImplicitWaitTimeout(5000);
209
+
210
+ // ❌ Fixed sleep — arbitrary, slow on fast machines, still flaky on slow ones
211
+ await driver.sleep(3000);
212
+ const el = await driver.findElement(By.css('#result'));
213
+ good_example: |
214
+ // ✓ Explicit wait — resolves as soon as element appears, up to 5s
215
+ const el = await driver.wait(
216
+ until.elementLocated(By.css('[data-testid="result"]')),
217
+ 5000,
218
+ 'Result element not found within 5s'
219
+ );
220
+ - id: driver_not_quit
221
+ severity: critical
222
+ description: "Not calling driver.quit() after each test leaves orphaned Chrome/Firefox processes. Each test creates a new browser process — 20 tests without cleanup = 20 browser processes competing for memory. In CI, this causes OOM kills."
223
+ bad_example: |
224
+ // ❌ No cleanup — browser process leaked after every test
225
+ afterEach(async () => {
226
+ // Missing: await driver.quit();
227
+ });
228
+ good_example: |
229
+ // ✓ quit() in try/catch so cleanup always runs
230
+ afterEach(async () => {
231
+ try {
232
+ await driver.quit();
233
+ } catch {
234
+ // driver may already be closed if test crashed it
235
+ }
236
+ });
237
+ - id: new_builder_in_tests
238
+ severity: warning
239
+ description: "Calling `new Builder()` in individual test files instead of importing from the shared driver factory. Browser configuration (headless mode, window size, proxy) is duplicated and must be updated in every test file when it changes."
240
+ bad_example: |
241
+ // ❌ Builder configuration duplicated in every test file
242
+ beforeEach(async () => {
243
+ driver = await new Builder().forBrowser('chrome').build(); // headless? window size? proxy?
244
+ });
245
+ good_example: |
246
+ // ✓ Import from shared factory — one place to configure all browser options
247
+ import { createDriver } from '../fixtures/create-driver';
248
+ beforeEach(async () => { driver = await createDriver(); });
249
+ - id: xpath_selectors
250
+ severity: warning
251
+ description: "Using XPath selectors (By.xpath()) for element selection. XPath selectors are tightly coupled to the DOM tree structure — adding a wrapper <div>, moving elements, or renaming tags breaks the selector. They are also slow compared to CSS selectors."
252
+ bad_example: |
253
+ // ❌ XPath — breaks on any DOM structure change
254
+ await driver.findElement(By.xpath('//div[@class="login-form"]/form/div[2]/input')).sendKeys(email);
255
+ await driver.findElement(By.xpath('//button[contains(text(),"Submit")]')).click();
256
+ good_example: |
257
+ // ✓ data-testid selector — stable regardless of DOM structure
258
+ await driver.findElement(By.css('[data-testid="email-input"]')).sendKeys(email);
259
+ await driver.findElement(By.css('[data-testid="login-submit"]')).click();
260
+ - id: no_screenshot_on_failure
261
+ severity: warning
262
+ description: "Not capturing screenshots on test failure. A headless browser test that fails in CI with no screenshot leaves only an error message — developers cannot see what the page looked like when the test failed, making debugging extremely difficult."
263
+ bad_example: |
264
+ // ❌ No screenshot — only error message on failure
265
+ afterEach(async () => {
266
+ await driver.quit(); // no screenshot before quitting
267
+ });
268
+ good_example: |
269
+ // ✓ Screenshot saved before quit when test fails
270
+ afterEach(async function() {
271
+ if (this.currentTest?.state === 'failed') {
272
+ const screenshot = await driver.takeScreenshot();
273
+ fs.writeFileSync(`test-screenshots/${Date.now()}.png`, screenshot, 'base64');
274
+ }
275
+ await driver.quit();
276
+ });
@@ -0,0 +1,298 @@
1
+ schema_version: "2.0.0"
2
+ id: supabase-auth
3
+ name: "Supabase Auth"
4
+ version: "2.0.0"
5
+ description: "Supabase built-in authentication with SSR session management via @supabase/ssr, RLS row-level security policies tied to auth.uid(), OAuth callback routes, and middleware session refresh."
6
+ category: pattern
7
+ language: javascript
8
+ frameworks:
9
+ - supabase
10
+ dependencies:
11
+ none:
12
+ - "@clerk/nextjs"
13
+ - next-auth
14
+ - lucia
15
+ detection:
16
+ dependencies:
17
+ any:
18
+ - "@supabase/ssr"
19
+ - "@supabase/supabase-js"
20
+ source_indicators:
21
+ - "supabase.auth.signIn"
22
+ - "supabase.auth.signUp"
23
+ - "auth.uid()"
24
+ - "exchangeCodeForSession"
25
+ - "createServerClient"
26
+ - "supabase.auth.getUser"
27
+ structure:
28
+ required_dirs:
29
+ - path: app/auth
30
+ purpose: "Supabase auth UI routes and OAuth callback handler. The callback route at app/auth/callback/route.ts is mandatory for all OAuth providers — it exchanges the one-time code from Supabase's redirect URL for a session cookie. Without it, OAuth sign-in always fails."
31
+ recommended_dirs:
32
+ - path: src/lib
33
+ purpose: "Supabase client factory functions — one file for server contexts (createClient reading from Next.js cookies) and one for browser contexts (createBrowserClient). These files are imported by Server Components, API routes, middleware, and Client Components respectively. Never mix them."
34
+ - path: middleware.ts
35
+ purpose: "Session refresh middleware — calls supabase.auth.getUser() on every request to refresh the session cookie before it expires. Without this, users are silently logged out when their session cookie ages past the expiry window even if they are actively using the app."
36
+ separation:
37
+ rules:
38
+ - concern: ssr_sessions
39
+ belongs_in: src/lib
40
+ rule_text: "Use @supabase/ssr (not @supabase/supabase-js directly) for Next.js and server-rendered apps. Create a server client in src/lib/supabase-server.ts that reads and writes session cookies using Next.js cookie utilities. Create a browser client in src/lib/supabase-browser.ts for Client Components."
41
+ example: |
42
+ // src/lib/supabase-server.ts — for Server Components, API routes, middleware
43
+ import { createServerClient } from '@supabase/ssr';
44
+ import { cookies } from 'next/headers';
45
+
46
+ export async function createClient() {
47
+ const cookieStore = await cookies();
48
+ return createServerClient(
49
+ process.env.NEXT_PUBLIC_SUPABASE_URL!,
50
+ process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
51
+ {
52
+ cookies: {
53
+ getAll: () => cookieStore.getAll(),
54
+ setAll: (cookiesToSet) => {
55
+ cookiesToSet.forEach(({ name, value, options }) =>
56
+ cookieStore.set(name, value, options)
57
+ );
58
+ },
59
+ },
60
+ }
61
+ );
62
+ }
63
+
64
+ // src/lib/supabase-browser.ts — for Client Components only
65
+ import { createBrowserClient } from '@supabase/ssr';
66
+ export const supabase = createBrowserClient(
67
+ process.env.NEXT_PUBLIC_SUPABASE_URL!,
68
+ process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
69
+ );
70
+ indicators:
71
+ - "createServerClient"
72
+ - "@supabase/ssr"
73
+ - "createBrowserClient"
74
+ - "exchangeCodeForSession"
75
+ - concern: middleware_refresh
76
+ belongs_in: middleware.ts
77
+ rule_text: "Call supabase.auth.getUser() in middleware on every request to refresh the session cookie. This keeps short-lived access tokens alive as long as the user is active. Without middleware refresh, sessions expire mid-visit even when the user is actively interacting with the app."
78
+ example: |
79
+ // middleware.ts
80
+ import { createServerClient } from '@supabase/ssr';
81
+ import { NextResponse, type NextRequest } from 'next/server';
82
+
83
+ export async function middleware(request: NextRequest) {
84
+ let supabaseResponse = NextResponse.next({ request });
85
+
86
+ const supabase = createServerClient(
87
+ process.env.NEXT_PUBLIC_SUPABASE_URL!,
88
+ process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
89
+ {
90
+ cookies: {
91
+ getAll: () => request.cookies.getAll(),
92
+ setAll: (cookiesToSet) => {
93
+ cookiesToSet.forEach(({ name, value, options }) => {
94
+ request.cookies.set(name, value);
95
+ supabaseResponse.cookies.set(name, value, options);
96
+ });
97
+ },
98
+ },
99
+ }
100
+ );
101
+
102
+ // Refreshes the session if expired — MUST NOT be removed
103
+ const { data: { user } } = await supabase.auth.getUser();
104
+
105
+ if (!user && !request.nextUrl.pathname.startsWith('/login')) {
106
+ const url = request.nextUrl.clone();
107
+ url.pathname = '/login';
108
+ return NextResponse.redirect(url);
109
+ }
110
+
111
+ return supabaseResponse;
112
+ }
113
+ indicators:
114
+ - "supabase.auth.getUser()"
115
+ - "createServerClient"
116
+ - "supabaseResponse"
117
+ - concern: rls_integration
118
+ belongs_in: src/lib
119
+ rule_text: "Every table that stores user data must have Row Level Security enabled and at least one policy using auth.uid(). Supabase Auth's primary security mechanism is RLS — authenticated queries still see all rows if no policy restricts them. Write policies before writing application code."
120
+ example: |
121
+ -- Enable RLS on every user-data table (run in Supabase SQL editor)
122
+ ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
123
+ ALTER TABLE comments ENABLE ROW LEVEL SECURITY;
124
+ ALTER TABLE profiles ENABLE ROW LEVEL SECURITY;
125
+
126
+ -- Policy: users can only read, create, update, and delete their own rows
127
+ CREATE POLICY "own posts" ON posts
128
+ FOR ALL
129
+ USING (auth.uid() = user_id)
130
+ WITH CHECK (auth.uid() = user_id);
131
+
132
+ -- Policy: profiles are readable by everyone but only editable by owner
133
+ CREATE POLICY "public profiles read" ON profiles
134
+ FOR SELECT USING (true);
135
+ CREATE POLICY "own profile write" ON profiles
136
+ FOR ALL USING (auth.uid() = id);
137
+ indicators:
138
+ - "auth.uid()"
139
+ - "USING ("
140
+ - "CREATE POLICY"
141
+ - "ROW LEVEL SECURITY"
142
+ - concern: social_providers
143
+ belongs_in: app/auth
144
+ rule_text: "Configure OAuth providers in the Supabase dashboard (Authentication > Providers) — not in code. Create a mandatory callback route at app/auth/callback/route.ts that calls supabase.auth.exchangeCodeForSession() to convert the one-time OAuth code into a session cookie."
145
+ example: |
146
+ // app/auth/callback/route.ts — mandatory for all OAuth providers
147
+ import { createServerClient } from '@supabase/ssr';
148
+ import { cookies } from 'next/headers';
149
+ import { NextResponse } from 'next/server';
150
+
151
+ export async function GET(request: Request) {
152
+ const { searchParams, origin } = new URL(request.url);
153
+ const code = searchParams.get('code');
154
+ const next = searchParams.get('next') ?? '/';
155
+
156
+ if (code) {
157
+ const cookieStore = await cookies();
158
+ const supabase = createServerClient(
159
+ process.env.NEXT_PUBLIC_SUPABASE_URL!,
160
+ process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
161
+ { cookies: { getAll: () => cookieStore.getAll(), setAll: (cs) => cs.forEach(({ name, value, options }) => cookieStore.set(name, value, options)) } }
162
+ );
163
+ const { error } = await supabase.auth.exchangeCodeForSession(code);
164
+ if (!error) {
165
+ return NextResponse.redirect(`${origin}${next}`);
166
+ }
167
+ }
168
+
169
+ return NextResponse.redirect(`${origin}/auth/auth-code-error`);
170
+ }
171
+ indicators:
172
+ - "exchangeCodeForSession"
173
+ - "app/auth/callback"
174
+ - "searchParams.get('code')"
175
+ - concern: server_component_auth
176
+ belongs_in: app
177
+ rule_text: "In Server Components, call supabase.auth.getUser() (not getSession()) to get the authenticated user. getUser() validates the token with Supabase's server — getSession() only reads the local cookie which can be forged. Always use getUser() for security-sensitive decisions."
178
+ example: |
179
+ // app/dashboard/page.tsx — Server Component with auth check
180
+ import { createClient } from '@/lib/supabase-server';
181
+ import { redirect } from 'next/navigation';
182
+
183
+ export default async function DashboardPage() {
184
+ const supabase = await createClient();
185
+ // getUser() validates with Supabase server — getSession() does NOT
186
+ const { data: { user }, error } = await supabase.auth.getUser();
187
+ if (error || !user) redirect('/login');
188
+
189
+ const { data: posts } = await supabase
190
+ .from('posts')
191
+ .select('*')
192
+ .eq('user_id', user.id); // RLS also enforces this at DB level
193
+
194
+ return <PostList posts={posts ?? []} />;
195
+ }
196
+ indicators:
197
+ - "supabase.auth.getUser()"
198
+ - "from('@/lib/supabase-server')"
199
+ patterns:
200
+ data_flow:
201
+ direction: "Request → Middleware (session refresh) → Server Component (getUser) → RLS-enforced Supabase Query → Database"
202
+ rules:
203
+ - "Middleware refreshes the session cookie on every request — without it users get silently logged out mid-session."
204
+ - "Server Components call supabase.auth.getUser() for the authenticated user — never getSession() for security decisions."
205
+ - "Client Components use the browser client from supabase-browser.ts — it reads the session from localStorage."
206
+ - "RLS policies enforce per-user data isolation at the database level — authenticated queries still need policies to restrict rows."
207
+ - "The OAuth callback route at app/auth/callback/route.ts is the mandatory landing point for all social provider sign-ins."
208
+ - "Service role key is only used server-side for admin operations that intentionally bypass RLS — never in browser code."
209
+ error_handling:
210
+ recommended: "Always call supabase.auth.getUser() (not getSession()) for auth decisions — getUser() validates with Supabase's server. Check both error and data.user being null. Redirect to /login when no valid session."
211
+ naming:
212
+ callback_route: "app/auth/callback/route.ts — OAuth code exchange via exchangeCodeForSession()"
213
+ server_client: "src/lib/supabase-server.ts — SSR client reading session from cookies (@supabase/ssr)"
214
+ browser_client: "src/lib/supabase-browser.ts — browser client (createBrowserClient from @supabase/ssr)"
215
+ rls_policies: "Named 'own [resource]' — e.g. 'own posts', 'own profiles'; reference auth.uid() = user_id"
216
+ anti_patterns:
217
+ - id: no_rls_with_auth
218
+ severity: critical
219
+ description: "Using Supabase Auth to authenticate users but not creating RLS policies. All authenticated users can read and modify each other's data — enabling auth without RLS does not provide any data isolation. This is the most common Supabase security mistake."
220
+ bad_example: |
221
+ -- ❌ RLS enabled but no policies — Supabase defaults to DENY for anon,
222
+ -- but ALL authenticated users can read ALL rows from each other
223
+ ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
224
+ -- Missing: CREATE POLICY
225
+ -- Any signed-in user: SELECT * FROM posts → sees every user's posts
226
+ good_example: |
227
+ -- ✓ RLS with per-user policy — each user only sees their own rows
228
+ ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
229
+ CREATE POLICY "own posts" ON posts
230
+ FOR ALL
231
+ USING (auth.uid() = user_id)
232
+ WITH CHECK (auth.uid() = user_id);
233
+ - id: get_session_for_auth_decisions
234
+ severity: critical
235
+ description: "Using supabase.auth.getSession() to verify authentication instead of supabase.auth.getUser(). getSession() reads the local session cookie without validating it with Supabase's server — a forged or replayed cookie will pass getSession() but fail getUser()."
236
+ bad_example: |
237
+ // ❌ getSession() only reads the cookie — can be forged
238
+ const { data: { session } } = await supabase.auth.getSession();
239
+ if (!session) redirect('/login');
240
+ // session.user is from the unvalidated cookie — not trustworthy
241
+ const userId = session.user.id;
242
+ good_example: |
243
+ // ✓ getUser() validates with Supabase's auth server — cannot be forged
244
+ const { data: { user }, error } = await supabase.auth.getUser();
245
+ if (error || !user) redirect('/login');
246
+ const userId = user.id; // validated server-side
247
+ - id: direct_supabase_js_on_server
248
+ severity: warning
249
+ description: "Using @supabase/supabase-js createClient() in Server Components instead of @supabase/ssr createServerClient(). The plain createClient() has no way to read Next.js cookie storage — it cannot find the user session and all authenticated queries run as anonymous."
250
+ bad_example: |
251
+ // ❌ Plain supabase-js on server — can't read session cookies
252
+ import { createClient } from '@supabase/supabase-js';
253
+ const supabase = createClient(URL, ANON_KEY);
254
+ // supabase.auth.getUser() always returns null — session is invisible
255
+ const { data: { user } } = await supabase.auth.getUser(); // user = null
256
+ good_example: |
257
+ // ✓ SSR client reads session from Next.js cookie store
258
+ import { createClient } from '@/lib/supabase-server';
259
+ const supabase = await createClient();
260
+ const { data: { user } } = await supabase.auth.getUser(); // user = authenticated user
261
+ - id: missing_callback_route
262
+ severity: warning
263
+ description: "Using OAuth social login (GitHub, Google, etc.) without an auth callback route. When Supabase redirects back after OAuth, there is nowhere to exchange the one-time code for a session — the user lands on the redirect URL with a 404 or a broken page."
264
+ bad_example: |
265
+ // ❌ No callback route — sign-in with OAuth always fails with a broken redirect
266
+ await supabase.auth.signInWithOAuth({ provider: 'github' });
267
+ // Supabase redirects to: /auth/callback?code=... → 404 Not Found
268
+ good_example: |
269
+ // ✓ app/auth/callback/route.ts exists and calls exchangeCodeForSession()
270
+ const { error } = await supabase.auth.exchangeCodeForSession(code);
271
+ if (!error) return NextResponse.redirect(`${origin}/dashboard`);
272
+ - id: missing_middleware_refresh
273
+ severity: warning
274
+ description: "Not running session refresh in middleware. Supabase access tokens expire after 1 hour by default. Without middleware calling getUser() on each request, the session cookie ages and the user is silently logged out even while actively using the app."
275
+ bad_example: |
276
+ // middleware.ts — missing or empty, no session refresh
277
+ export function middleware(req: NextRequest) {
278
+ return NextResponse.next(); // session cookie never refreshed
279
+ }
280
+ // User gets logged out every hour even while actively using the app
281
+ good_example: |
282
+ // ✓ Middleware calls supabase.auth.getUser() which refreshes the access token
283
+ const supabase = createServerClient(URL, KEY, { cookies: { ... } });
284
+ await supabase.auth.getUser(); // side effect: refreshes session if near expiry
285
+ - id: insecure_rls_policy
286
+ severity: critical
287
+ description: "Writing an RLS policy with USING (true) which grants full access to all authenticated users — equivalent to having no RLS at all. Often introduced by accident when following generic Supabase examples."
288
+ bad_example: |
289
+ -- ❌ USING (true) grants every authenticated user full table access
290
+ CREATE POLICY "allow all" ON posts
291
+ FOR ALL USING (true);
292
+ -- Any logged-in user can read, update, or delete any other user's posts
293
+ good_example: |
294
+ -- ✓ Scope the policy to the authenticated user's own rows
295
+ CREATE POLICY "own posts" ON posts
296
+ FOR ALL
297
+ USING (auth.uid() = user_id)
298
+ WITH CHECK (auth.uid() = user_id);