@massu/core 0.4.2 → 0.6.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 (125) hide show
  1. package/README.md +40 -0
  2. package/agents/massu-architecture-reviewer.md +104 -0
  3. package/agents/massu-blast-radius-analyzer.md +84 -0
  4. package/agents/massu-competitive-scorer.md +126 -0
  5. package/agents/massu-help-sync.md +73 -0
  6. package/agents/massu-migration-writer.md +94 -0
  7. package/agents/massu-output-scorer.md +87 -0
  8. package/agents/massu-pattern-reviewer.md +84 -0
  9. package/agents/massu-plan-auditor.md +170 -0
  10. package/agents/massu-schema-sync-verifier.md +70 -0
  11. package/agents/massu-security-reviewer.md +98 -0
  12. package/agents/massu-ux-reviewer.md +106 -0
  13. package/commands/_shared-preamble.md +53 -23
  14. package/commands/_shared-references/auto-learning-protocol.md +71 -0
  15. package/commands/_shared-references/blast-radius-protocol.md +76 -0
  16. package/commands/_shared-references/security-pre-screen.md +64 -0
  17. package/commands/_shared-references/test-first-protocol.md +87 -0
  18. package/commands/_shared-references/verification-table.md +52 -0
  19. package/commands/massu-article-review.md +343 -0
  20. package/commands/massu-autoresearch/references/eval-runner.md +84 -0
  21. package/commands/massu-autoresearch/references/safety-rails.md +125 -0
  22. package/commands/massu-autoresearch/references/scoring-protocol.md +151 -0
  23. package/commands/massu-autoresearch.md +258 -0
  24. package/commands/massu-batch.md +44 -12
  25. package/commands/massu-bearings.md +42 -8
  26. package/commands/massu-checkpoint.md +588 -0
  27. package/commands/massu-ci-fix.md +2 -2
  28. package/commands/massu-command-health.md +132 -0
  29. package/commands/massu-command-improve.md +232 -0
  30. package/commands/massu-commit.md +205 -44
  31. package/commands/massu-create-plan.md +239 -57
  32. package/commands/massu-data/references/common-queries.md +79 -0
  33. package/commands/massu-data/references/table-guide.md +50 -0
  34. package/commands/massu-data.md +66 -0
  35. package/commands/massu-dead-code.md +29 -34
  36. package/commands/massu-debug/references/auto-learning.md +61 -0
  37. package/commands/massu-debug/references/codegraph-tracing.md +80 -0
  38. package/commands/massu-debug/references/common-shortcuts.md +98 -0
  39. package/commands/massu-debug/references/investigation-phases.md +294 -0
  40. package/commands/massu-debug/references/report-format.md +107 -0
  41. package/commands/massu-debug.md +105 -386
  42. package/commands/massu-docs.md +1 -1
  43. package/commands/massu-full-audit.md +61 -0
  44. package/commands/massu-gap-enhancement-analyzer.md +276 -16
  45. package/commands/massu-golden-path/references/approval-points.md +216 -0
  46. package/commands/massu-golden-path/references/competitive-mode.md +273 -0
  47. package/commands/massu-golden-path/references/error-handling.md +121 -0
  48. package/commands/massu-golden-path/references/phase-0-requirements.md +53 -0
  49. package/commands/massu-golden-path/references/phase-1-plan-creation.md +168 -0
  50. package/commands/massu-golden-path/references/phase-2-implementation.md +397 -0
  51. package/commands/massu-golden-path/references/phase-2.5-gap-analyzer.md +156 -0
  52. package/commands/massu-golden-path/references/phase-3-simplify.md +40 -0
  53. package/commands/massu-golden-path/references/phase-4-commit.md +94 -0
  54. package/commands/massu-golden-path/references/phase-5-push.md +116 -0
  55. package/commands/massu-golden-path/references/phase-5.5-production-verify.md +170 -0
  56. package/commands/massu-golden-path/references/phase-6-completion.md +113 -0
  57. package/commands/massu-golden-path/references/qa-evaluator-spec.md +137 -0
  58. package/commands/massu-golden-path/references/sprint-contract-protocol.md +117 -0
  59. package/commands/massu-golden-path/references/vr-visual-calibration.md +73 -0
  60. package/commands/massu-golden-path.md +114 -848
  61. package/commands/massu-guide.md +72 -69
  62. package/commands/massu-hooks.md +27 -12
  63. package/commands/massu-hotfix.md +221 -144
  64. package/commands/massu-incident.md +49 -20
  65. package/commands/massu-infra-audit.md +187 -0
  66. package/commands/massu-learning-audit.md +211 -0
  67. package/commands/massu-loop/references/auto-learning.md +49 -0
  68. package/commands/massu-loop/references/checkpoint-audit.md +40 -0
  69. package/commands/massu-loop/references/guardrails.md +17 -0
  70. package/commands/massu-loop/references/iteration-structure.md +115 -0
  71. package/commands/massu-loop/references/loop-controller.md +188 -0
  72. package/commands/massu-loop/references/plan-extraction.md +78 -0
  73. package/commands/massu-loop/references/vr-plan-spec.md +140 -0
  74. package/commands/massu-loop-playwright.md +9 -9
  75. package/commands/massu-loop.md +115 -670
  76. package/commands/massu-new-pattern.md +423 -0
  77. package/commands/massu-perf.md +422 -0
  78. package/commands/massu-plan-audit.md +1 -1
  79. package/commands/massu-plan.md +389 -122
  80. package/commands/massu-production-verify.md +433 -0
  81. package/commands/massu-push.md +62 -378
  82. package/commands/massu-recap.md +29 -3
  83. package/commands/massu-rollback.md +613 -0
  84. package/commands/massu-scaffold-hook.md +2 -4
  85. package/commands/massu-scaffold-page.md +2 -3
  86. package/commands/massu-scaffold-router.md +1 -2
  87. package/commands/massu-security.md +619 -0
  88. package/commands/massu-simplify.md +115 -85
  89. package/commands/massu-squirrels.md +2 -2
  90. package/commands/massu-tdd.md +38 -22
  91. package/commands/massu-test.md +3 -3
  92. package/commands/massu-type-mismatch-audit.md +469 -0
  93. package/commands/massu-ui-audit.md +587 -0
  94. package/commands/massu-verify-playwright.md +287 -32
  95. package/commands/massu-verify.md +150 -46
  96. package/dist/cli.js +1451 -1047
  97. package/dist/hooks/post-tool-use.js +75 -6
  98. package/dist/hooks/user-prompt.js +16 -0
  99. package/package.json +6 -2
  100. package/patterns/build-patterns.md +302 -0
  101. package/patterns/component-patterns.md +246 -0
  102. package/patterns/display-patterns.md +185 -0
  103. package/patterns/form-patterns.md +890 -0
  104. package/patterns/integration-testing-checklist.md +445 -0
  105. package/patterns/security-patterns.md +219 -0
  106. package/patterns/testing-patterns.md +569 -0
  107. package/patterns/tool-routing.md +81 -0
  108. package/patterns/ui-patterns.md +371 -0
  109. package/protocols/plan-implementation.md +267 -0
  110. package/protocols/recovery.md +225 -0
  111. package/protocols/verification.md +404 -0
  112. package/reference/command-taxonomy.md +178 -0
  113. package/reference/cr-rules-reference.md +76 -0
  114. package/reference/hook-execution-order.md +148 -0
  115. package/reference/lessons-learned.md +175 -0
  116. package/reference/patterns-quickref.md +208 -0
  117. package/reference/standards.md +135 -0
  118. package/reference/subagents-reference.md +17 -0
  119. package/reference/vr-verification-reference.md +867 -0
  120. package/src/commands/init.ts +27 -0
  121. package/src/commands/install-commands.ts +149 -53
  122. package/src/hooks/post-tool-use.ts +17 -0
  123. package/src/hooks/user-prompt.ts +21 -0
  124. package/src/memory-file-ingest.ts +127 -0
  125. package/src/memory-tools.ts +34 -1
