@tonycasey/lisa 0.5.13
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 +42 -0
- package/dist/cli.js +390 -0
- package/dist/lib/interfaces/IDockerClient.js +2 -0
- package/dist/lib/interfaces/IMcpClient.js +2 -0
- package/dist/lib/interfaces/IServices.js +2 -0
- package/dist/lib/interfaces/ITemplateCopier.js +2 -0
- package/dist/lib/mcp.js +35 -0
- package/dist/lib/services.js +57 -0
- package/dist/package.json +36 -0
- package/dist/templates/agents/.sample.env +12 -0
- package/dist/templates/agents/docs/STORAGE_SETUP.md +161 -0
- package/dist/templates/agents/skills/common/group-id.js +193 -0
- package/dist/templates/agents/skills/init-review/SKILL.md +119 -0
- package/dist/templates/agents/skills/init-review/scripts/ai-enrich.js +258 -0
- package/dist/templates/agents/skills/init-review/scripts/init-review.js +769 -0
- package/dist/templates/agents/skills/lisa/SKILL.md +92 -0
- package/dist/templates/agents/skills/lisa/cache/.gitkeep +0 -0
- package/dist/templates/agents/skills/lisa/scripts/storage.js +374 -0
- package/dist/templates/agents/skills/memory/SKILL.md +31 -0
- package/dist/templates/agents/skills/memory/scripts/memory.js +533 -0
- package/dist/templates/agents/skills/prompt/SKILL.md +19 -0
- package/dist/templates/agents/skills/prompt/scripts/prompt.js +184 -0
- package/dist/templates/agents/skills/tasks/SKILL.md +31 -0
- package/dist/templates/agents/skills/tasks/scripts/tasks.js +489 -0
- package/dist/templates/claude/config.js +40 -0
- package/dist/templates/claude/hooks/README.md +158 -0
- package/dist/templates/claude/hooks/common/complexity-rater.js +290 -0
- package/dist/templates/claude/hooks/common/context.js +263 -0
- package/dist/templates/claude/hooks/common/group-id.js +188 -0
- package/dist/templates/claude/hooks/common/mcp-client.js +131 -0
- package/dist/templates/claude/hooks/common/transcript-parser.js +256 -0
- package/dist/templates/claude/hooks/common/zep-client.js +175 -0
- package/dist/templates/claude/hooks/session-start.js +401 -0
- package/dist/templates/claude/hooks/session-stop-worker.js +341 -0
- package/dist/templates/claude/hooks/session-stop.js +122 -0
- package/dist/templates/claude/hooks/user-prompt-submit.js +256 -0
- package/dist/templates/claude/settings.json +46 -0
- package/dist/templates/docker/.env.lisa.example +17 -0
- package/dist/templates/docker/docker-compose.graphiti.yml +45 -0
- package/dist/templates/rules/shared/clean-architecture.md +333 -0
- package/dist/templates/rules/shared/code-quality-rules.md +469 -0
- package/dist/templates/rules/shared/git-rules.md +64 -0
- package/dist/templates/rules/shared/testing-principles.md +469 -0
- package/dist/templates/rules/typescript/coding-standards.md +751 -0
- package/dist/templates/rules/typescript/testing.md +629 -0
- package/dist/templates/rules/typescript/typescript-config-guide.md +465 -0
- package/package.json +64 -0
- package/scripts/postinstall.js +710 -0
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
/**
|
|
5
|
+
* Claude Code - User Prompt Submit Hook
|
|
6
|
+
*
|
|
7
|
+
* This hook runs before a user's prompt is submitted to Claude.
|
|
8
|
+
* It can be used to validate, enhance, or log prompts.
|
|
9
|
+
*
|
|
10
|
+
* Configuration: .claude/settings.json -> hooks.UserPromptSubmit
|
|
11
|
+
*
|
|
12
|
+
* Note: This hook is optional and only runs if configured.
|
|
13
|
+
* If the hook exits with non-zero status, the prompt submission is cancelled.
|
|
14
|
+
*/
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
const path = require('path');
|
|
17
|
+
const { spawn } = require('child_process');
|
|
18
|
+
const { PROJECT_ROOT, PROMPT_SKILL_PATH, DEV_DIR } = require('../config');
|
|
19
|
+
/**
|
|
20
|
+
* Validate prompt for potentially problematic patterns
|
|
21
|
+
*/
|
|
22
|
+
function validatePrompt(prompt) {
|
|
23
|
+
const warnings = [];
|
|
24
|
+
// Check for overly broad requests
|
|
25
|
+
if (prompt.toLowerCase().includes('delete all') ||
|
|
26
|
+
prompt.toLowerCase().includes('remove everything')) {
|
|
27
|
+
warnings.push(' Destructive operation detected - please be specific');
|
|
28
|
+
}
|
|
29
|
+
// Check for requests without context
|
|
30
|
+
if (prompt.length < 10) {
|
|
31
|
+
warnings.push(' Very short prompt - consider providing more context');
|
|
32
|
+
}
|
|
33
|
+
return warnings;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Enhance prompt with project context if needed
|
|
37
|
+
*/
|
|
38
|
+
function enhancePrompt(prompt) {
|
|
39
|
+
const suggestions = [];
|
|
40
|
+
if (prompt.toLowerCase().includes('architecture') ||
|
|
41
|
+
prompt.toLowerCase().includes('structure')) {
|
|
42
|
+
const archPath = path.join(DEV_DIR, 'architecture.md');
|
|
43
|
+
if (fs.existsSync(archPath)) {
|
|
44
|
+
suggestions.push(' Consider referencing @.dev/architecture.md for context');
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (prompt.toLowerCase().includes('todo') ||
|
|
48
|
+
prompt.toLowerCase().includes('task')) {
|
|
49
|
+
const todoPath = path.join(DEV_DIR, 'todo.md');
|
|
50
|
+
if (fs.existsSync(todoPath)) {
|
|
51
|
+
suggestions.push(' Your todo list is available at @.dev/todo.md');
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return suggestions;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Log prompt for analytics (optional)
|
|
58
|
+
*/
|
|
59
|
+
function logPrompt(prompt) {
|
|
60
|
+
const logPath = path.join(DEV_DIR, '.prompt-log.jsonl');
|
|
61
|
+
const logEntry = {
|
|
62
|
+
timestamp: new Date().toISOString(),
|
|
63
|
+
promptLength: prompt.length,
|
|
64
|
+
promptPreview: prompt.substring(0, 100)
|
|
65
|
+
};
|
|
66
|
+
try {
|
|
67
|
+
fs.appendFileSync(logPath, JSON.stringify(logEntry) + '\n');
|
|
68
|
+
}
|
|
69
|
+
catch (_error) {
|
|
70
|
+
// Silently fail if logging doesn't work
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Check if Graphiti MCP server is available
|
|
75
|
+
* Returns true if server responds, false otherwise
|
|
76
|
+
*/
|
|
77
|
+
async function isGraphitiAvailable() {
|
|
78
|
+
// Read endpoint from .env file (same logic as prompt skill)
|
|
79
|
+
const envPath = path.join(PROJECT_ROOT, '.agents', 'skills', '.env');
|
|
80
|
+
let endpoint = 'http://localhost:8010/mcp/';
|
|
81
|
+
try {
|
|
82
|
+
if (fs.existsSync(envPath)) {
|
|
83
|
+
const raw = fs.readFileSync(envPath, 'utf8');
|
|
84
|
+
const lines = raw.split(/\r?\n/);
|
|
85
|
+
for (const line of lines) {
|
|
86
|
+
if (line.startsWith('GRAPHITI_ENDPOINT=')) {
|
|
87
|
+
endpoint = line.slice('GRAPHITI_ENDPOINT='.length).trim();
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
// Use default endpoint
|
|
95
|
+
}
|
|
96
|
+
try {
|
|
97
|
+
const controller = new AbortController();
|
|
98
|
+
const timeoutId = setTimeout(() => controller.abort(), 2000);
|
|
99
|
+
const response = await fetch(endpoint, {
|
|
100
|
+
method: 'POST',
|
|
101
|
+
headers: { 'Content-Type': 'application/json' },
|
|
102
|
+
body: JSON.stringify({
|
|
103
|
+
jsonrpc: '2.0',
|
|
104
|
+
id: 'health',
|
|
105
|
+
method: 'ping',
|
|
106
|
+
params: {}
|
|
107
|
+
}),
|
|
108
|
+
signal: controller.signal
|
|
109
|
+
});
|
|
110
|
+
clearTimeout(timeoutId);
|
|
111
|
+
return response.ok || response.status === 400; // 400 means server is up but method not found
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Store prompt to Graphiti MCP for cross-session memory
|
|
119
|
+
* Graphiti's LLM automatically classifies the content as:
|
|
120
|
+
* - KeyDecision, DirectionChange, ArchitecturalChoice, Preference, etc.
|
|
121
|
+
* Classification happens server-side based on entity_types in config.yaml
|
|
122
|
+
*/
|
|
123
|
+
async function storeToGraphiti(prompt) {
|
|
124
|
+
if (!fs.existsSync(PROMPT_SKILL_PATH)) {
|
|
125
|
+
return null; // Silently skip if skill not found
|
|
126
|
+
}
|
|
127
|
+
// Check if Graphiti is available before attempting storage
|
|
128
|
+
const available = await isGraphitiAvailable();
|
|
129
|
+
if (!available) {
|
|
130
|
+
return { status: 'unavailable' };
|
|
131
|
+
}
|
|
132
|
+
return new Promise((resolve) => {
|
|
133
|
+
const child = spawn('node', [
|
|
134
|
+
PROMPT_SKILL_PATH,
|
|
135
|
+
'--text', prompt,
|
|
136
|
+
'--role', 'user',
|
|
137
|
+
'--source', 'user-prompt' // Graphiti LLM classifies content automatically
|
|
138
|
+
], {
|
|
139
|
+
cwd: PROJECT_ROOT,
|
|
140
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
141
|
+
});
|
|
142
|
+
let stdout = '';
|
|
143
|
+
let stderr = '';
|
|
144
|
+
child.stdout.on('data', (data) => { stdout += data; });
|
|
145
|
+
child.stderr.on('data', (data) => { stderr += data; });
|
|
146
|
+
child.on('close', (code) => {
|
|
147
|
+
if (code === 0) {
|
|
148
|
+
try {
|
|
149
|
+
const result = JSON.parse(stdout);
|
|
150
|
+
resolve(result);
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
resolve({ status: 'ok', raw: stdout.trim() });
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
// Check if it's a connection error
|
|
158
|
+
const errorMsg = stderr.trim() || `exit code ${code}`;
|
|
159
|
+
if (errorMsg.includes('fetch failed') || errorMsg.includes('ECONNREFUSED')) {
|
|
160
|
+
resolve({ status: 'unavailable' });
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
resolve({ status: 'error', error: errorMsg });
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
child.on('error', (err) => {
|
|
168
|
+
if (err.message.includes('fetch failed') || err.message.includes('ECONNREFUSED')) {
|
|
169
|
+
resolve({ status: 'unavailable' });
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
resolve({ status: 'error', error: err.message });
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
// Timeout after 5 seconds
|
|
176
|
+
setTimeout(() => {
|
|
177
|
+
child.kill();
|
|
178
|
+
resolve({ status: 'timeout' });
|
|
179
|
+
}, 5000);
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Main execution - reads JSON from stdin (Claude Code hook protocol)
|
|
184
|
+
*/
|
|
185
|
+
function main() {
|
|
186
|
+
let inputData = '';
|
|
187
|
+
// Read JSON from stdin
|
|
188
|
+
process.stdin.on('data', (chunk) => {
|
|
189
|
+
inputData += chunk;
|
|
190
|
+
});
|
|
191
|
+
process.stdin.on('end', async () => {
|
|
192
|
+
try {
|
|
193
|
+
const hookInput = JSON.parse(inputData);
|
|
194
|
+
const prompt = hookInput.prompt || '';
|
|
195
|
+
if (!prompt) {
|
|
196
|
+
console.log(' No prompt in hook input');
|
|
197
|
+
process.exit(0);
|
|
198
|
+
}
|
|
199
|
+
// Log what we captured for debugging
|
|
200
|
+
console.log(`Captured prompt (${prompt.length} chars): "${prompt.substring(0, 80)}${prompt.length > 80 ? '...' : ''}"`);
|
|
201
|
+
// Validate prompt
|
|
202
|
+
const warnings = validatePrompt(prompt);
|
|
203
|
+
if (warnings.length > 0) {
|
|
204
|
+
warnings.forEach(warning => console.log(warning));
|
|
205
|
+
}
|
|
206
|
+
// Enhance prompt with suggestions
|
|
207
|
+
const suggestions = enhancePrompt(prompt);
|
|
208
|
+
if (suggestions.length > 0) {
|
|
209
|
+
suggestions.forEach(suggestion => console.log(suggestion));
|
|
210
|
+
}
|
|
211
|
+
// Log prompt for analytics
|
|
212
|
+
logPrompt(prompt);
|
|
213
|
+
// Store to Graphiti MCP for cross-session memory
|
|
214
|
+
const graphitiResult = await storeToGraphiti(prompt);
|
|
215
|
+
if (graphitiResult) {
|
|
216
|
+
if (graphitiResult.status === 'ok') {
|
|
217
|
+
// Successfully stored - no need to show message
|
|
218
|
+
}
|
|
219
|
+
else if (graphitiResult.status === 'skipped') {
|
|
220
|
+
// Duplicate - silently skip
|
|
221
|
+
}
|
|
222
|
+
else if (graphitiResult.status === 'unavailable') {
|
|
223
|
+
console.log(' lisa works but needs further setup to persist her memory');
|
|
224
|
+
}
|
|
225
|
+
else if (graphitiResult.status === 'error') {
|
|
226
|
+
// Only show actual errors, not connection issues
|
|
227
|
+
console.log(' lisa works but needs further setup to persist her memory');
|
|
228
|
+
}
|
|
229
|
+
else if (graphitiResult.status === 'timeout') {
|
|
230
|
+
console.log(' lisa works but needs further setup to persist her memory');
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
// Exit with 0 to allow prompt to proceed
|
|
234
|
+
process.exit(0);
|
|
235
|
+
}
|
|
236
|
+
catch (error) {
|
|
237
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
238
|
+
console.error(` Failed to parse hook input: ${message}`);
|
|
239
|
+
// Exit with 0 to not block the prompt on errors
|
|
240
|
+
process.exit(0);
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
// Run if called directly
|
|
245
|
+
if (require.main === module) {
|
|
246
|
+
try {
|
|
247
|
+
main();
|
|
248
|
+
}
|
|
249
|
+
catch (error) {
|
|
250
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
251
|
+
console.error(` Prompt validation failed: ${message}`);
|
|
252
|
+
// Exit with 0 to not block the prompt
|
|
253
|
+
process.exit(0);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
module.exports = { validatePrompt, enhancePrompt, logPrompt };
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"workflowsDirectory": ".claude/workflows",
|
|
3
|
+
"autoLoadRules": true,
|
|
4
|
+
"hooks": {
|
|
5
|
+
"SessionStart": [
|
|
6
|
+
{
|
|
7
|
+
"hooks": [
|
|
8
|
+
{
|
|
9
|
+
"type": "command",
|
|
10
|
+
"command": "node .claude/hooks/session-start.js"
|
|
11
|
+
}
|
|
12
|
+
]
|
|
13
|
+
}
|
|
14
|
+
],
|
|
15
|
+
"Stop": [
|
|
16
|
+
{
|
|
17
|
+
"hooks": [
|
|
18
|
+
{
|
|
19
|
+
"type": "command",
|
|
20
|
+
"command": "node .claude/hooks/session-stop.js"
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
}
|
|
24
|
+
],
|
|
25
|
+
"UserPromptSubmit": [
|
|
26
|
+
{
|
|
27
|
+
"hooks": [
|
|
28
|
+
{
|
|
29
|
+
"type": "command",
|
|
30
|
+
"command": "node .claude/hooks/user-prompt-submit.js"
|
|
31
|
+
}
|
|
32
|
+
]
|
|
33
|
+
}
|
|
34
|
+
]
|
|
35
|
+
},
|
|
36
|
+
"excludePatterns": [
|
|
37
|
+
"**/node_modules/**",
|
|
38
|
+
"**/dist/**",
|
|
39
|
+
"**/build/**",
|
|
40
|
+
"**/lib/**",
|
|
41
|
+
"**/.git/**",
|
|
42
|
+
"**/coverage/**",
|
|
43
|
+
"**/.next/**",
|
|
44
|
+
"**/.nuxt/**"
|
|
45
|
+
]
|
|
46
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Lisa Memory Settings
|
|
2
|
+
# Copy this file to .env in your project root and fill in the values
|
|
3
|
+
|
|
4
|
+
# OpenAI API key (required for entity extraction)
|
|
5
|
+
OPENAI_API_KEY=sk-...
|
|
6
|
+
|
|
7
|
+
# Neo4j credentials
|
|
8
|
+
NEO4J_USER=neo4j
|
|
9
|
+
NEO4J_PASSWORD=demodemo
|
|
10
|
+
NEO4J_DATABASE=neo4j
|
|
11
|
+
|
|
12
|
+
# Graphiti MCP
|
|
13
|
+
# Group ID defaults to project name from package.json or directory name
|
|
14
|
+
GRAPHITI_GROUP_ID=your-project-name
|
|
15
|
+
GRAPHITI_ENDPOINT=http://localhost:8010/mcp/
|
|
16
|
+
SEMAPHORE_LIMIT=10
|
|
17
|
+
USE_PARALLEL_RUNTIME=false
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
name: lisa
|
|
2
|
+
|
|
3
|
+
services:
|
|
4
|
+
neo4j:
|
|
5
|
+
image: neo4j:5.26.2
|
|
6
|
+
hostname: neo4j
|
|
7
|
+
ports:
|
|
8
|
+
- "7474:7474"
|
|
9
|
+
- "7687:7687"
|
|
10
|
+
environment:
|
|
11
|
+
- NEO4J_AUTH=${NEO4J_USER:-neo4j}/${NEO4J_PASSWORD:-demodemo}
|
|
12
|
+
volumes:
|
|
13
|
+
- neo4j_data:/data
|
|
14
|
+
- neo4j_logs:/logs
|
|
15
|
+
restart: always
|
|
16
|
+
healthcheck:
|
|
17
|
+
test: ["CMD-SHELL", "wget -qO- http://localhost:7474 || exit 1"]
|
|
18
|
+
interval: 5s
|
|
19
|
+
timeout: 5s
|
|
20
|
+
retries: 5
|
|
21
|
+
start_period: 15s
|
|
22
|
+
|
|
23
|
+
graphiti-mcp:
|
|
24
|
+
image: zepai/knowledge-graph-mcp:latest
|
|
25
|
+
depends_on:
|
|
26
|
+
neo4j:
|
|
27
|
+
condition: service_healthy
|
|
28
|
+
env_file:
|
|
29
|
+
- path: ../.env
|
|
30
|
+
required: false
|
|
31
|
+
environment:
|
|
32
|
+
# NEO4J_URI must point to container hostname, not localhost
|
|
33
|
+
- NEO4J_URI=bolt://neo4j:7687
|
|
34
|
+
ports:
|
|
35
|
+
- "8010:8000"
|
|
36
|
+
restart: always
|
|
37
|
+
healthcheck:
|
|
38
|
+
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
|
|
39
|
+
interval: 10s
|
|
40
|
+
timeout: 5s
|
|
41
|
+
retries: 3
|
|
42
|
+
|
|
43
|
+
volumes:
|
|
44
|
+
neo4j_data:
|
|
45
|
+
neo4j_logs:
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
# Clean Architecture Principles
|
|
2
|
+
|
|
3
|
+
**Note:** These principles apply to ALL programming languages. See your language-specific rules for implementation details.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
Clean Architecture is a software design philosophy that emphasizes separation of concerns and dependency management. The core idea is to organize code into layers, with dependencies flowing inward toward the core business logic.
|
|
8
|
+
|
|
9
|
+
## Layer Structure
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
src/
|
|
13
|
+
├── domain/ # Core business entities and rules
|
|
14
|
+
│ ├── entities/ # Business entities
|
|
15
|
+
│ ├── errors/ # Domain-specific errors
|
|
16
|
+
│ └── interfaces/ # Contracts (interfaces/protocols/traits)
|
|
17
|
+
├── application/ # Business logic and use cases
|
|
18
|
+
│ ├── interfaces/ # Application service contracts
|
|
19
|
+
│ └── services/ # Use case implementations
|
|
20
|
+
└── infrastructure/ # External concerns
|
|
21
|
+
├── repositories/ # Data access implementations
|
|
22
|
+
└── services/ # External API integrations
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Layer Responsibilities
|
|
26
|
+
|
|
27
|
+
### 1. Domain Layer (`src/domain/`)
|
|
28
|
+
|
|
29
|
+
**Purpose:** Contains the core business rules and entities with zero dependencies on external frameworks or libraries.
|
|
30
|
+
|
|
31
|
+
**What belongs here:**
|
|
32
|
+
- Core business entities (Product, Order, User, etc.)
|
|
33
|
+
- Business rules and validation logic
|
|
34
|
+
- Domain errors and exceptions
|
|
35
|
+
- Repository interfaces (contracts for data access)
|
|
36
|
+
- Core service interfaces (business logic contracts)
|
|
37
|
+
|
|
38
|
+
**Dependencies:** NONE - Domain depends on nothing else
|
|
39
|
+
|
|
40
|
+
**Examples across languages:**
|
|
41
|
+
- Entities: `Product`, `Order`, `Customer`
|
|
42
|
+
- Interfaces: Repository contracts, service contracts
|
|
43
|
+
- Errors: `ProductNotFoundError`, `InvalidOrderStateError`
|
|
44
|
+
|
|
45
|
+
### 2. Application Layer (`src/application/`)
|
|
46
|
+
|
|
47
|
+
**Purpose:** Orchestrates the domain layer and coordinates infrastructure to implement use cases.
|
|
48
|
+
|
|
49
|
+
**What belongs here:**
|
|
50
|
+
- Use case implementations
|
|
51
|
+
- Application service interfaces
|
|
52
|
+
- Application-specific logic
|
|
53
|
+
- Coordination between domain and infrastructure
|
|
54
|
+
|
|
55
|
+
**Dependencies:** Can depend on Domain layer ONLY
|
|
56
|
+
|
|
57
|
+
**Examples:**
|
|
58
|
+
- Services: `OrderService`, `CartService`, `CheckoutService`
|
|
59
|
+
- Use cases: `CreateOrderUseCase`, `ProcessPaymentUseCase`
|
|
60
|
+
- Configurations: `CheckoutConfig`, `PaymentConfig`
|
|
61
|
+
|
|
62
|
+
### 3. Infrastructure Layer (`src/infrastructure/`)
|
|
63
|
+
|
|
64
|
+
**Purpose:** Handles all interactions with external systems, databases, APIs, and frameworks.
|
|
65
|
+
|
|
66
|
+
**What belongs here:**
|
|
67
|
+
- Repository implementations (database access)
|
|
68
|
+
- External API clients
|
|
69
|
+
- Framework-specific code
|
|
70
|
+
- Database connections
|
|
71
|
+
- Third-party service integrations
|
|
72
|
+
|
|
73
|
+
**Dependencies:** Can depend on both Application and Domain layers
|
|
74
|
+
|
|
75
|
+
**Examples:**
|
|
76
|
+
- Repositories: `ProductRepository`, `OrderRepository`
|
|
77
|
+
- Services: `DatabaseService`, `PaymentGatewayService`
|
|
78
|
+
- Clients: `EmailServiceClient`, `SMSServiceClient`
|
|
79
|
+
|
|
80
|
+
## Dependency Rules
|
|
81
|
+
|
|
82
|
+
### The Dependency Rule
|
|
83
|
+
|
|
84
|
+
**Dependencies MUST point inward:**
|
|
85
|
+
|
|
86
|
+
```
|
|
87
|
+
Infrastructure → Application → Domain
|
|
88
|
+
↑
|
|
89
|
+
Core Business
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
- **Domain** has NO dependencies
|
|
93
|
+
- **Application** depends on Domain only
|
|
94
|
+
- **Infrastructure** depends on Application and Domain
|
|
95
|
+
|
|
96
|
+
### Why This Matters
|
|
97
|
+
|
|
98
|
+
1. **Testability**: Domain can be tested without databases or external services
|
|
99
|
+
2. **Flexibility**: Swap infrastructure without changing business logic
|
|
100
|
+
3. **Maintainability**: Changes to external systems don't affect core business rules
|
|
101
|
+
4. **Independence**: Business logic is independent of frameworks and tools
|
|
102
|
+
|
|
103
|
+
## Dependency Inversion Principle
|
|
104
|
+
|
|
105
|
+
The key to Clean Architecture is **Dependency Inversion**:
|
|
106
|
+
|
|
107
|
+
- High-level modules should not depend on low-level modules
|
|
108
|
+
- Both should depend on abstractions (interfaces/contracts)
|
|
109
|
+
- Abstractions should not depend on details
|
|
110
|
+
- Details should depend on abstractions
|
|
111
|
+
|
|
112
|
+
### Example Flow
|
|
113
|
+
|
|
114
|
+
```
|
|
115
|
+
OrderService (Application)
|
|
116
|
+
↓ depends on
|
|
117
|
+
IOrderRepository (Domain interface)
|
|
118
|
+
↑ implemented by
|
|
119
|
+
OrderRepository (Infrastructure)
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
The service depends on the interface, not the implementation. The implementation depends on the interface.
|
|
123
|
+
|
|
124
|
+
## Repository Pattern
|
|
125
|
+
|
|
126
|
+
The Repository pattern mediates between the domain and data mapping layers.
|
|
127
|
+
|
|
128
|
+
### Key Concepts
|
|
129
|
+
|
|
130
|
+
1. **Interface in Domain**: Repository contracts live in domain layer
|
|
131
|
+
2. **Implementation in Infrastructure**: Actual data access in infrastructure
|
|
132
|
+
3. **Services use Interfaces**: Application services depend on interfaces, not implementations
|
|
133
|
+
4. **Centralized Data Access**: All data operations go through repositories
|
|
134
|
+
|
|
135
|
+
### Why Use Repositories
|
|
136
|
+
|
|
137
|
+
- Decouples business logic from data storage
|
|
138
|
+
- Makes testing easier (mock repositories)
|
|
139
|
+
- Enables swapping data sources (SQL → NoSQL)
|
|
140
|
+
- Centralizes data access patterns
|
|
141
|
+
- Provides clear separation of concerns
|
|
142
|
+
|
|
143
|
+
## Dependency Injection
|
|
144
|
+
|
|
145
|
+
All dependencies should be injected, not instantiated.
|
|
146
|
+
|
|
147
|
+
### Constructor Injection
|
|
148
|
+
|
|
149
|
+
Dependencies are provided through the constructor, making them explicit and testable.
|
|
150
|
+
|
|
151
|
+
**Benefits:**
|
|
152
|
+
- Explicit dependencies
|
|
153
|
+
- Easy to test (inject mocks)
|
|
154
|
+
- Promotes loose coupling
|
|
155
|
+
- Enables swapping implementations
|
|
156
|
+
|
|
157
|
+
### Registration
|
|
158
|
+
|
|
159
|
+
All services and repositories must be registered in a dependency injection container.
|
|
160
|
+
|
|
161
|
+
**Location:** `src/infrastructure/di/`
|
|
162
|
+
|
|
163
|
+
## SOLID Principles
|
|
164
|
+
|
|
165
|
+
### Single Responsibility Principle (SRP)
|
|
166
|
+
|
|
167
|
+
**Definition:** Each class should have only one reason to change.
|
|
168
|
+
|
|
169
|
+
**Application:**
|
|
170
|
+
- Each repository handles one entity
|
|
171
|
+
- Each service handles one use case or related group
|
|
172
|
+
- Keep classes focused and cohesive
|
|
173
|
+
|
|
174
|
+
### Open/Closed Principle (OCP)
|
|
175
|
+
|
|
176
|
+
**Definition:** Open for extension, closed for modification.
|
|
177
|
+
|
|
178
|
+
**Application:**
|
|
179
|
+
- Use interfaces to allow extension
|
|
180
|
+
- Compose behavior rather than modifying existing code
|
|
181
|
+
- Use dependency injection to swap implementations
|
|
182
|
+
|
|
183
|
+
### Liskov Substitution Principle (LSP)
|
|
184
|
+
|
|
185
|
+
**Definition:** Derived classes must be substitutable for their base classes.
|
|
186
|
+
|
|
187
|
+
**Application:**
|
|
188
|
+
- Implementations must honor interface contracts
|
|
189
|
+
- No unexpected behavior in derived classes
|
|
190
|
+
- Maintain consistent semantics
|
|
191
|
+
|
|
192
|
+
### Interface Segregation Principle (ISP)
|
|
193
|
+
|
|
194
|
+
**Definition:** Clients should not depend on interfaces they don't use.
|
|
195
|
+
|
|
196
|
+
**Application:**
|
|
197
|
+
- Create small, focused interfaces
|
|
198
|
+
- Split large interfaces into smaller ones
|
|
199
|
+
- Each interface should represent one capability
|
|
200
|
+
|
|
201
|
+
### Dependency Inversion Principle (DIP)
|
|
202
|
+
|
|
203
|
+
**Definition:** Depend on abstractions, not concretions.
|
|
204
|
+
|
|
205
|
+
**Application:**
|
|
206
|
+
- Services depend on repository interfaces
|
|
207
|
+
- Use dependency injection
|
|
208
|
+
- Infrastructure implements domain interfaces
|
|
209
|
+
|
|
210
|
+
## Common Violations
|
|
211
|
+
|
|
212
|
+
### ❌ Layer Violations
|
|
213
|
+
|
|
214
|
+
**Problem:** Domain importing from infrastructure
|
|
215
|
+
|
|
216
|
+
```
|
|
217
|
+
// BAD - Domain importing infrastructure
|
|
218
|
+
// In: src/domain/services/ProductService
|
|
219
|
+
import { Database } from '../../infrastructure/database';
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
**Solution:** Domain should only define interfaces
|
|
223
|
+
|
|
224
|
+
```
|
|
225
|
+
// GOOD - Domain defines interface
|
|
226
|
+
// In: src/domain/interfaces/
|
|
227
|
+
export interface IDatabase { ... }
|
|
228
|
+
|
|
229
|
+
// Infrastructure implements it
|
|
230
|
+
// In: src/infrastructure/
|
|
231
|
+
export class Database implements IDatabase { ... }
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
### ❌ Application Importing Infrastructure
|
|
235
|
+
|
|
236
|
+
**Problem:** Application layer importing infrastructure implementations
|
|
237
|
+
|
|
238
|
+
```
|
|
239
|
+
// BAD - Application importing infrastructure
|
|
240
|
+
// In: src/application/services/OrderService
|
|
241
|
+
import { ProductRepository } from '../../infrastructure/repositories/ProductRepository';
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
**Solution:** Application should depend on domain interfaces
|
|
245
|
+
|
|
246
|
+
```
|
|
247
|
+
// GOOD - Application depends on interface
|
|
248
|
+
// In: src/application/services/OrderService
|
|
249
|
+
import { IProductRepository } from '../../domain/interfaces/IProductRepository';
|
|
250
|
+
|
|
251
|
+
// Infrastructure is injected via DI
|
|
252
|
+
constructor(private readonly productRepo: IProductRepository) {}
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
### ❌ Direct Database Access in Services
|
|
256
|
+
|
|
257
|
+
**Problem:** Services accessing database directly
|
|
258
|
+
|
|
259
|
+
**Solution:** Always use repository abstractions
|
|
260
|
+
|
|
261
|
+
### ❌ Business Logic in Infrastructure
|
|
262
|
+
|
|
263
|
+
**Problem:** Business rules in repository implementations
|
|
264
|
+
|
|
265
|
+
**Solution:** Keep repositories focused on data access, move logic to domain/application
|
|
266
|
+
|
|
267
|
+
## Testing Strategy
|
|
268
|
+
|
|
269
|
+
### Domain Layer Tests
|
|
270
|
+
|
|
271
|
+
- Test business logic in isolation
|
|
272
|
+
- No mocking needed (pure logic)
|
|
273
|
+
- Fast, deterministic tests
|
|
274
|
+
|
|
275
|
+
### Application Layer Tests
|
|
276
|
+
|
|
277
|
+
- Mock repository interfaces
|
|
278
|
+
- Test use case orchestration
|
|
279
|
+
- Verify correct repository calls
|
|
280
|
+
|
|
281
|
+
### Infrastructure Layer Tests
|
|
282
|
+
|
|
283
|
+
- Integration tests with real dependencies
|
|
284
|
+
- Test data access patterns
|
|
285
|
+
- Verify error handling
|
|
286
|
+
|
|
287
|
+
## Benefits of Clean Architecture
|
|
288
|
+
|
|
289
|
+
1. **Independent of Frameworks**: Business rules don't depend on external libraries
|
|
290
|
+
2. **Testable**: Business logic can be tested without UI, database, or external services
|
|
291
|
+
3. **Independent of UI**: Can change UI without changing business rules
|
|
292
|
+
4. **Independent of Database**: Can swap databases without changing business logic
|
|
293
|
+
5. **Independent of External Services**: Business rules don't know about external APIs
|
|
294
|
+
|
|
295
|
+
## Migration Path
|
|
296
|
+
|
|
297
|
+
### Legacy Code
|
|
298
|
+
|
|
299
|
+
If you have existing code that doesn't follow Clean Architecture:
|
|
300
|
+
|
|
301
|
+
1. Start with new features following the pattern
|
|
302
|
+
2. Gradually extract interfaces from existing code
|
|
303
|
+
3. Move business logic to domain/application
|
|
304
|
+
4. Create repository interfaces for data access
|
|
305
|
+
5. Refactor incrementally, don't rewrite everything
|
|
306
|
+
|
|
307
|
+
### Red Flags
|
|
308
|
+
|
|
309
|
+
- Circular dependencies
|
|
310
|
+
- Business logic in infrastructure
|
|
311
|
+
- Direct database access in services
|
|
312
|
+
- Framework coupling in domain
|
|
313
|
+
- God classes (too many responsibilities)
|
|
314
|
+
|
|
315
|
+
## Language-Specific Details
|
|
316
|
+
|
|
317
|
+
For language-specific implementation details (naming conventions, syntax, idioms), see your language-specific rules:
|
|
318
|
+
|
|
319
|
+
- **TypeScript**: `languages/typescript/coding-standards.md`
|
|
320
|
+
- **Python**: `languages/python/coding-standards.md`
|
|
321
|
+
- **Go**: `languages/go/coding-standards.md`
|
|
322
|
+
- **Java**: `languages/java/coding-standards.md`
|
|
323
|
+
|
|
324
|
+
## Summary Checklist
|
|
325
|
+
|
|
326
|
+
- [ ] Domain layer has no dependencies
|
|
327
|
+
- [ ] Application layer depends on domain only
|
|
328
|
+
- [ ] Infrastructure implements domain interfaces
|
|
329
|
+
- [ ] All data access through repositories
|
|
330
|
+
- [ ] Dependencies injected, not instantiated
|
|
331
|
+
- [ ] Each class has single responsibility
|
|
332
|
+
- [ ] Interfaces are small and focused
|
|
333
|
+
- [ ] Business logic is testable in isolation
|