nextjs-hackathon-stack 0.1.31 → 0.1.33

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 (28) hide show
  1. package/dist/index.js +157 -10
  2. package/package.json +1 -1
  3. package/template/.cursor/agents/backend.md +18 -0
  4. package/template/.cursor/agents/business-intelligence.md +22 -1
  5. package/template/.cursor/agents/code-reviewer.md +3 -0
  6. package/template/.cursor/agents/frontend.md +18 -0
  7. package/template/.cursor/agents/technical-lead.md +8 -4
  8. package/template/.cursor/agents/test-qa.md +37 -1
  9. package/template/.cursor/mcp.json +8 -0
  10. package/template/.cursor/memory/architecture-snapshot.md +50 -0
  11. package/template/.cursor/rules/architecture.mdc +3 -2
  12. package/template/.cursor/rules/coding-standards.mdc +43 -13
  13. package/template/.cursor/rules/components.mdc +7 -0
  14. package/template/.cursor/rules/general.mdc +3 -2
  15. package/template/.cursor/rules/migrations.mdc +2 -19
  16. package/template/.cursor/rules/security.mdc +4 -1
  17. package/template/.cursor/rules/supabase.mdc +14 -2
  18. package/template/.cursor/rules/testing.mdc +8 -0
  19. package/template/.cursor/skills/create-feature/SKILL.md +94 -14
  20. package/template/.cursor/skills/create-feature/references/server-action-test-template.md +92 -40
  21. package/template/CLAUDE.md +27 -1
  22. package/template/next.config.ts +4 -1
  23. package/template/src/app/(protected)/page.tsx +1 -1
  24. package/template/src/features/todos/components/add-todo-form.tsx +1 -1
  25. package/template/src/features/todos/components/todo-list.tsx +4 -4
  26. package/template/src/shared/components/providers.tsx +10 -1
  27. package/template/src/shared/test-utils/supabase-mock.ts +59 -0
  28. package/template/tailwind.css +20 -14
package/dist/index.js CHANGED
@@ -43,13 +43,25 @@ async function runCli(argProjectName, skipInstall) {
43
43
  }
44
44
  projectName = result;
45
45
  }
46
- return { projectName, skipInstall };
46
+ const templateResult = await p2.select({
47
+ message: "What would you like to start with?",
48
+ options: [
49
+ { value: "example", label: "Todo example app", hint: "Full working app with todos CRUD" },
50
+ { value: "empty", label: "Empty app", hint: "Auth + empty dashboard" }
51
+ ]
52
+ });
53
+ if (p2.isCancel(templateResult)) {
54
+ p2.cancel("Cancelled");
55
+ process.exit(0);
56
+ }
57
+ const template = templateResult;
58
+ return { projectName, skipInstall, template };
47
59
  }
48
60
 
49
61
  // src/scaffold.ts
50
62
  import { execSync } from "child_process";
51
- import { copyFileSync, cpSync, existsSync, lstatSync, mkdirSync, readFileSync, readlinkSync, readdirSync, symlinkSync, writeFileSync } from "fs";
52
- import { join, dirname } from "path";
63
+ import { copyFileSync, cpSync, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, readlinkSync, symlinkSync, writeFileSync } from "fs";
64
+ import { join, dirname, resolve } from "path";
53
65
  import { fileURLToPath } from "url";
54
66
  import * as p3 from "@clack/prompts";
55
67
  import pc2 from "picocolors";