@@ -1606,9 +1606,66 @@ function storeSecurityScore(db, sessionId, filePath, riskScore, findings) {
1606
1606
  }
1607
1607
 
1608
1608
  // src/hooks/post-tool-use.ts
1609
- import { readFileSync as readFileSync5, existsSync as existsSync6 } from "fs";
1609
+ import { readFileSync as readFileSync6, existsSync as existsSync7 } from "fs";
1610
1610
  import { join as join2 } from "path";
1611
+ import { parse as parseYaml3 } from "yaml";
1612
+
1613
+ // src/memory-file-ingest.ts
1614
+ import { readFileSync as readFileSync5, existsSync as existsSync6, readdirSync } from "fs";
1611
1615
  import { parse as parseYaml2 } from "yaml";
1616
+ function ingestMemoryFile(db, sessionId, filePath) {
1617
+ if (!existsSync6(filePath)) return "skipped";
1618
+ const content = readFileSync5(filePath, "utf-8");
1619
+ const basename2 = (filePath.split("/").pop() ?? "").replace(".md", "");
1620
+ const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
1621
+ let name = basename2;
1622
+ let description = "";
1623
+ let type = "discovery";
1624
+ let confidence;
1625
+ if (frontmatterMatch) {
1626
+ try {
1627
+ const fm = parseYaml2(frontmatterMatch[1]);
1628
+ name = fm.name ?? basename2;
1629
+ description = fm.description ?? "";
1630
+ type = fm.type ?? "discovery";
1631
+ confidence = fm.confidence != null ? Number(fm.confidence) : void 0;
1632
+ } catch {
1633
+ }
1634
+ }
1635
+ const obsType = mapMemoryTypeToObservationType(type);
1636
+ const importance = confidence != null ? Math.max(1, Math.min(5, Math.round(confidence * 4 + 1))) : 4;
1637
+ const bodyMatch = content.match(/^---\n[\s\S]*?\n---\n([\s\S]*)/);
1638
+ const body = bodyMatch ? bodyMatch[1].trim().slice(0, 500) : "";
1639
+ const title = `[memory-file] ${name}`;
1640
+ const detail = description ? `${description}
1641
+
1642
+ ${body}` : body;
1643
+ const existing = db.prepare(
1644
+ "SELECT id FROM observations WHERE title = ? LIMIT 1"
1645
+ ).get(title);
1646
+ if (existing) {
1647
+ db.prepare("UPDATE observations SET detail = ?, importance = ? WHERE id = ?").run(detail, importance, existing.id);
1648
+ return "updated";
1649
+ } else {
1650
+ addObservation(db, sessionId, obsType, title, detail, { importance });
1651
+ return "inserted";
1652
+ }
1653
+ }
1654
+ function mapMemoryTypeToObservationType(memoryType) {
1655
+ switch (memoryType) {
1656
+ case "user":
1657
+ case "feedback":
1658
+ return "decision";
1659
+ case "project":
1660
+ return "feature";
1661
+ case "reference":
1662
+ return "discovery";
1663
+ default:
1664
+ return "discovery";
1665
+ }
1666
+ }
1667
+
1668
+ // src/hooks/post-tool-use.ts
1612
1669
  var seenReads = /* @__PURE__ */ new Set();
