@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.
- package/README.md +40 -0
- package/agents/massu-architecture-reviewer.md +104 -0
- package/agents/massu-blast-radius-analyzer.md +84 -0
- package/agents/massu-competitive-scorer.md +126 -0
- package/agents/massu-help-sync.md +73 -0
- package/agents/massu-migration-writer.md +94 -0
- package/agents/massu-output-scorer.md +87 -0
- package/agents/massu-pattern-reviewer.md +84 -0
- package/agents/massu-plan-auditor.md +170 -0
- package/agents/massu-schema-sync-verifier.md +70 -0
- package/agents/massu-security-reviewer.md +98 -0
- package/agents/massu-ux-reviewer.md +106 -0
- package/commands/_shared-preamble.md +53 -23
- package/commands/_shared-references/auto-learning-protocol.md +71 -0
- package/commands/_shared-references/blast-radius-protocol.md +76 -0
- package/commands/_shared-references/security-pre-screen.md +64 -0
- package/commands/_shared-references/test-first-protocol.md +87 -0
- package/commands/_shared-references/verification-table.md +52 -0
- package/commands/massu-article-review.md +343 -0
- package/commands/massu-autoresearch/references/eval-runner.md +84 -0
- package/commands/massu-autoresearch/references/safety-rails.md +125 -0
- package/commands/massu-autoresearch/references/scoring-protocol.md +151 -0
- package/commands/massu-autoresearch.md +258 -0
- package/commands/massu-batch.md +44 -12
- package/commands/massu-bearings.md +42 -8
- package/commands/massu-checkpoint.md +588 -0
- package/commands/massu-ci-fix.md +2 -2
- package/commands/massu-command-health.md +132 -0
- package/commands/massu-command-improve.md +232 -0
- package/commands/massu-commit.md +205 -44
- package/commands/massu-create-plan.md +239 -57
- package/commands/massu-data/references/common-queries.md +79 -0
- package/commands/massu-data/references/table-guide.md +50 -0
- package/commands/massu-data.md +66 -0
- package/commands/massu-dead-code.md +29 -34
- package/commands/massu-debug/references/auto-learning.md +61 -0
- package/commands/massu-debug/references/codegraph-tracing.md +80 -0
- package/commands/massu-debug/references/common-shortcuts.md +98 -0
- package/commands/massu-debug/references/investigation-phases.md +294 -0
- package/commands/massu-debug/references/report-format.md +107 -0
- package/commands/massu-debug.md +105 -386
- package/commands/massu-docs.md +1 -1
- package/commands/massu-full-audit.md +61 -0
- package/commands/massu-gap-enhancement-analyzer.md +276 -16
- package/commands/massu-golden-path/references/approval-points.md +216 -0
- package/commands/massu-golden-path/references/competitive-mode.md +273 -0
- package/commands/massu-golden-path/references/error-handling.md +121 -0
- package/commands/massu-golden-path/references/phase-0-requirements.md +53 -0
- package/commands/massu-golden-path/references/phase-1-plan-creation.md +168 -0
- package/commands/massu-golden-path/references/phase-2-implementation.md +397 -0
- package/commands/massu-golden-path/references/phase-2.5-gap-analyzer.md +156 -0
- package/commands/massu-golden-path/references/phase-3-simplify.md +40 -0
- package/commands/massu-golden-path/references/phase-4-commit.md +94 -0
- package/commands/massu-golden-path/references/phase-5-push.md +116 -0
- package/commands/massu-golden-path/references/phase-5.5-production-verify.md +170 -0
- package/commands/massu-golden-path/references/phase-6-completion.md +113 -0
- package/commands/massu-golden-path/references/qa-evaluator-spec.md +137 -0
- package/commands/massu-golden-path/references/sprint-contract-protocol.md +117 -0
- package/commands/massu-golden-path/references/vr-visual-calibration.md +73 -0
- package/commands/massu-golden-path.md +114 -848
- package/commands/massu-guide.md +72 -69
- package/commands/massu-hooks.md +27 -12
- package/commands/massu-hotfix.md +221 -144
- package/commands/massu-incident.md +49 -20
- package/commands/massu-infra-audit.md +187 -0
- package/commands/massu-learning-audit.md +211 -0
- package/commands/massu-loop/references/auto-learning.md +49 -0
- package/commands/massu-loop/references/checkpoint-audit.md +40 -0
- package/commands/massu-loop/references/guardrails.md +17 -0
- package/commands/massu-loop/references/iteration-structure.md +115 -0
- package/commands/massu-loop/references/loop-controller.md +188 -0
- package/commands/massu-loop/references/plan-extraction.md +78 -0
- package/commands/massu-loop/references/vr-plan-spec.md +140 -0
- package/commands/massu-loop-playwright.md +9 -9
- package/commands/massu-loop.md +115 -670
- package/commands/massu-new-pattern.md +423 -0
- package/commands/massu-perf.md +422 -0
- package/commands/massu-plan-audit.md +1 -1
- package/commands/massu-plan.md +389 -122
- package/commands/massu-production-verify.md +433 -0
- package/commands/massu-push.md +62 -378
- package/commands/massu-recap.md +29 -3
- package/commands/massu-rollback.md +613 -0
- package/commands/massu-scaffold-hook.md +2 -4
- package/commands/massu-scaffold-page.md +2 -3
- package/commands/massu-scaffold-router.md +1 -2
- package/commands/massu-security.md +619 -0
- package/commands/massu-simplify.md +115 -85
- package/commands/massu-squirrels.md +2 -2
- package/commands/massu-tdd.md +38 -22
- package/commands/massu-test.md +3 -3
- package/commands/massu-type-mismatch-audit.md +469 -0
- package/commands/massu-ui-audit.md +587 -0
- package/commands/massu-verify-playwright.md +287 -32
- package/commands/massu-verify.md +150 -46
- package/dist/cli.js +1451 -1047
- package/dist/hooks/post-tool-use.js +75 -6
- package/dist/hooks/user-prompt.js +16 -0
- package/package.json +6 -2
- package/patterns/build-patterns.md +302 -0
- package/patterns/component-patterns.md +246 -0
- package/patterns/display-patterns.md +185 -0
- package/patterns/form-patterns.md +890 -0
- package/patterns/integration-testing-checklist.md +445 -0
- package/patterns/security-patterns.md +219 -0
- package/patterns/testing-patterns.md +569 -0
- package/patterns/tool-routing.md +81 -0
- package/patterns/ui-patterns.md +371 -0
- package/protocols/plan-implementation.md +267 -0
- package/protocols/recovery.md +225 -0
- package/protocols/verification.md +404 -0
- package/reference/command-taxonomy.md +178 -0
- package/reference/cr-rules-reference.md +76 -0
- package/reference/hook-execution-order.md +148 -0
- package/reference/lessons-learned.md +175 -0
- package/reference/patterns-quickref.md +208 -0
- package/reference/standards.md +135 -0
- package/reference/subagents-reference.md +17 -0
- package/reference/vr-verification-reference.md +867 -0
- package/src/commands/init.ts +27 -0
- package/src/commands/install-commands.ts +149 -53
- package/src/hooks/post-tool-use.ts +17 -0
- package/src/hooks/user-prompt.ts +21 -0
- package/src/memory-file-ingest.ts +127 -0
- 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
|
|
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 (!
|
|
1772
|
-
const content =
|
|
1773
|
-
const parsed =
|
|
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 (!
|
|
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 =
|
|
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.
|
|
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),
|
|
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
|