@@ -60,7 +72,7 @@ function getTemplateDir() {
60
72
  function processTemplate(content, vars) {
61
73
  return content.replace(/\{\{(\w+)\}\}/g, (_, key) => vars[key] ?? `{{${key}}}`);
62
74
  }
63
- function copyDir(src, dest, vars) {
75
+ function copyDir(src, dest, vars, skip = /* @__PURE__ */ new Set(), templateRoot = src) {
64
76
  mkdirSync(dest, { recursive: true });
65
77
  for (const entry of readdirSync(src)) {
66
78
  const srcPath = join(src, entry);
@@ -70,11 +82,18 @@ function copyDir(src, dest, vars) {
70
82
  }
71
83
  const destPath = join(dest, destEntry);
72
84
  const stat = lstatSync(srcPath);
85
+ const relPath = srcPath.slice(templateRoot.length + 1).replace(/\\/g, "/");
86
+ if (skip.has(relPath)) continue;
73
87
  if (stat.isSymbolicLink()) {
74
88
  const target = readlinkSync(srcPath);
75
- symlinkSync(target, destPath);
89
+ const resolvedTarget = resolve(dirname(srcPath), target);
90
+ if (existsSync(resolvedTarget) && lstatSync(resolvedTarget).isFile()) {
91
+ cpSync(resolvedTarget, destPath);
92
+ } else {
93
+ symlinkSync(target, destPath);
94
+ }
76
95
  } else if (stat.isDirectory()) {
77
- copyDir(srcPath, destPath, vars);
96
+ copyDir(srcPath, destPath, vars, skip, templateRoot);
78
97
  } else if (entry.endsWith(".tmpl")) {
79
98
  const content = readFileSync(srcPath, "utf-8");
80
99
  writeFileSync(destPath, processTemplate(content, vars));
@@ -83,7 +102,97 @@ function copyDir(src, dest, vars) {
83
102
  }
84
103
  }
85
104
  }
86
- async function scaffold(projectName, skipInstall) {
105
+ var EMPTY_APP_PAGE = `import { redirect } from "next/navigation";
106
+ import { logoutAction } from "@/features/auth/actions/logout.action";
107
+ import { createClient } from "@/shared/lib/supabase/server";
108
+
109
+ export default async function HomePage() {
110
+ const supabase = await createClient();
111
+ const { data: { user } } = await supabase.auth.getUser();
112
+ if (!user) redirect("/login");
113
+
114
+ return (
115
+ <main className="container mx-auto max-w-2xl p-8">
116
+ <div className="mb-6 flex items-center justify-between">
117
+ <h1 className="text-2xl font-bold">Dashboard</h1>
118
+ <form action={logoutAction}>
119
+ <button type="submit" className="rounded bg-muted px-4 py-2 text-sm hover:bg-muted-foreground/10">
120
+ Cerrar sesi\xF3n
121
+ </button>
122
+ </form>
123
+ </div>
124
+ <p className="text-muted-foreground">\xA1Bienvenido! Empieza a construir tu aplicaci\xF3n.</p>
125
+ </main>
126
+ );
127
+ }
128
+ `;
129
+ var EMPTY_APP_SCHEMA = `import { pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core";
130
+ import { createInsertSchema, createSelectSchema } from "drizzle-zod";
131
+
132
+ export const profiles = pgTable("profiles", {
133
+ id: uuid("id").primaryKey(),
134
+ email: text("email").notNull().unique(),
135
+ createdAt: timestamp("created_at").notNull().defaultNow(),
136
+ updatedAt: timestamp("updated_at").notNull().defaultNow(),
137
+ });
138
+
139
+ export const insertProfileSchema = createInsertSchema(profiles);
140
+ export const selectProfileSchema = createSelectSchema(profiles);
141
+
142
+ export type InsertProfile = typeof profiles.$inferInsert;
143
+ export type SelectProfile = typeof profiles.$inferSelect;
144
+ `;
145
+ var EMPTY_APP_PAGE_TEST = `import { describe, it, expect, vi, beforeEach } from "vitest";
146
+
147
+ const mockGetUser = vi.fn();
148
+ const mockRedirect = vi.fn();
149
+
150
+ vi.mock("@/shared/lib/supabase/server", () => ({
151
+ createClient: vi.fn(() =>
152
+ Promise.resolve({ auth: { getUser: mockGetUser } })
153
+ ),
154
+ }));
155
+
156
+ vi.mock("next/navigation", () => ({
157
+ redirect: (url: string): unknown => mockRedirect(url),
158
+ }));
159
+
160
+ describe("HomePage (protected)", () => {
161
+ beforeEach(() => {
162
+ vi.clearAllMocks();
163
+ mockRedirect.mockImplementation(() => { throw new Error("NEXT_REDIRECT"); });
164
+ });
165
+
166
+ it("redirects to /login when user is not authenticated", async () => {
167
+ // Arrange
168
+ mockGetUser.mockResolvedValue({ data: { user: null } });
169
+ const { default: HomePage } = await import("../(protected)/page");
170
+
171
+ // Act + Assert
172
+ await expect(HomePage()).rejects.toThrow("NEXT_REDIRECT");
173
+ expect(mockRedirect).toHaveBeenCalledWith("/login");
174
+ });
175
+
176
+ it("renders dashboard when user is authenticated", async () => {
177
+ // Arrange
178
+ mockGetUser.mockResolvedValue({ data: { user: { id: "u1", email: "user@example.com" } } });
179
+ const { default: HomePage } = await import("../(protected)/page");
180
+
181
+ // Act
182
+ const result = await HomePage();
183
+
184
+ // Assert
185
+ expect(result).toBeTruthy();
186
+ });
187
+ });
188
+ `;
189
+ function buildSkipSet() {
190
+ return /* @__PURE__ */ new Set([
191
+ "src/features/todos",
192
+ "src/e2e/todos.spec.ts"
193
+ ]);
194
+ }
195
+ async function scaffold(projectName, skipInstall, skipMcp = false, template = "example") {
87
196
  const targetDir = join(process.cwd(), projectName);
88
197
  if (existsSync(targetDir)) {
89
198
  p3.cancel(`Directory "${projectName}" already exists`);
@@ -91,9 +200,19 @@ async function scaffold(projectName, skipInstall) {
91
200
  }
92
201
  const templateDir = getTemplateDir();
93
202
  const vars = { projectName };
203
+ const skip = template === "empty" ? buildSkipSet() : /* @__PURE__ */ new Set();
94
204
  const spinner2 = p3.spinner();
95
205
  spinner2.start("Copying template files");
96
- copyDir(templateDir, targetDir, vars);
206
+ copyDir(templateDir, targetDir, vars, skip, templateDir);
207
+ if (template === "empty") {
208
+ const pageDir = join(targetDir, "src/app/(protected)");
209
+ mkdirSync(pageDir, { recursive: true });
210
+ writeFileSync(join(pageDir, "page.tsx"), EMPTY_APP_PAGE);
211
+ writeFileSync(join(targetDir, "src/shared/db/schema.ts"), EMPTY_APP_SCHEMA);
212
+ const testDir = join(targetDir, "src/app/__tests__");
213
+ mkdirSync(testDir, { recursive: true });
214
+ writeFileSync(join(testDir, "protected-page.test.tsx"), EMPTY_APP_PAGE_TEST);
215
+ }
97
216
  spinner2.stop("Template files copied");
98
217
  const envExamplePath = join(targetDir, ".env.example");
99
218
  const envLocalPath = join(targetDir, ".env.local");
@@ -154,6 +273,34 @@ async function scaffold(projectName, skipInstall) {
154
273
  } catch {
155
274
  spinner2.stop("Husky setup failed \u2014 run manually: pnpm exec husky");
156
275
  }
276
+ if (!skipMcp) {
277
+ spinner2.start("Installing mcp-memory-service");
278
+ try {
279
+ const pipCmd = (() => {
280
+ try {
281
+ execSync("pip3 --version", { stdio: "pipe" });
282
+ return "pip3";
283
+ } catch {
284
+ try {
285
+ execSync("pip --version", { stdio: "pipe" });
286
+ return "pip";
287
+ } catch {
288
+ return null;
289
+ }
290
+ }
291
+ })();
292
+ if (pipCmd) {
293
+ execSync(`${pipCmd} install mcp-memory-service`, { cwd: targetDir, stdio: "pipe" });
294
+ spinner2.stop("mcp-memory-service installed");
295
+ } else {
296
+ spinner2.stop("pip not found \u2014 skipping mcp-memory-service");
297
+ p3.log.warn("Install Python and run: pip install mcp-memory-service");
298
+ }
299
+ } catch {
300
+ spinner2.stop("mcp-memory-service installation failed");
301
+ p3.log.warn("Install manually: pip install mcp-memory-service");
302
+ }
303
+ }
157
304
  success(pc2.bold(`\xA1Proyecto "${projectName}" creado!`));
158
305
  console.log(`
159
306
  ${pc2.dim("Siguientes pasos:")}`);
@@ -170,10 +317,10 @@ async function scaffold(projectName, skipInstall) {
170
317
 
171
318
  // src/index.ts
172
319
  var program = new Command();
173
- program.name("nextjs-hackathon-stack").description("Scaffold a full-stack Next.js 15 hackathon starter").version("0.1.0").argument("[project-name]", "Name of the project to create").option("--skip-install", "Skip pnpm install and shadcn/ui init", false).action(async (projectName, opts) => {
320
+ program.name("nextjs-hackathon-stack").description("Scaffold a full-stack Next.js 15 hackathon starter").version("0.1.0").argument("[project-name]", "Name of the project to create").option("--skip-install", "Skip pnpm install and shadcn/ui init", false).option("--skip-mcp", "Skip mcp-memory-service installation", false).action(async (projectName, opts) => {
174
321
  try {
175
322
  const options = await runCli(projectName, opts.skipInstall);
176
- await scaffold(options.projectName, options.skipInstall);
323
+ await scaffold(options.projectName, options.skipInstall, opts.skipMcp, options.template);
177
324
  outro2("Happy hacking! \u{1F680}");
178
325
  } catch (err) {
179
326
  p4.cancel(err instanceof Error ? err.message : "Something went wrong");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nextjs-hackathon-stack",
3
- "version": "0.1.31",
3
+ "version": "0.1.33",
4
4
  "description": "Scaffold a full-stack Next.js hackathon starter",
5
5
  "type": "module",
6
6
  "bin": {
@@ -7,6 +7,10 @@ readonly: false
7
7
 
8
8
  # Backend Agent
9
9
 
10
+ ## First Step
11
+
12
+ Read `.cursor/memory/architecture-snapshot.md` to understand the existing schema, features, and proven Server Action patterns before writing new code. Use the canonical pattern references to copy the correct structure.
13
+
10
14
  ## Responsibilities
11
15
  - Build Next.js Server Actions, API routes, and middleware
12
16
  - Define Drizzle ORM schemas and manage migrations via CLI
@@ -23,6 +27,20 @@ readonly: false
23
27
  - Default to **Node.js runtime** for Server Actions and API routes
24
28
  - Use **Edge runtime** only when latency is critical and no Node.js APIs are needed
25
29
 
30
+ ## RLS Policy Requirement
31
+
32
+ RLS is not optional and is never a post-implementation step. When you create a new table:
33
+
34
+ 1. Enable RLS on the table in Supabase
35
+ 2. Write the RLS policies before the feature is considered done. Default template:
36
+ ```sql
37
+ -- Users can only access their own rows
38
+ CREATE POLICY "users_own_rows" ON <table>
39
+ FOR ALL USING (user_id = auth.uid());
40
+ ```
41
+ 3. Document the policy in the architecture snapshot under the table entry
42
+ 4. Never leave "add RLS later" as a TODO — a table without RLS is a security vulnerability
43
+
26
44
  ## Dev Server
27
45
  - Before running `next dev`, check `.next/dev/lock`. If it exists and the PID is alive, an instance is already running — use that server's URL instead of starting a new one.
28
46
  - Enable `logging.browserToTerminal` in `next.config.ts` for terminal-based debugging (recommended: `'error'` for production-like sessions, `true` during active development).
@@ -7,6 +7,10 @@ readonly: true
7
7
 
8
8
  # Business Intelligence Agent
9
9
 
10
+ ## First Step
11
+
12
+ Read `.cursor/memory/architecture-snapshot.md` to understand existing features before asking discovery questions. Use it to identify relationships with existing data and patterns.
13
+
10
14
  ## Responsibilities
11
15
  - Discover requirements through user questions before defining anything
12
16
  - Write functional issues describing user-visible behavior
@@ -20,10 +24,21 @@ readonly: true
20
24
 
21
25
  Never assume requirements. Always ask first.
22
26
 
27
+ Complete ALL of these before writing a spec:
28
+
23
29
  1. **Problem & audience** — "What problem does this solve? Who experiences it?"
24
30
  2. **User flows** — "Walk me through the happy path. What happens on error?"
25
31
  3. **Edge cases & constraints** — "What are the limits? What should NOT happen?"
26
- 4. **Confirm understanding** — Restate what you heard and ask for approval before writing anything
32
+ 4. **Field constraints** — "What are the length limits, allowed formats, required vs optional fields?"
33
+ 5. **Volume & scale** — "How many records are expected? Do you need search or pagination?"
34
+ 6. **File/upload specifics** — (if applicable) "What file types and size limits are allowed?"
35
+ 7. **Privacy & access** — "Who can see this data? Is it per-user or shared?"
36
+ 8. **Relationship to existing features** — (informed by the snapshot) "Does this link to existing data?"
37
+ 9. **Confirm understanding** — Restate what you heard and ask for approval before writing anything
38
+
39
+ **Minimum gate:** Cover at least items 1–3 + any that apply from 4–8 before writing.
40
+
41
+ If the user says "just do it" without answering, document all assumptions explicitly in an `## Assumptions` section at the top of the spec.
27
42
 
28
43
  Only after the user confirms your understanding should you produce a functional issue.
29
44
 
@@ -52,6 +67,12 @@ As a [user type], I want [goal] so that [reason].
52
67
  - Every acceptance criterion maps to at least one test case
53
68
  - No mention of database tables, API calls, component names, or file paths
54
69
 
70
+ ## Language
71
+
72
+ Write requirements and acceptance criteria in the user's language. However:
73
+ - Acceptance criteria IDs (`AC1`, `AC2`), test case IDs (`TC1`), and technical terms (`ActionResult`, `Server Action`, component names) always remain in English
74
+ - Specify that all code-level text (test `it()` descriptions, variable names, error strings in code) must be written in English — only user-visible strings (UI labels, toast messages, validation messages shown on screen) should be in the target language
75
+
55
76
  ## Guardrails
56
77
  - Always ask questions before writing — never assume
57
78
  - Document all requirements in `.requirements/` directory
@@ -59,6 +59,9 @@ glab --version 2>/dev/null && echo "GitLab"
59
59
  - [ ] Semantic HTML
60
60
  - [ ] ARIA labels where needed
61
61
 
62
+ ### Styling
63
+ - [ ] No hardcoded Tailwind palette colors — only design tokens from `tailwind.css`
64
+
62
65
  ## Output Format
63
66
  ```
64
67
  ## Review: <branch/PR name>
@@ -7,6 +7,10 @@ readonly: false
7
7
 
8
8
  # Frontend Agent
9
9
 
10
+ ## First Step
11
+
12
+ Read `.cursor/memory/architecture-snapshot.md` to see which shadcn/ui components are already installed before running `shadcn add`. Use the canonical component test reference to follow the established testing pattern.
13
+
10
14
  ## Responsibilities
11
15
  - Build accessible React components with shadcn/ui and Tailwind v4
12
16
  - Enforce Server vs Client component boundaries
@@ -35,6 +39,19 @@ export function InteractiveComponent() {
35
39
  }
36
40
  ```
37
41
 
42
+ ## Common UI Patterns
43
+
44
+ Use these shadcn components for standard layouts — never roll custom alternatives:
45
+
46
+ | Pattern | shadcn Component | Notes |
47
+ |---------|-----------------|-------|
48
+ | Master-detail (table + side panel) | `ResizablePanelGroup` + `ResizablePanel` | Or CSS grid for fixed layouts |
49
+ | Dialog / modal form | `Dialog`, `DialogContent`, `DialogHeader` | Never use native `<dialog>` — breaks in jsdom |
50
+ | Data table | `Table`, `TableHeader`, `TableRow`, `TableCell` | Or `DataTable` pattern from shadcn docs |
51
+ | Empty state | Centered `<p>` in Spanish inside the table container | Always handle — never leave blank |
52
+ | Loading/pending | `Button` with `loading` prop, or `Spinner` | Use `useTransition` for action pending state |
53
+ | Form with validation | `Form` + `FormField` from shadcn + `zodResolver` | See `forms.mdc` |
54
+
38
55
  ## Toast Feedback
39
56
 
40
57
  All user-initiated actions that modify the database must show a toast (success or error) via `sonner`. Never let a mutation complete silently.
@@ -48,3 +65,4 @@ All user-initiated actions that modify the database must show a toast (success o
48
65
  - Write RTL tests for every component
49
66
  - Verify a11y before marking work done
50
67
  - No component file over 200 lines — split by responsibility
68
+ - Never use raw Tailwind palette colors (`red-500`, `gray-400`, etc.) — only design tokens from `@theme {}` in `tailwind.css`
@@ -9,6 +9,10 @@ readonly: false
9
9
 
10
10
  # Technical Lead Agent
11
11
 
12
+ ## First Step
13
+
14
+ Read `.cursor/memory/architecture-snapshot.md` to understand the current project state before planning. Use it to identify which patterns to reuse and what already exists.
15
+
12
16
  ## Responsibilities
13
17
  - Act as the default entry point for all requests
14
18
  - Classify requests and delegate to the appropriate specialist agent(s)
@@ -43,7 +47,7 @@ Delegate to the appropriate specialist based on domain:
43
47
  ## Workflow Sequences
44
48
 
45
49
  ### New feature
46
- @business-intelligence (functional issue) → @technical-lead (task breakdown + test plan) → @test-qa (RED) → @backend/@frontend (GREEN) → @code-reviewer @security-researcher
50
+ @business-intelligence (functional issue) → @technical-lead (task breakdown + test plan) → @test-qa (RED) → **@backend + @frontend in parallel** (GREEN) → **@code-reviewer + @security-researcher in parallel**
47
51
 
48
52
  ### Bug fix
49
53
  @test-qa (reproduce with failing test) → @backend/@frontend (fix) → @code-reviewer
@@ -62,10 +66,10 @@ When you receive a functional issue from @business-intelligence, do this before
62
66
  - One `describe` block per acceptance criterion
63
67
  - Which mocks are needed (Supabase, HTTP, etc.)
64
68
  - Which edge cases map to which criteria
65
- 4. **Delegate in order**:
69
+ 4. **Delegate in order** (parallel where possible):
66
70
  - @test-qa gets the test plan → writes failing tests (RED)
67
- - @backend/@frontend implements to make tests pass (GREEN)
68
- - @code-reviewer and @security-researcher review the result
71
+ - **@backend + @frontend simultaneously** they work on independent files, no need to serialize (GREEN)
72
+ - **@code-reviewer + @security-researcher simultaneously** — independent review passes, run in parallel
69
73
 
70
74
  ### TDD efficiency guardrails
71
75
  - Plan test structure before delegating to @test-qa — never send "write tests for this feature" without a plan
@@ -7,12 +7,30 @@ readonly: false
7
7
 
8
8
  # Test & QA Agent
9
9
 
10
+ ## First Step
11
+
12
+ Read `.cursor/memory/architecture-snapshot.md` to find the canonical test references. Import `makeChain` and `makeSupabaseMock` from `@/shared/test-utils/supabase-mock` — never recreate the mock chain from scratch.
13
+
10
14
  ## TDD Workflow
15
+
11
16
  1. **RED** — write failing test that describes the desired behavior
12
17
  2. **GREEN** — write minimum code to make test pass
13
18
  3. **REFACTOR** — clean up while keeping tests green
14
19
  4. **VERIFY** — run `pnpm test:coverage` and confirm 100%
15
20
 
21
+ ## Verification Gates (mandatory — show output, do not just claim)
22
+
23
+ Each phase requires pasting the actual terminal output before proceeding:
24
+
25
+ | Gate | Command | Expected output |
26
+ |------|---------|-----------------|
27
+ | After RED | `pnpm test:unit` | All new tests **FAIL** (red) |
28
+ | After GREEN | `pnpm test:unit` | All tests **PASS** (green) |
29
+ | After VERIFY | `pnpm test:coverage` | 100% across all metrics |
30
+
31
+ **Never say "tests pass" or "tests fail" without showing the actual output.**
32
+ **Do NOT write any implementation code until the RED gate output confirms failures.**
33
+
16
34
  ## Key Rules (auto-loaded by file context)
17
35
  - Testing standards, AAA pattern, coverage: `testing.mdc`
18
36
  - TDD iron rules: `coding-standards.mdc`
@@ -30,6 +48,24 @@ After every change:
30
48
  2. If below 100%, add missing tests
31
49
  3. Never mark work complete without full coverage
32
50
 
51
+ ## Supabase Mock Pattern
52
+
53
+ Always use the shared helpers — never inline mock chain construction:
54
+
55
+ ```typescript
56
+ import { makeChain, makeSupabaseMock } from "@/shared/test-utils/supabase-mock";
57
+
58
+ // Unauthenticated
59
+ const { mockClient } = makeSupabaseMock({ user: null });
60
+
61
+ // Authenticated + DB success
62
+ const { mockClient, mockFrom } = makeSupabaseMock({ user: { id: "user-1" } });
63
+ mockFrom.mockReturnValue(makeChain({ data: [...], error: null }));
64
+
65
+ // DB error
66
+ mockFrom.mockReturnValue(makeChain({ error: { message: "db error" } }));
67
+ ```
68
+
33
69
  ## Test Plan Compliance
34
70
 
35
71
  When the Technical Lead provides a test plan, follow it:
@@ -37,7 +73,7 @@ When the Technical Lead provides a test plan, follow it:
37
73
  - Flag missing edge cases to the TL **before** adding unplanned tests — do not expand scope unilaterally
38
74
  - If the plan is ambiguous or incomplete, ask for clarification rather than guessing
39
75
 
40
- For Server Action test templates, see `create-feature` skill references.
76
+ For the full Server Action test template, see `create-feature` skill references.
41
77
 
42
78
  ## Guardrails
43
79
  - Never write implementation without a failing test first
@@ -0,0 +1,8 @@
1
+ {
2
+ "mcpServers": {
3
+ "memory": {
4
+ "command": "memory",
5
+ "args": ["server"]
6
+ }
7
+ }
8
+ }
@@ -0,0 +1,50 @@
1
+ # Architecture Snapshot
2
+
3
+ > Auto-updated after each feature. Agents MUST read this before exploring the codebase.
4
+
5
+ ## Installed shadcn/ui Components
6
+
7
+ button, card, input, label, spinner
8
+
9
+ _Add new components here after `shadcn add <component>`_
10
+
11
+ ## DB Schema
12
+
13
+ | Table | Columns |
14
+ |-------|---------|
15
+ | `profiles` | id (uuid PK), email (text unique), createdAt, updatedAt |
16
+ | `todos` | id (uuid PK), userId (uuid), title (text), completed (bool), createdAt |
17
+
18
+ _Add new tables here after `pnpm db:generate && pnpm db:migrate`_
19
+
20
+ ## Existing Features
21
+
22
+ | Feature | Path | Description |
23
+ |---------|------|-------------|
24
+ | auth | `src/features/auth/` | Login/logout, cookie-based sessions via Supabase |
25
+ | todos | `src/features/todos/` | CRUD todos with Supabase RLS |
26
+
27
+ _Add new features here after implementation_
28
+
29
+ ## Canonical Pattern References
30
+
31
+ Read these files to understand the project's proven patterns before writing new code:
32
+
33
+ | Pattern | File |
34
+ |---------|------|
35
+ | Server Action | `src/features/todos/actions/todos.action.ts` |
36
+ | Query (repository) | `src/features/todos/queries/todos.queries.ts` |
37
+ | Client component | `src/features/todos/components/todo-list.tsx` |
38
+ | Action test | `src/features/todos/__tests__/todos.action.test.ts` |
39
+ | Component test | `src/features/todos/__tests__/todo-list.test.tsx` |
40
+ | Supabase mock chain | `src/shared/test-utils/supabase-mock.ts` |
41
+
42
+ ## Key Rules (summary)
43
+
44
+ - Runtime queries: Supabase client only — Drizzle is schema/migrations only
45
+ - All mutations: Server Actions returning `ActionResult` from `@/shared/lib/action-result`
46
+ - Auth check first in every Server Action: `supabase.auth.getUser()`
47
+ - Toast feedback required for every mutation (success + error) via `sonner`
48
+ - RLS must be enabled on every table before a feature is considered complete
49
+ - UI text: Spanish | Code-level text: English (test descriptions, variable names)
50
+ - 100% test coverage — no exceptions
@@ -1,6 +1,7 @@
1
1
  ---
2
- description: Feature-based architecture rules. Always applies.
3
- alwaysApply: true
2
+ description: Feature-based architecture rules. Applies to all source files.
3
+ alwaysApply: false
4
+ globs: ["src/**"]
4
5
  ---
5
6
 
6
7
  # Architecture Rules
@@ -1,6 +1,7 @@
1
1
  ---
2
- description: Code quality standards. Always applies to all files.
3
- alwaysApply: true
2
+ description: Code quality standards. Applies to TypeScript/TSX source files.
3
+ alwaysApply: false
4
+ globs: ["src/**/*.ts", "src/**/*.tsx"]
4
5
  ---
5
6
 
6
7
  # Coding Standards
@@ -13,18 +14,9 @@ alwaysApply: true
13
14
  - **No `!` non-null assertions** — use proper null checks
14
15
  - **No `eslint-disable` inline comments** — fix the code instead. If a rule conflict is truly unavoidable (e.g. third-party API with no non-deprecated alternative), add a file-level override in `eslint.config.ts` with a `// TODO` comment explaining why.
15
16
 
16
- ## TDD (Test-Driven Development)
17
- 1. **RED**: Write a failing test first
18
- 2. **GREEN**: Write minimum code to pass the test
19
- 3. **REFACTOR**: Improve code while tests stay green
17
+ ## TDD
20
18
 
21
- Never write implementation before tests exist.
22
-
23
- ## TDD — Iron Rule
24
- - Tests define the contract. **NEVER modify a test to make it pass.**
25
- - If a test fails, the implementation is wrong — fix the code, not the test.
26
- - Only modify tests to: add new tests (RED), restructure without weakening (REFACTOR).
27
- - Weakening a test (removing assertions, loosening matchers, adding `.skip`) is a rejection-worthy offense.
19
+ Follow the RED → GREEN → REFACTOR cycle. See `testing.mdc` for the full rules and iron rules. Summary: never write implementation without a prior failing test.
28
20
 
29
21
  ## SOLID Principles
30
22
  - **S**ingle Responsibility: one reason to change per module
@@ -63,6 +55,44 @@ const items = schema.parse(data);
63
55
  const items = data as SupportListItem[];
64
56
  ```
65
57
 
58
+ ## Language Rules
59
+
60
+ - **Code-level text must be in English**: test `it()` descriptions, `describe()` block names, variable names, code comments, developer-facing `Error` constructor messages (never shown to users), `console` output
61
+ - **User-facing text stays in the target language**: UI labels, placeholders, toast messages, form validation messages shown on screen, `ActionResult` error/success strings that end up displayed in the UI
62
+
63
+ ```typescript
64
+ // ✅ Code-level: English
65
+ it("returns error when candidate is not found", async () => { ... });
66
+ throw new Error("Unexpected DB response shape"); // developer-facing, never shown in UI
67
+
68
+ // ✅ User-facing: target language (Spanish)
69
+ return { success: false, error: "Candidato no encontrado" }; // shown in toast
70
+ toast.error("No se pudo guardar el cambio");
71
+
72
+ // ❌ Wrong: code-level in wrong language
73
+ it("retorna error si el candidato no existe", async () => { ... });
74
+ ```
75
+
76
+ ## Coverage Exclusions (configure upfront, not reactively)
77
+
78
+ Before writing tests, add exclusions to `vitest.config.ts` for:
79
+ - Drizzle schema definition files (`src/shared/db/schema.ts`)
80
+ - Pure type files (files with only `type`/`interface` exports, no runtime logic)
81
+ - Third-party UI wrapper components that rely on portals or browser APIs not supported in jsdom (e.g., `sonner.tsx`, toast providers)
82
+
83
+ Configure these exclusions **before writing tests** to avoid reactive coverage fixes mid-implementation.
84
+
85
+ ```typescript
86
+ // vitest.config.ts
87
+ coverage: {
88
+ exclude: [
89
+ "src/shared/db/schema.ts",
90
+ "src/shared/components/ui/sonner.tsx",
91
+ // add other portal/type-only files here
92
+ ],
93
+ }
94
+ ```
95
+
66
96
  ## Pre-commit Quality Gates
67
97
  Every commit must pass two sequential gates:
68
98
  1. **lint-staged** — runs `eslint --max-warnings 0` on staged `.ts/.tsx` files. Any lint warning or error blocks the commit immediately (before the slower test suite runs).
@@ -24,6 +24,13 @@ globs: ["src/**/components/**"]
24
24
  - No inline styles, no CSS modules
25
25
  - Use Tailwind utility classes only
26
26
 
27
+ ## Design Tokens (Required)
28
+ - Always use semantic token classes (`bg-primary`, `text-destructive`, `text-muted-foreground`, `border-border`, etc.)
29
+ - Never use raw Tailwind palette colors (`bg-blue-500`, `text-gray-700`, `text-red-500`, etc.)
30
+ - All colors must reference CSS variables defined in `@theme {}` in `tailwind.css`
31
+ - Available semantic colors: primary, secondary, accent, destructive, success, warning, info, muted, background, foreground, border, input, ring
32
+ - If a needed color does not exist as a token, add it to `@theme {}` in `tailwind.css` first
33
+
27
34
  ## Accessibility (Required)
28
35
  - Semantic HTML elements (`<button>`, `<nav>`, `<main>`, etc.)
29
36
  - ARIA labels for interactive elements without visible text
@@ -1,6 +1,7 @@
1
1
  ---
2
- description: Stack overview and project conventions. Always applies.
3
- alwaysApply: true
2
+ description: Stack overview and project conventions. Applies to all source files.
3
+ alwaysApply: false
4
+ globs: ["src/**"]
4
5
  ---
5
6
 
6
7
  # Stack Overview
@@ -5,23 +5,6 @@ globs: ["src/shared/db/migrations/**"]
5
5
 
6
6
  # Migration Files — Generated Artifacts
7
7
 
8
- Files in `src/shared/db/migrations/` are **auto-generated** by the Drizzle CLI. Do not create, edit, or delete them by hand.
8
+ Files in `src/shared/db/migrations/` are **auto-generated**. See `supabase.mdc` for the full migration workflow and CLI commands.
9
9
 
10
- ## Correct Workflow
11
-
12
- 1. **Edit the schema**: `src/shared/db/schema.ts`
13
- 2. **Generate the migration**: `pnpm db:generate`
14
- 3. **Review** the generated SQL file in `src/shared/db/migrations/`
15
- 4. **Apply the migration**: `pnpm db:migrate`
16
-
17
- ## Available CLI Commands
18
-
19
- | Command | Purpose |
20
- |---|---|
21
- | `pnpm db:generate` | Generate a new migration SQL file from schema changes |
22
- | `pnpm db:migrate` | Apply pending migrations to the database |
23
- | `pnpm db:push` | Push schema directly to DB (dev only, no migration file) |
24
-
25
- ## Why
26
-
27
- Hand-editing migration files causes schema drift and breaks the migration history. The CLI is the only safe source of truth for migration SQL.
10
+ **Never create, edit, or delete migration files by hand.**
@@ -94,7 +94,10 @@ const next = searchParams.get("next") ?? "/";
94
94
  "script-src 'self' 'unsafe-inline'", // TODO: replace with nonce-based CSP
95
95
  "style-src 'self' 'unsafe-inline'",
96
96
 
97
- // Never — unsafe-eval is forbidden
97
+ // Dev-only — unsafe-eval for React debugging features (silences installHook.js warning)
98
+ `script-src 'self' 'unsafe-inline'${isDev ? " 'unsafe-eval'" : ""}`,
99
+
100
+ // ❌ Never — unsafe-eval unconditionally in production
98
101
  "script-src 'self' 'unsafe-eval' 'unsafe-inline'",
99
102
  ```
100
103
 
@@ -48,8 +48,20 @@ const users = await db.select().from(usersTable); // WRONG — bypasses RLS
48
48
 
49
49
  ## RLS Mandate
50
50
  - Enable RLS on ALL tables in Supabase dashboard
51
- - Every table must have explicit policies
52
- - Test RLS policies in migrations
51
+ - Every table must have explicit policies — RLS is not a post-step
52
+ - Default policy template: `CREATE POLICY "users_own_rows" ON <table> FOR ALL USING (user_id = auth.uid());`
53
+
54
+ ## Migrations
55
+
56
+ Files in `src/shared/db/migrations/` are **auto-generated** by Drizzle CLI — never create, edit, or delete them directly.
57
+
58
+ | Command | Purpose |
59
+ |---|---|
60
+ | `pnpm db:generate` | Generate migration SQL from schema changes |
61
+ | `pnpm db:migrate` | Apply pending migrations to the database |
62
+ | `pnpm db:push` | Push schema directly to DB (dev only, no migration file) |
63
+
64
+ Workflow: edit `src/shared/db/schema.ts` → `pnpm db:generate` → review SQL → `pnpm db:migrate`
53
65
 
54
66
  ## Environment Variables
55
67
  - `NEXT_PUBLIC_SUPABASE_URL` + `NEXT_PUBLIC_SUPABASE_ANON_KEY` — client-safe, used in browser + server
@@ -111,6 +111,14 @@ For every function, ask: what happens with...
111
111
 
112
112
  If any of these apply, write a test for it. No exceptions.
113
113
 
114
+ ## jsdom Known Limitations (avoid rework cycles)
115
+
116
+ jsdom does not implement all browser APIs. Hitting these in tests causes failures that require full component rewrites — avoid them upfront:
117
+
118
+ - **No `HTMLDialogElement` methods** — `dialog.showModal()` and `dialog.close()` throw in jsdom. Use state-controlled visibility (`isOpen` prop / conditional render) instead of the native `<dialog>` API.
119
+ - **No portal-based components in unit tests** — components like `<Toaster>` (sonner), `<Tooltip>`, `<Popover>` render outside the React tree and are unreliable in jsdom. Exclude their wrapper files from coverage; test toast behavior indirectly via the action's `ActionResult`.
120
+ - **Run `pnpm lint` before finishing** — do not leave lint errors to a separate pass. Fix inline as you go.
121
+
114
122
  ## Coverage Thresholds
115
123
  ```typescript
116
124
  // vitest.config.ts
@@ -5,6 +5,17 @@ description: Scaffold a new feature following TDD and project conventions. Use w
5
5
 
6
6
  # Create Feature Skill
7
7
 
8
+ > **Invoke as:** `/create-feature @.requirements/<feature-name>.md`
9
+ > This skill is orchestrated by the `@technical-lead` agent — do NOT add `use @technical-lead` to the invocation, it is redundant.
10
+
11
+ ## IMPORTANT: Run this in a FRESH conversation
12
+
13
+ If this conversation already contains requirements gathering (BI agent output), **start a new conversation** before running `/create-feature`. Reusing the same context risks hitting the token limit mid-implementation.
14
+
15
+ If context usage exceeds 60% at any point during this skill, stop and tell the user to continue in a new conversation, referencing this plan file.
16
+
17
+ ---
18
+
8
19
  ## Process
9
20
 
10
21
  ### 1. Requirements First
@@ -13,7 +24,15 @@ description: Scaffold a new feature following TDD and project conventions. Use w
13
24
  - **If no spec exists, call @business-intelligence** to run the discovery process and produce a functional issue in `.requirements/<feature-name>.md`
14
25
  - Wait for the functional issue to be written and confirmed by the user before proceeding to Step 2
15
26
 
16
- ### 2. Create Feature Structure
27
+ ### 2. Read the Architecture Snapshot
28
+
29
+ Read `.cursor/memory/architecture-snapshot.md`. This tells you:
30
+ - Which shadcn/ui components are already installed (no need to `shadcn add` duplicates)
31
+ - The current DB schema (what tables exist, what to extend)
32
+ - Existing features and their paths (avoid recreating patterns)
33
+ - Canonical pattern references (copy from these, do not reinvent)
34
+
35
+ ### 3. Create Feature Structure
17
36
  ```
18
37
  src/features/<feature-name>/
19
38
  ├── components/
@@ -25,32 +44,93 @@ src/features/<feature-name>/
25
44
  └── __tests__/
26
45
  ```
27
46
 
28
- ### 3. TDD: RED Phase
29
- Write ALL test files first:
47
+ ### 4. Pre-Test Setup (do this before writing any tests)
48
+
49
+ Update `vitest.config.ts` to exclude files that cannot be meaningfully tested in jsdom:
50
+ - Drizzle schema file(s) (`src/shared/db/schema.ts`)
51
+ - Pure type-only files (no runtime logic)
52
+ - Portal/browser-API-dependent UI wrappers (e.g., `src/shared/components/ui/sonner.tsx`)
53
+
54
+ ```typescript
55
+ // vitest.config.ts — add to coverage.exclude before writing tests
56
+ exclude: [
57
+ "src/shared/db/schema.ts",
58
+ "src/shared/components/ui/sonner.tsx",
59
+ ]
60
+ ```
61
+
62
+ Configuring exclusions upfront prevents reactive coverage fixes mid-implementation.
63
+
64
+ ### 5. TDD: RED Phase
65
+
66
+ Write ALL test files first — zero implementation code at this stage:
30
67
  - `__tests__/<component>.test.tsx` — component tests
31
- - `__tests__/use-<feature>.test.ts` — hook tests
32
- - `__tests__/<action>.test.ts` — action tests (see `references/server-action-test-template.md`)
68
+ - `__tests__/use-<feature>.test.ts` — hook tests (if applicable)
69
+ - `__tests__/<action>.action.test.ts` — action tests (see `references/server-action-test-template.md`)
33
70
  - `__tests__/<feature>.queries.test.ts` — query tests
34
71
 
35
- Run `pnpm test:unit` — all tests must FAIL (RED).
72
+ Import mock helpers from `@/shared/test-utils/supabase-mock` — do not inline mock chains.
36
73
 
37
- ### 4. TDD: GREEN Phase
38
- Implement minimum code to pass each test:
39
- - Components → actions → hooks → API routes
74
+ **BLOCKING GATE do not proceed until this passes:**
75
+ Run `pnpm test:unit` and paste the output here. All new tests must **FAIL** (red). If any new test passes without implementation, the test is wrong — fix it before continuing.
40
76
 
41
- Run `pnpm test:unit` all tests must PASS (GREEN).
77
+ ### 6. TDD: GREEN Phase
42
78
 
43
- ### 5. Refactor
79
+ Launch **@backend and @frontend in parallel** — they work on independent files:
80
+
81
+ | @backend handles | @frontend handles |
82
+ |-----------------|-------------------|
83
+ | `actions/` | `components/` |
84
+ | `queries/` | `page.tsx` |
85
+ | `schema.ts` changes | shadcn component installs |
86
+ | RLS policies | Loading/empty states |
87
+
88
+ Do NOT serialize these — they touch different files.
89
+
90
+ **BLOCKING GATE — do not proceed until this passes:**
91
+ Run `pnpm test:unit` and paste the output. All tests must **PASS** (green).
92
+
93
+ ### 7. Refactor
44
94
  Clean up while keeping tests green.
45
95
 
46
- ### 6. Verify
96
+ ### 8. Verify & Self-Check
97
+
98
+ Run all three and paste output:
47
99
  ```bash
48
- pnpm test:coverage # Must show 100%
100
+ pnpm test:coverage # Must show 100% across all metrics
49
101
  pnpm lint # Must pass with 0 warnings
50
102
  pnpm typecheck # Must pass with 0 errors
51
103
  ```
52
104
 
105
+ Before moving to the review gate, verify manually:
106
+ - [ ] No `any` types: `grep -r ": any" src/features/<feature>/`
107
+ - [ ] No `eslint-disable` comments
108
+ - [ ] All UI text in Spanish, all `it()`/`describe()` text in English
109
+ - [ ] No file over 200 lines
110
+ - [ ] No function over 20 lines
111
+ - [ ] AAA pattern (`// Arrange`, `// Act`, `// Assert`) in every test
112
+ - [ ] `ActionResult` returned from every Server Action mutation
113
+ - [ ] Toast feedback (`toast.success` / `toast.error`) for every mutation
114
+ - [ ] RLS policies written for every new table
115
+
116
+ ### 9. Review Gate
117
+
118
+ Run @code-reviewer and @security-researcher **in parallel** on the changed files. Fix any findings before marking the feature complete.
119
+
120
+ This step is not optional. The feature is not done until both reviewers pass.
121
+
122
+ ### 10. Update Architecture Snapshot
123
+
124
+ Update `.cursor/memory/architecture-snapshot.md`:
125
+ - Add new DB tables to the schema table
126
+ - Add new shadcn components to the installed list
127
+ - Add the new feature to the Existing Features table
128
+ - Add any new canonical pattern references that differ from existing ones
129
+
130
+ ---
131
+
53
132
  ## Guardrails
54
- - NEVER start implementation before tests exist
133
+ - NEVER start implementation before the RED gate is confirmed with actual test output
55
134
  - 100% coverage before marking done
56
135
  - Follow `features/* → shared/*` dependency direction
136
+ - RLS is mandatory for every new table — not a post-step
@@ -2,50 +2,102 @@
2
2
 
3
3
  Test Server Actions by mocking `createClient`, asserting on the returned `ActionResult`, and verifying side effects.
4
4
 
5
+ ## Setup
6
+
5
7
  ```typescript
6
- import { vi, it, expect, describe } from "vitest";
8
+ import { vi, it, expect, describe, beforeEach } from "vitest";
9
+ import { createClient } from "@/shared/lib/supabase/server";
10
+ import { revalidatePath } from "next/cache";
11
+ import { makeChain, makeSupabaseMock } from "@/shared/test-utils/supabase-mock";
7
12
  import { myAction } from "@/features/example/actions/my.action";
8
13
 
9
- vi.mock("@/shared/lib/supabase/server", () => ({
10
- createClient: vi.fn(),
11
- }));
14
+ vi.mock("@/shared/lib/supabase/server", () => ({ createClient: vi.fn() }));
12
15
  vi.mock("next/cache", () => ({ revalidatePath: vi.fn() }));
13
16
 
14
- describe("myAction", () => {
15
- it("returns error when user is not authenticated", async () => {
16
- // Arrange
17
- const mockSupabase = {
18
- auth: { getUser: vi.fn().mockResolvedValue({ data: { user: null } }) },
19
- };
20
- vi.mocked(createClient).mockResolvedValue(mockSupabase as never);
21
- const formData = new FormData();
22
-
23
- // Act
24
- const result = await myAction(formData);
25
-
26
- // Assert
27
- expect(result).toEqual({ status: "error", message: "No autenticado" });
28
- expect(revalidatePath).not.toHaveBeenCalled();
29
- });
30
-
31
- it("returns success and revalidates on valid input", async () => {
32
- // Arrange
33
- const mockUser = { id: "user-1" };
34
- const mockSupabase = {
35
- auth: { getUser: vi.fn().mockResolvedValue({ data: { user: mockUser } }) },
36
- from: vi.fn().mockReturnThis(),
37
- insert: vi.fn().mockResolvedValue({ error: null }),
38
- };
39
- vi.mocked(createClient).mockResolvedValue(mockSupabase as never);
40
- const formData = new FormData();
41
- formData.set("name", "Test Item");
42
-
43
- // Act
44
- const result = await myAction(formData);
45
-
46
- // Assert
47
- expect(result).toEqual({ status: "success", message: expect.any(String) });
48
- expect(revalidatePath).toHaveBeenCalledWith("/");
49
- });
17
+ beforeEach(() => {
18
+ vi.clearAllMocks();
19
+ });
20
+ ```
21
+
22
+ ## Unauthenticated case (required in every action test)
23
+
24
+ ```typescript
25
+ it("returns error when user is not authenticated", async () => {
26
+ // Arrange
27
+ const { mockClient } = makeSupabaseMock({ user: null });
28
+ vi.mocked(createClient).mockResolvedValue(mockClient as never);
29
+ const formData = new FormData();
30
+
31
+ // Act
32
+ const result = await myAction(formData);
33
+
34
+ // Assert
35
+ expect(result).toEqual({ status: "error", message: "No autenticado" });
36
+ expect(revalidatePath).not.toHaveBeenCalled();
50
37
  });
51
38
  ```
39
+
40
+ ## Happy path (insert example)
41
+
42
+ ```typescript
43
+ it("returns success and revalidates on valid input", async () => {
44
+ // Arrange
45
+ const { mockClient, mockFrom } = makeSupabaseMock({ user: { id: "user-1" } });
46
+ vi.mocked(createClient).mockResolvedValue(mockClient as never);
47
+ mockFrom.mockReturnValue(makeChain({ error: null }));
48
+ const formData = new FormData();
49
+ formData.set("title", "My item");
50
+
51
+ // Act
52
+ const result = await myAction(formData);
53
+
54
+ // Assert
55
+ expect(result).toEqual({ status: "success", message: expect.any(String) });
56
+ expect(revalidatePath).toHaveBeenCalledWith("/");
57
+ });
58
+ ```
59
+
60
+ ## DB error case
61
+
62
+ ```typescript
63
+ it("returns error when db insert fails", async () => {
64
+ // Arrange
65
+ const { mockClient, mockFrom } = makeSupabaseMock({ user: { id: "user-1" } });
66
+ vi.mocked(createClient).mockResolvedValue(mockClient as never);
67
+ mockFrom.mockReturnValue(makeChain({ error: { message: "db error" } }));
68
+ const formData = new FormData();
69
+ formData.set("title", "My item");
70
+
71
+ // Act
72
+ const result = await myAction(formData);
73
+
74
+ // Assert
75
+ expect(result).toEqual({ status: "error", message: expect.any(String) });
76
+ expect(revalidatePath).not.toHaveBeenCalled();
77
+ });
78
+ ```
79
+
80
+ ## Validation error case
81
+
82
+ ```typescript
83
+ it("returns error when required field is missing", async () => {
84
+ // Arrange
85
+ const { mockClient } = makeSupabaseMock({ user: { id: "user-1" } });
86
+ vi.mocked(createClient).mockResolvedValue(mockClient as never);
87
+ const formData = new FormData(); // no fields set
88
+
89
+ // Act
90
+ const result = await myAction(formData);
91
+
92
+ // Assert
93
+ expect(result).toEqual({ status: "error", message: expect.any(String) });
94
+ });
95
+ ```
96
+
97
+ ## Key Rules
98
+
99
+ - Always import `makeChain` and `makeSupabaseMock` from `@/shared/test-utils/supabase-mock`
100
+ - Every action test must cover: unauthenticated, validation error, DB error, happy path
101
+ - `mockFrom` returns a chain — each chained method (`.select()`, `.insert()`, etc.) returns the same chain object
102
+ - `then` on the chain resolves with the value you pass to `makeChain()`
103
+ - Never use `mockReturnThis()` — it does not match Supabase's actual chaining API
@@ -19,10 +19,26 @@ Read these rules when working on related files:
19
19
 
20
20
  This project uses specialist agents in `.claude/agents/`. Follow the technical-lead workflow:
21
21
 
22
- - **New feature**: requirements (`business-intelligence`) → task breakdown (`technical-lead`) → tests first (`test-qa`) → implementation (`backend`/`frontend`) → review (`code-reviewer`) security (`security-researcher`)
22
+ - **New feature**: requirements (`business-intelligence`) → task breakdown (`technical-lead`) → tests first (`test-qa`) → implementation (`backend` + `frontend` **in parallel**) → review (`code-reviewer` + `security-researcher` **in parallel**)
23
23
  - **Bug fix**: reproduce with failing test (`test-qa`) → fix (`backend`/`frontend`) → review (`code-reviewer`)
24
24
  - **Refactor**: plan (`technical-lead`) → implement (`backend`/`frontend`) → verify (`test-qa`) → review (`code-reviewer`)
25
25
 
26
+ ## Recommended: 2-Conversation Workflow for New Features
27
+
28
+ Split new feature work across two conversations to avoid context exhaustion (hitting >80% context mid-implementation):
29
+
30
+ **Conversation 1 — Requirements** (low context, can be discarded after)
31
+ ```
32
+ @business-intelligence → write .requirements/<feature-name>.md → confirm with user
33
+ ```
34
+
35
+ **Conversation 2 — Implementation** (fresh context, reads spec from file)
36
+ ```
37
+ /create-feature → reads .requirements/<feature-name>.md → full TDD pipeline
38
+ ```
39
+
40
+ The `/create-feature` skill reads the requirements file directly, so the BI conversation history is not needed during implementation. This keeps each conversation well under 50% context usage.
41
+
26
42
  # File Naming
27
43
 
28
44
  - Server Actions: `name.action.ts` with `"use server"` at top
@@ -30,4 +46,14 @@ This project uses specialist agents in `.claude/agents/`. Follow the technical-l
30
46
  - Components: PascalCase `.tsx`
31
47
  - Tests: colocated in `__tests__/`, named `name.test.ts`
32
48
 
49
+ # MCP Memory Service
50
+
51
+ This project uses [mcp-memory-service](https://github.com/doobidoo/mcp-memory-service) for persistent semantic memory across sessions.
52
+ Config is shared via `.cursor/mcp.json` (symlinked to `.mcp.json`).
53
+
54
+ If not installed automatically during scaffolding:
55
+ ```bash
56
+ pip install mcp-memory-service
57
+ ```
58
+
33
59
  # Maintenance Notes
@@ -1,5 +1,7 @@
1
1
  import type { NextConfig } from "next";
2
2
 
3
+ const isDev = process.env.NODE_ENV !== "production";
4
+
3
5
  const nextConfig: NextConfig = {
4
6
  typedRoutes: true,
5
7
  logging: {
@@ -21,7 +23,8 @@ const nextConfig: NextConfig = {
21
23
  value: [
22
24
  "default-src 'self'",
23
25
  // TODO: replace 'unsafe-inline' with nonce-based CSP
24
- "script-src 'self' 'unsafe-inline'",
26
+ // 'unsafe-eval' is added in dev only for React's debugging features (never in production)
27
+ `script-src 'self' 'unsafe-inline'${isDev ? " 'unsafe-eval'" : ""}`,
25
28
  "style-src 'self' 'unsafe-inline'",
26
29
  "img-src 'self' data: blob: https:",
27
30
  "font-src 'self'",
@@ -21,7 +21,7 @@ export default async function HomePage() {
21
21
  <form action={logoutAction}>
22
22
  <button
23
23
  type="submit"
24
- className="rounded bg-gray-200 px-4 py-2 text-sm hover:bg-gray-300"
24
+ className="rounded bg-muted px-4 py-2 text-sm hover:bg-muted-foreground/10"
25
25
  >
26
26
  Cerrar sesión
27
27
  </button>
@@ -34,7 +34,7 @@ export function AddTodoForm() {
34
34
  placeholder="Nueva tarea…"
35
35
  required
36
36
  disabled={isPending}
37
- className="flex-1 rounded border px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-gray-400"
37
+ className="flex-1 rounded border px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"
38
38
  aria-label="Nueva tarea"
39
39
  />
40
40
  <Button type="submit" loading={isPending}>
@@ -17,7 +17,7 @@ export function TodoList({ items }: TodoListProps) {
17
17
 
18
18
  if (items.length === 0) {
19
19
  return (
20
- <p className="mt-4 text-center text-sm text-gray-500">
20
+ <p className="mt-4 text-center text-sm text-muted-foreground">
21
21
  No tienes tareas. ¡Agrega una!
22
22
  </p>
23
23
  );
@@ -44,10 +44,10 @@ export function TodoList({ items }: TodoListProps) {
44
44
  }}
45
45
  aria-label={`Marcar "${todo.title}" como completada`}
46
46
  />
47
- <span className={todo.completed ? "line-through text-gray-400" : ""}>
47
+ <span className={todo.completed ? "line-through text-muted-foreground" : ""}>
48
48
  {todo.title}
49
49
  </span>
50
- {isPending && <Spinner className="size-4 text-gray-400" />}
50
+ {isPending && <Spinner className="size-4 text-muted-foreground" />}
51
51
  </label>
52
52
  <button
53
53
  type="button"
@@ -59,7 +59,7 @@ export function TodoList({ items }: TodoListProps) {
59
59
  else toast.success(result.message);
60
60
  });
61
61
  }}
62
- className="rounded px-2 py-1 text-sm text-red-500 hover:bg-red-50"
62
+ className="rounded px-2 py-1 text-sm text-destructive hover:bg-destructive/10"
63
63
  aria-label={`Eliminar "${todo.title}"`}
64
64
  >
65
65
  Eliminar
@@ -19,7 +19,16 @@ export function Providers({ children }: { children: React.ReactNode }) {
19
19
  return (
20
20
  <QueryClientProvider client={queryClient}>
21
21
  {children}
22
- <Toaster />
22
+ <Toaster
23
+ toastOptions={{
24
+ classNames: {
25
+ success: "!bg-success !text-success-foreground !border-success",
26
+ error: "!bg-destructive !text-destructive-foreground !border-destructive",
27
+ warning: "!bg-warning !text-warning-foreground !border-warning",
28
+ info: "!bg-info !text-info-foreground !border-info",
29
+ },
30
+ }}
31
+ />
23
32
  </QueryClientProvider>
24
33
  );
25
34
  }
@@ -0,0 +1,59 @@
1
+ import { vi } from "vitest";
2
+
3
+ type Chain = Record<string, ReturnType<typeof vi.fn>>;
4
+
5
+ /**
6
+ * Creates a chainable Supabase query mock.
7
+ *
8
+ * Usage:
9
+ * const chain = makeChain({ data: [...], error: null });
10
+ * mockSupabase.from.mockReturnValue(chain);
11
+ *
12
+ * Supports: .select() .insert() .update() .delete() .eq() .single() .order()
13
+ * Each method returns the same chain, so calls can be chained arbitrarily.
14
+ * The chain resolves with `resolvedValue` when awaited.
15
+ */
16
+ export function makeChain(resolvedValue: unknown): Chain {
17
+ const chain: Chain = {};
18
+ const methods = [
19
+ "select",
20
+ "insert",
21
+ "update",
22
+ "delete",
23
+ "eq",
24
+ "single",
25
+ "order",
26
+ "limit",
27
+ "match",
28
+ ];
29
+ for (const method of methods) {
30
+ chain[method] = vi.fn().mockReturnValue(chain);
31
+ }
32
+ chain["then"] = vi.fn((cb: (v: unknown) => unknown) => cb(resolvedValue));
33
+ return chain;
34
+ }
35
+
36
+ /**
37
+ * Creates a minimal Supabase client mock with auth + from.
38
+ *
39
+ * Usage:
40
+ * const { mockClient, mockFrom } = makeSupabaseMock({ user: mockUser });
41
+ * vi.mocked(createClient).mockResolvedValue(mockClient as never);
42
+ * mockFrom.mockReturnValue(makeChain({ data: [...], error: null }));
43
+ */
44
+ export function makeSupabaseMock(authResult: {
45
+ user: { id: string } | null;
46
+ error?: unknown;
47
+ }) {
48
+ const mockFrom = vi.fn();
49
+ const mockClient = {
50
+ auth: {
51
+ getUser: vi.fn().mockResolvedValue({
52
+ data: { user: authResult.user },
53
+ error: authResult.error ?? null,
54
+ }),
55
+ },
56
+ from: mockFrom,
57
+ };
58
+ return { mockClient, mockFrom };
59
+ }
@@ -1,20 +1,26 @@
1
1
  @import "tailwindcss";
2
2
 
3
3
  @theme {
4
- --color-background: hsl(0 0% 100%);
5
- --color-foreground: hsl(222.2 84% 4.9%);
6
- --color-primary: hsl(222.2 47.4% 11.2%);
7
- --color-primary-foreground: hsl(210 40% 98%);
8
- --color-secondary: hsl(210 40% 96.1%);
9
- --color-secondary-foreground: hsl(222.2 47.4% 11.2%);
10
- --color-muted: hsl(210 40% 96.1%);
11
- --color-muted-foreground: hsl(215.4 16.3% 46.9%);
12
- --color-accent: hsl(210 40% 96.1%);
13
- --color-accent-foreground: hsl(222.2 47.4% 11.2%);
4
+ --color-background: #ffffff;
5
+ --color-foreground: #111111;
6
+ --color-primary: #022633;
7
+ --color-primary-foreground: #ffffff;
8
+ --color-secondary: #f6a623;
9
+ --color-secondary-foreground: #ffffff;
10
+ --color-muted: #f5f5f5;
11
+ --color-muted-foreground: #666666;
12
+ --color-accent: #044a68;
13
+ --color-accent-foreground: #ffffff;
14
14
  --color-destructive: hsl(0 84.2% 60.2%);
15
- --color-destructive-foreground: hsl(210 40% 98%);
16
- --color-border: hsl(214.3 31.8% 91.4%);
17
- --color-input: hsl(214.3 31.8% 91.4%);
18
- --color-ring: hsl(222.2 84% 4.9%);
15
+ --color-destructive-foreground: #ffffff;
16
+ --color-success: hsl(142 71% 45%);
17
+ --color-success-foreground: #ffffff;
18
+ --color-warning: hsl(38 92% 50%);
19
+ --color-warning-foreground: #ffffff;
20
+ --color-info: hsl(199 89% 48%);
21
+ --color-info-foreground: #ffffff;
22
+ --color-border: #e5e5e5;
23
+ --color-input: #e5e5e5;
24
+ --color-ring: #022633;
19
25
  --radius: 0.5rem;
20
26
  }