1613
1670
  var currentSessionId = null;
1614
1671
  async function main() {
@@ -1705,6 +1762,18 @@ async function main() {
1705
1762
  }
1706
1763
  } catch (_memoryErr) {
1707
1764
  }
1765
+ try {
1766
+ if (tool_name === "Edit" || tool_name === "Write") {
1767
+ const filePath = tool_input.file_path ?? "";
1768
+ if (filePath && filePath.includes("/memory/") && filePath.endsWith(".md")) {
1769
+ const basename2 = filePath.split("/").pop() ?? "";
1770
+ if (basename2 !== "MEMORY.md") {
1771
+ ingestMemoryFile(db, session_id, filePath);
1772
+ }
1773
+ }
1774
+ }
1775
+ } catch (_memoryIngestErr) {
1776
+ }
1708
1777
  try {
1709
1778
  if (tool_name === "Edit" || tool_name === "Write") {
1710
1779
  const filePath = tool_input.file_path ?? "";
@@ -1768,9 +1837,9 @@ function readConventions(cwd) {
1768
1837
  try {
1769
1838
  const projectRoot = cwd ?? process.cwd();
1770
1839
  const configPath = join2(projectRoot, "massu.config.yaml");
1771
- if (!existsSync6(configPath)) return defaults;
1772
- const content = readFileSync5(configPath, "utf-8");
1773
- const parsed = parseYaml2(content);
1840
+ if (!existsSync7(configPath)) return defaults;
1841
+ const content = readFileSync6(configPath, "utf-8");
1842
+ const parsed = parseYaml3(content);
1774
1843
  if (!parsed || typeof parsed !== "object") return defaults;
1775
1844
  const conventions = parsed.conventions;
1776
1845
  if (!conventions || typeof conventions !== "object") return defaults;
@@ -1797,11 +1866,11 @@ function isKnowledgeSourceFile(filePath) {
1797
1866
  function checkMemoryFileIntegrity(filePath) {
1798
1867
  const issues = [];
1799
1868
  try {
1800
- if (!existsSync6(filePath)) {
1869
+ if (!existsSync7(filePath)) {
1801
1870
  issues.push("MEMORY.md file does not exist after write");
1802
1871
  return issues;
1803
1872
  }
1804
- const content = readFileSync5(filePath, "utf-8");
1873
+ const content = readFileSync6(filePath, "utf-8");
1805
1874
  const lines = content.split("\n");
1806
1875
  const MAX_LINES = 200;
1807
1876
  if (lines.length > MAX_LINES) {
@@ -951,6 +951,22 @@ async function main() {
951
951
  }
952
952
  } catch (_knowledgeErr) {
953
953
  }
954
+ try {
955
+ const significantSignals = ["fix", "implement", "migrate", "refactor", "debug", "decision", "chose", "architecture", "redesign", "rewrite"];
956
+ const promptLower = prompt.toLowerCase();
957
+ const signalCount = significantSignals.filter((s) => promptLower.includes(s)).length;
958
+ if (signalCount >= 2) {
959
+ const memoryFileCount = db.prepare(
960
+ "SELECT COUNT(*) as count FROM observations WHERE session_id = ? AND title LIKE '[memory-file] %'"
961
+ ).get(session_id);
962
+ if (memoryFileCount.count === 0) {
963
+ process.stderr.write(
964
+ "\n[MEMORY REMINDER] Significant work detected but no memory files have been written.\nConsider saving learnings to memory/*.md files for future sessions.\n\n"
965
+ );
966
+ }
967
+ }
968
+ } catch (_memoryNagErr) {
969
+ }
954
970
  } finally {
955
971
  db.close();
956
972
  }
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@massu/core",
3
- "version": "0.4.2",
3
+ "version": "0.6.0",
4
4
  "type": "module",
5
- "description": "AI Engineering Governance MCP Server - Session memory, knowledge system, feature registry, code intelligence, rule enforcement, tiered tooling (12 free / 72 total), 43 workflow commands",
5
+ "description": "AI Engineering Governance MCP Server - Session memory, knowledge system, feature registry, code intelligence, rule enforcement, tiered tooling (12 free / 72 total), 55+ workflow commands, 11 agents, 20+ patterns",
6
6
  "main": "src/server.ts",
7
7
  "bin": {
8
8
  "massu": "./dist/cli.js"
@@ -32,6 +32,10 @@
32
32
  "!src/__tests__/**",
33
33
  "dist/**/*",
34
34
  "commands/**/*",
35
+ "agents/**/*",
36
+ "patterns/**/*",
37
+ "protocols/**/*",
38
+ "reference/**/*",
35
39
  "LICENSE"
36
40
  ],
37
41
  "keywords": [
@@ -0,0 +1,302 @@
1
+ # Build Patterns
2
+
3
+ **Purpose**: Patterns for resolving build issues in Next.js applications with Prisma, native modules, and Edge Runtime.
4
+
5
+ **When to Read**: When encountering build errors, hangs, or warnings. Before adding heavy dependencies.
6
+
7
+ ---
8
+
9
+ ## Client/Server Code Separation
10
+
11
+ ### The Problem
12
+
13
+ PrismaClient gets bundled into browser JavaScript when imported in client components, causing:
14
+ - Build failures
15
+ - Massive bundle sizes
16
+ - Runtime errors
17
+
18
+ ### The Pattern: Domain Split Files
19
+
20
+ ```
21
+ src/lib/services/
22
+ ├── contacts-types.ts ← Types only (shared client + server)
23
+ ├── contacts-service.ts ← Server logic (PrismaClient, DB queries)
24
+ └── contacts-client.ts ← Client-safe utilities (formatting, validation)
25
+ ```
26
+
27
+ **Rule**: Files ending in `-service.ts` contain server code. Client components MUST NOT import them.
28
+
29
+ ```typescript
30
+ // contacts-types.ts — SAFE for client import
31
+ export interface Contact {
32
+ id: string;
33
+ name: string;
34
+ email: string;
35
+ }
36
+
37
+ export type ContactCreateInput = {
38
+ name: string;
39
+ email: string;
40
+ };
41
+
42
+ // contacts-service.ts — SERVER ONLY
43
+ import { db } from '@/lib/db';
44
+
45
+ export async function getContacts(): Promise<Contact[]> {
46
+ return db.contacts.findMany();
47
+ }
48
+ ```
49
+
50
+ ### Detection
51
+
52
+ ```bash
53
+ # Find client components importing server code
54
+ grep -rn "from '@/lib/db'" src/components/ --include="*.tsx"
55
+ # Expected: 0 matches
56
+
57
+ # Find barrel exports that leak server code
58
+ grep -rn "export \* from.*service" src/lib/
59
+ # Check if any client component imports the barrel
60
+ ```
61
+
62
+ ---
63
+
64
+ ## Native Module Externalization
65
+
66
+ ### The Problem
67
+
68
+ Modules like `jsdom`, `canvas`, `sharp`, and `puppeteer` contain native binaries that cannot be bundled by webpack/turbopack. They cause build hangs or failures.
69
+
70
+ ### The Pattern
71
+
72
+ **Step 1: Add to `serverExternalPackages` in `next.config.js`:**
73
+ ```javascript
74
+ /** @type {import('next').NextConfig} */
75
+ const nextConfig = {
76
+ serverExternalPackages: [
77
+ 'jsdom', 'canvas', 'sharp', 'puppeteer', 'puppeteer-core',
78
+ '@sparticuz/chromium', 'pdfkit', 'pdf-parse'
79
+ ],
80
+ };
81
+ ```
82
+
83
+ **Step 2: Add webpack externals as fallback:**
84
+ ```javascript
85
+ webpack: (config, { isServer }) => {
86
+ if (isServer) {
87
+ config.externals = config.externals || [];
88
+ config.externals.push({
89
+ jsdom: 'commonjs jsdom',
90
+ canvas: 'commonjs canvas',
91
+ sharp: 'commonjs sharp',
92
+ });
93
+ }
94
+ return config;
95
+ },
96
+ ```
97
+
98
+ **Step 3: Use dynamic imports:**
99
+ ```typescript
100
+ // WRONG - Static import causes build issues
101
+ import { JSDOM } from 'jsdom';
102
+
103
+ // CORRECT - Dynamic import
104
+ const { JSDOM } = await import('jsdom');
105
+ ```
106
+
107
+ ### Known Problematic Packages
108
+
109
+ | Package | Issue | Solution |
110
+ |---------|-------|---------|
111
+ | `jsdom` | ESM + native deps | Dynamic import + externalize |
112
+ | `canvas` | Native binary | Externalize |
113
+ | `sharp` | Native binary | serverExternalPackages |
114
+ | `puppeteer` | Chromium binary | Externalize + dynamic import |
115
+ | `cheerio` | Heavy dependency tree | Dynamic import |
116
+ | `pdfkit` | Native deps | Externalize |
117
+
118
+ ---
119
+
120
+ ## Edge Runtime Compatibility
121
+
122
+ ### The Rule
123
+
124
+ Files that run in Edge Runtime (middleware, edge functions) CANNOT use Node.js APIs.
125
+
126
+ **Banned in Edge Runtime:**
127
+ - `fs` / `path` / `crypto` (Node.js builtins)
128
+ - `pino` / `winston` (logging libraries with Node.js deps)
129
+ - `PrismaClient` (requires Node.js)
130
+ - Any package that imports Node.js builtins
131
+
132
+ ### Detection
133
+
134
+ ```bash
135
+ # Check middleware for Node.js imports
136
+ grep -rn "require('fs')\|require('path')\|require('crypto')" src/middleware.ts
137
+ # Expected: 0 matches
138
+
139
+ # Check for logging library imports in edge files
140
+ grep -rn "from 'pino'\|from 'winston'" src/middleware.ts
141
+ # Expected: 0 matches
142
+ ```
143
+
144
+ ### Alternative for Edge Runtime
145
+
146
+ ```typescript
147
+ // WRONG - pino in middleware
148
+ import pino from 'pino';
149
+
150
+ // CORRECT - console in middleware (Edge Runtime safe)
151
+ console.log('[middleware]', 'Processing request');
152
+
153
+ // CORRECT - Use Web Crypto API instead of Node crypto
154
+ const hash = await crypto.subtle.digest('SHA-256', data);
155
+ ```
156
+
157
+ ---
158
+
159
+ ## Import Chain Protection
160
+
161
+ ### The Problem
162
+
163
+ Even `import type` triggers webpack/turbopack analysis of the entire module graph. If a type-only import points to a file that imports heavy dependencies, the bundler will try to resolve them.
164
+
165
+ ### The Pattern
166
+
167
+ ```typescript
168
+ // WRONG - Type import still triggers analysis of heavy-deps.ts
169
+ import type { HeavyType } from '@/lib/heavy-deps';
170
+
171
+ // CORRECT - Inline the type definition
172
+ interface HeavyType {
173
+ id: string;
174
+ data: Record<string, unknown>;
175
+ }
176
+ ```
177
+
178
+ ### Detection
179
+
180
+ ```bash
181
+ # Find imports from known heavy modules in client code
182
+ grep -rn "from '@/lib/db'" src/components/ --include="*.tsx"
183
+ grep -rn "from '@/lib/services/.*-service'" src/components/ --include="*.tsx"
184
+ # Expected: 0 matches for both
185
+ ```
186
+
187
+ ---
188
+
189
+ ## Build Warnings
190
+
191
+ ### Zero-Warning Builds
192
+
193
+ All builds MUST produce zero warnings. Common warning sources and fixes:
194
+
195
+ | Warning | Cause | Fix |
196
+ |---------|-------|-----|
197
+ | CSS color deprecation | Hardcoded hex/rgb values | Use CSS variables: `var(--color-name)` |
198
+ | Unused variable | Dead code | Remove or prefix with `_` |
199
+ | Missing key prop | Map without key | Add `key={item.id}` |
200
+ | React hook deps | Missing useEffect dependency | Add to dependency array or wrap in useCallback |
201
+
202
+ ### Semantic Color Classes
203
+
204
+ Use semantic CSS classes instead of hardcoded colors:
205
+
206
+ ```css
207
+ /* WRONG */
208
+ .badge { color: #22c55e; }
209
+
210
+ /* CORRECT */
211
+ .badge-primary { color: var(--color-primary); }
212
+ .badge-success { color: var(--color-success); }
213
+ .badge-warning { color: var(--color-warning); }
214
+ .badge-error { color: var(--color-error); }
215
+ ```
216
+
217
+ ---
218
+
219
+ ## next-intl Setup
220
+
221
+ If using next-intl for internationalization, ALL three pieces are required:
222
+
223
+ 1. **Plugin in `next.config.js`:**
224
+ ```javascript
225
+ const withNextIntl = require('next-intl/plugin')();
226
+ module.exports = withNextIntl(nextConfig);
227
+ ```
228
+
229
+ 2. **`src/i18n/request.ts`:**
230
+ ```typescript
231
+ import { getRequestConfig } from 'next-intl/server';
232
+ export default getRequestConfig(async () => ({
233
+ locale: 'en',
234
+ messages: (await import(`../../messages/en.json`)).default
235
+ }));
236
+ ```
237
+
238
+ 3. **Provider in layout:**
239
+ ```typescript
240
+ import { NextIntlClientProvider } from 'next-intl';
241
+ <NextIntlClientProvider locale="en" messages={messages}>
242
+ {children}
243
+ </NextIntlClientProvider>
244
+ ```
245
+
246
+ Missing any one of these causes build failures.
247
+
248
+ ---
249
+
250
+ ## Suspense Boundaries
251
+
252
+ Pages using `use(params)` or `useSearchParams()` MUST be wrapped in Suspense:
253
+
254
+ ```typescript
255
+ // page.tsx
256
+ import { Suspense } from 'react';
257
+
258
+ export default function Page() {
259
+ return (
260
+ <Suspense fallback={<LoadingState />}>
261
+ <PageContent />
262
+ </Suspense>
263
+ );
264
+ }
265
+ ```
266
+
267
+ Without Suspense, static generation fails with cryptic errors.
268
+
269
+ ---
270
+
271
+ ## React Query v5 Callbacks
272
+
273
+ ```typescript
274
+ // WRONG - onSuccess removed in React Query v5
275
+ api.contacts.list.useQuery(undefined, {
276
+ onSuccess: (data) => setContacts(data), // TypeScript error
277
+ });
278
+
279
+ // CORRECT - Destructure data directly
280
+ const { data: contacts } = api.contacts.list.useQuery();
281
+ ```
282
+
283
+ See `patterns/component-patterns.md` for the full React Query v5 pattern.
284
+
285
+ ---
286
+
287
+ ## Quick Reference
288
+
289
+ | Rule | Pattern | Error if Violated |
290
+ |------|---------|-------------------|
291
+ | JSDOM dynamic import | `await import('jsdom')` | ESM error on Vercel |
292
+ | Cheerio dynamic import | `await import('cheerio')` | Build hang on Vercel |
293
+ | Import type from heavy deps | Inline types instead | Build hang (47+ min) |
294
+ | Client/Server boundary | No `@/lib/db` in client | PrismaClient bundled |
295
+ | next-intl setup | Plugin + request.ts + Provider | Build fails |
296
+ | Suspense boundaries | Wrap `use(params)` pages | Static generation fails |
297
+ | React Query v5 callbacks | Destructure data, no `onSuccess` in useQuery | TypeScript error |
298
+
299
+ ---
300
+
301
+ **Document Status**: ACTIVE
302
+ **Compliance**: Mandatory for all build-related work
@@ -0,0 +1,246 @@
1
+ # Component Patterns
2
+
3
+ **Purpose**: Standard patterns for UI components, forms, search inputs, and common interactive elements.
4
+
5
+ **When to Read**: Before creating or modifying UI components.
6
+
7
+ ---
8
+
9
+ ## Toast Notifications
10
+
11
+ ### Standard Pattern: Sonner
12
+
13
+ ```typescript
14
+ // CORRECT - Use Sonner directly
15
+ import { toast } from 'sonner';
16
+
17
+ // Success
18
+ toast.success('Changes saved successfully');
19
+
20
+ // Error
21
+ toast.error('Failed to save changes');
22
+
23
+ // With description
24
+ toast.success('Contact created', {
25
+ description: 'John Doe has been added to your contacts'
26
+ });
27
+
28
+ // Promise toast (loading → success/error)
29
+ toast.promise(mutation.mutateAsync(data), {
30
+ loading: 'Saving...',
31
+ success: 'Saved successfully',
32
+ error: 'Failed to save'
33
+ });
34
+ ```
35
+
36
+ ### WRONG - Deprecated useToast Hook
37
+
38
+ ```typescript
39
+ // WRONG - Legacy hook pattern, do NOT use
40
+ const { toast } = useToast();
41
+ toast({ title: 'Success', description: '...' });
42
+ ```
43
+
44
+ **Why Sonner**: Single import, consistent API, auto-dismiss, better DX.
45
+
46
+ ---
47
+
48
+ ## Loading State
49
+
50
+ ```typescript
51
+ import { LoadingState } from '@/components/common/LoadingState';
52
+
53
+ // In page or component
54
+ if (isLoading) {
55
+ return <LoadingState />;
56
+ }
57
+
58
+ // With custom message
59
+ if (isLoading) {
60
+ return <LoadingState message="Loading contacts..." />;
61
+ }
62
+ ```
63
+
64
+ **Rule**: ALL loading states MUST use the `LoadingState` component. Never use raw spinners or "Loading..." text.
65
+
66
+ ---
67
+
68
+ ## Empty State
69
+
70
+ ```typescript
71
+ import { EmptyState } from '@/components/common/EmptyState';
72
+
73
+ // Basic
74
+ if (data?.length === 0) {
75
+ return (
76
+ <EmptyState
77
+ title="No contacts found"
78
+ description="Add your first contact to get started"
79
+ action={{
80
+ label: "Add Contact",
81
+ onClick: () => setIsCreating(true)
82
+ }}
83
+ />
84
+ );
85
+ }
86
+ ```
87
+
88
+ **Rule**: ALL empty states MUST use the `EmptyState` component with actionable guidance.
89
+
90
+ ---
91
+
92
+ ## Form Inputs
93
+
94
+ ### Checkbox
95
+
96
+ ```typescript
97
+ import { Checkbox } from '@/components/ui/checkbox';
98
+
99
+ <div className="flex items-center space-x-2">
100
+ <Checkbox
101
+ id="agree"
102
+ checked={agreed}
103
+ onCheckedChange={setAgreed}
104
+ />
105
+ <label htmlFor="agree" className="text-sm">
106
+ I agree to the terms
107
+ </label>
108
+ </div>
109
+ ```
110
+
111
+ ### Select
112
+
113
+ ```typescript
114
+ import {
115
+ Select,
116
+ SelectContent,
117
+ SelectItem,
118
+ SelectTrigger,
119
+ SelectValue,
120
+ } from '@/components/ui/select';
121
+
122
+ // CRITICAL: Never use value="" - causes React crash
123
+ // Use __none__ for "no selection" option
124
+ <Select value={status} onValueChange={setStatus}>
125
+ <SelectTrigger>
126
+ <SelectValue placeholder="Select status" />
127
+ </SelectTrigger>
128
+ <SelectContent>
129
+ <SelectItem value="__none__">All Statuses</SelectItem>
130
+ <SelectItem value="active">Active</SelectItem>
131
+ <SelectItem value="inactive">Inactive</SelectItem>
132
+ </SelectContent>
133
+ </Select>
134
+ ```
135
+
136
+ ### DatePicker
137
+
138
+ ```typescript
139
+ import { DatePicker } from '@/components/ui/date-picker';
140
+
141
+ <DatePicker
142
+ date={selectedDate}
143
+ onSelect={setSelectedDate}
144
+ placeholder="Select date"
145
+ />
146
+ ```
147
+
148
+ ---
149
+
150
+ ## Search Input
151
+
152
+ ```typescript
153
+ // Standard search input with consistent styling
154
+ <Input
155
+ placeholder="Search..."
156
+ value={searchQuery}
157
+ onChange={(e) => setSearchQuery(e.target.value)}
158
+ className="page-search-input"
159
+ />
160
+ ```
161
+
162
+ **Rule**: ALL search inputs MUST use the `page-search-input` class for consistent styling.
163
+
164
+ ---
165
+
166
+ ## Query Keys (React Query / tRPC)
167
+
168
+ ```typescript
169
+ // CORRECT - Double brackets for tRPC query keys
170
+ queryKey: [['contacts', 'list']]
171
+
172
+ // WRONG - Single brackets
173
+ queryKey: ['contacts', 'list'] // Will not match tRPC invalidation
174
+ ```
175
+
176
+ **Why**: tRPC wraps query keys in an additional array. Using single brackets means `queryClient.invalidateQueries()` won't match.
177
+
178
+ ---
179
+
180
+ ## Form Validation (Zod + react-hook-form)
181
+
182
+ ```typescript
183
+ import { useForm } from 'react-hook-form';
184
+ import { zodResolver } from '@hookform/resolvers/zod';
185
+ import { z } from 'zod';
186
+
187
+ const schema = z.object({
188
+ name: z.string().min(1, 'Name is required'),
189
+ email: z.string().email('Invalid email'),
190
+ phone: z.string().optional(),
191
+ });
192
+
193
+ type FormData = z.infer<typeof schema>;
194
+
195
+ const {
196
+ register,
197
+ handleSubmit,
198
+ formState: { errors },
199
+ } = useForm<FormData>({
200
+ resolver: zodResolver(schema),
201
+ });
202
+ ```
203
+
204
+ **Rule**: ALL forms MUST use react-hook-form with Zod validation. See `patterns/form-patterns.md` for complete guide.
205
+
206
+ ---
207
+
208
+ ## ESLint Integration
209
+
210
+ Custom ESLint rules enforce component patterns:
211
+
212
+ | Rule | What It Catches |
213
+ |------|----------------|
214
+ | `no-deprecated-toast` | useToast() hook usage |
215
+ | `no-raw-loading` | Inline loading spinners instead of LoadingState |
216
+ | `no-empty-select-value` | `value=""` on Select.Item |
217
+ | `no-single-bracket-querykey` | Single bracket query keys |
218
+
219
+ ### Detection Commands
220
+
221
+ ```bash
222
+ # Check for deprecated toast usage
223
+ grep -rn "useToast" src/ --include="*.tsx" --include="*.ts"
224
+ # Expected: 0 matches (all should use sonner)
225
+
226
+ # Check for empty select values
227
+ grep -rn 'value=""' src/ --include="*.tsx"
228
+ # Expected: 0 matches
229
+
230
+ # Check for single bracket query keys
231
+ grep -rn "queryKey: \['" src/ --include="*.tsx" --include="*.ts"
232
+ # Expected: 0 matches (should be double brackets)
233
+ ```
234
+
235
+ ---
236
+
237
+ ## Related Documentation
238
+
239
+ - **Form Patterns**: `patterns/form-patterns.md`
240
+ - **UI Patterns**: `patterns/ui-patterns.md`
241
+ - **Display Patterns**: `patterns/display-patterns.md`
242
+
243
+ ---
244
+
245
+ **Document Status**: ACTIVE
246
+ **Compliance**: Mandatory for all component work