Analyze FSD project structure and generate health report
/plugin marketplace add jhlee0409/claude-plugins/plugin install fsdarch@jhlee0409-pluginsνμ¬ FSD νλ‘μ νΈ κ΅¬μ‘°λ₯Ό λΆμνκ³ μμΈ λ¦¬ν¬νΈλ₯Ό μμ±ν©λλ€.
.fsd-architect.json μ€μ νμΌ μ‘΄μ¬ (μμΌλ©΄ /fsdarch:init λ¨Όμ μ€ν)When /fsdarch:analyze is invoked, Claude MUST perform these steps in order:
.fsd-architect.json (error E104 if missing)βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β /fsdarch:analyze β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β 1. Load .fsd-architect.json β
β ββ Not found β Error E104, suggest /fsdarch:init β
β ββ Found β Continue β
β β
β 2. Check cache (unless --force) β
β ββ Valid cache exists β Use incremental analysis β
β ββ No cache / stale β Full scan β
β β
β 3. Invoke skill: layer-detector β
β β Scan all layers, slices, segments β
β β Count files per layer/slice β
β β
β 4. Invoke skill: boundary-checker β
β β Parse import statements β
β β Build dependency graph β
β β Detect violations (E201, E202, E203, E204, E205) β
β β
β 5. Calculate health score (100 points) β
β β Layer hierarchy (30%) β
β β Public API usage (25%) β
β β Slice isolation (20%) β
β β Naming consistency (15%) β
β β Segment structure (10%) β
β β
β 6. Display report with issues and recommendations β
β β
β 7. Update .fsd-architect.cache.json β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Action: Read and parse .fsd-architect.json
1. Use Read tool to read .fsd-architect.json
2. If file not found:
β Display error E104 and stop
3. If JSON parse error:
β Display error E401 with line number
4. Validate required fields:
β srcDir (required)
β layers (required)
5. Extract configuration values for subsequent steps
Read tool command:
Read: .fsd-architect.json
Error output if missing:
[E104] Configuration not found
.fsd-architect.json is required for this command.
Solution: Initialize first:
/fsdarch:init
Progress output:
> Loading configuration...
Action: Invoke skill: cache-manager for incremental analysis
1. Skip if --force flag is set
2. Read .fsd-architect.cache.json (if exists)
3. Compare file mtimes with cached values
4. Identify files that changed since last analysis
5. Only re-analyze changed files (incremental mode)
Cache benefits:
Progress output (if using cache):
> Using cached analysis (15 files changed since last run)
Action: Invoke skill: layer-detector
1. For each layer in config.layers:
β Glob for {srcDir}/{layerPath}/**/*
β If sliced layer: list immediate subdirectories as slices
β For each slice: list segments (ui/, model/, api/, lib/)
β Count files per layer/slice
2. Build LayerInfo structure for each layer:
{
name: "features",
path: "src/features",
slices: [
{ name: "auth", segments: ["ui", "model", "api"], hasPublicApi: true, fileCount: 12 },
{ name: "cart", segments: ["ui", "model"], hasPublicApi: true, fileCount: 8 }
],
totalFiles: 156,
isSliced: true
}
Glob commands to execute:
IMPORTANT: Memory Safety - Use pagination for large projects
For projects with many files, use head_limit and offset to batch file processing:
# RECOMMENDED: Batch processing with head_limit (max 1000 files per batch)
# This prevents memory exhaustion on large codebases (10,000+ files)
# For each layer defined in config (with pagination)
Glob: "{srcDir}/app/**/*.{ts,tsx,js,jsx}" head_limit=1000 offset=0
Glob: "{srcDir}/pages/**/*.{ts,tsx,js,jsx}" head_limit=1000 offset=0
Glob: "{srcDir}/widgets/**/*.{ts,tsx,js,jsx}" head_limit=1000 offset=0
Glob: "{srcDir}/features/**/*.{ts,tsx,js,jsx}" head_limit=1000 offset=0
Glob: "{srcDir}/entities/**/*.{ts,tsx,js,jsx}" head_limit=1000 offset=0
Glob: "{srcDir}/shared/**/*.{ts,tsx,js,jsx}" head_limit=1000 offset=0
# If more than 1000 files in a layer, continue with offset:
# Glob: "{srcDir}/features/**/*.{ts,tsx,js,jsx}" head_limit=1000 offset=1000
# ... repeat until all files processed
# For sliced layers - get slice list
Glob: "{srcDir}/features/*/"
Glob: "{srcDir}/entities/*/"
# Check public API for each slice
Glob: "{srcDir}/features/*/index.{ts,tsx,js,jsx}"
Batch Processing Algorithm:
const BATCH_SIZE = 1000;
async function batchGlob(pattern: string): Promise<string[]> {
const allFiles: string[] = [];
let offset = 0;
while (true) {
const batch = await glob(pattern, { head_limit: BATCH_SIZE, offset });
if (batch.length === 0) break;
allFiles.push(...batch);
if (batch.length < BATCH_SIZE) break; // Last batch
offset += BATCH_SIZE;
}
return allFiles;
}
TypeScript interfaces:
interface LayerInfo {
name: string;
path: string;
slices: SliceInfo[];
totalFiles: number;
isSliced: boolean;
}
interface SliceInfo {
name: string;
segments: string[];
hasPublicApi: boolean;
fileCount: number;
}
Progress output:
> Scanning 6 layers, 20 slices...
Action: Invoke skill: boundary-checker
1. For each TypeScript/JavaScript file in the project:
β Read file content
β Parse import/require statements
β Extract import paths
2. For each import:
β Resolve to actual file path
β Determine source layer/slice
β Determine target layer/slice
β Check against FSD rules
3. Build dependency graph:
β Track all layer β layer dependencies
β Track all slice β slice dependencies
β Identify violations
Grep commands to find imports:
# Find all import statements
Grep: "^import .* from ['\"]" --type ts
Grep: "require\\(['\"]" --type ts
# Focus on potential violations (cross-layer/slice imports)
Grep: "from ['\"]@features/" --type ts
Grep: "from ['\"]@entities/" --type ts
Grep: "from ['\"]\\.\\./\\.\\." --type ts # Relative cross-boundary
Violation detection rules:
// Layer hierarchy (lowest = 1, highest = 6)
const LAYER_ORDER = {
shared: 1,
entities: 2,
features: 3,
widgets: 4,
pages: 5,
app: 6
};
// E203: Higher layer import from lower layer
function checkLayerViolation(source: Layer, target: Layer): boolean {
return LAYER_ORDER[source] < LAYER_ORDER[target];
}
// E201: Cross-slice import (same layer)
function checkCrossSlice(source: Slice, target: Slice, layer: Layer): boolean {
return source.layer === target.layer && source.name !== target.name;
}
// E202: Public API sidestep
function checkPublicApiSidestep(importPath: string): boolean {
// Import should be @entities/user, not @entities/user/model/types
return importPath.split('/').length > 2 && !importPath.endsWith('/index');
}
// E204: Missing public API
function checkMissingPublicApi(slice: Slice): boolean {
return !slice.hasPublicApi;
}
Progress output:
> Analyzing dependencies...
Action: Compute weighted health score (100 points max)
Scoring criteria:
| Criterion | Weight | How to Calculate |
|---|---|---|
| Layer Hierarchy | 30% | 100 - (E203 violations * 10), min 0 |
| Public API Usage | 25% | (slices with index.ts / total slices) * 100 - (E202 violations * 5) |
| Slice Isolation | 20% | 100 - (E201 violations * 10), min 0 |
| Naming Consistency | 15% | (slices matching dominant pattern / total slices) * 100 |
| Segment Structure | 10% | (slices with standard segments / total slices) * 100 |
Calculation algorithm:
function calculateHealthScore(analysis: AnalysisResult): number {
const weights = {
layerHierarchy: 0.30,
publicApi: 0.25,
sliceIsolation: 0.20,
namingConsistency: 0.15,
segmentStructure: 0.10
};
// Layer Hierarchy: Penalize E203 violations
const e203Count = analysis.violations.filter(v => v.code === 'E203').length;
const layerScore = Math.max(0, 100 - e203Count * 10);
// Public API: Check index.ts presence, penalize E202
const slicesWithApi = analysis.slices.filter(s => s.hasPublicApi).length;
const e202Count = analysis.violations.filter(v => v.code === 'E202').length;
const apiScore = Math.max(0, (slicesWithApi / analysis.slices.length) * 100 - e202Count * 5);
// Slice Isolation: Penalize E201 violations
const e201Count = analysis.violations.filter(v => v.code === 'E201').length;
const isolationScore = Math.max(0, 100 - e201Count * 10);
// Naming Consistency: Check dominant pattern match
const namingScore = analysis.patterns.namingConsistency * 100;
// Segment Structure: Check standard segment usage
const segmentScore = analysis.patterns.segmentConsistency * 100;
return Math.round(
layerScore * weights.layerHierarchy +
apiScore * weights.publicApi +
isolationScore * weights.sliceIsolation +
namingScore * weights.namingConsistency +
segmentScore * weights.segmentStructure
);
}
function getScoreLabel(score: number): string {
if (score >= 90) return 'Excellent';
if (score >= 80) return 'Good';
if (score >= 70) return 'Fair';
if (score >= 60) return 'Needs Work';
return 'Critical';
}
Action: Display formatted analysis report
Report template:
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
FSD ARCHITECTURE REPORT
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
π Health Score: {score}/100 ({label})
π Layer Summary:
βββββββββββββββ¬ββββββββββ¬ββββββββββββ¬ββββββββββ
β Layer β Slices β Files β Status β
βββββββββββββββΌββββββββββΌββββββββββββΌββββββββββ€
β {layerName} β {count} β {files} β {status}β
βββββββββββββββ΄ββββββββββ΄ββββββββββββ΄ββββββββββ
β οΈ Issues Found: {issueCount}
{for each violation}
{index}. [{code}] {source} β {target} ({description})
Location: {file}:{line}
π Dependency Graph:
{visual graph representation}
π‘ Recommendations:
{numbered list of suggestions}
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Status icons:
β = No issues
β N = N issues found
β = Critical issues
Recommendation generation:
function generateRecommendations(violations: Violation[]): string[] {
const recommendations: string[] = [];
// Group by violation type
const e201s = violations.filter(v => v.code === 'E201');
const e202s = violations.filter(v => v.code === 'E202');
const e203s = violations.filter(v => v.code === 'E203');
const e204s = violations.filter(v => v.code === 'E204');
if (e201s.length > 0) {
recommendations.push(
`Extract shared logic between ${e201s[0].source} and ${e201s[0].target} to entities or shared layer`
);
}
if (e202s.length > 0) {
recommendations.push(
`Export ${e202s[0].target} through its slice public API (index.ts)`
);
}
if (e203s.length > 0) {
recommendations.push(
`Refactor ${e203s[0].source} to not depend on higher layer ${e203s[0].target}`
);
}
if (e204s.length > 0) {
recommendations.push(
`Add index.ts to slices: ${e204s.map(v => v.source).join(', ')}`
);
}
return recommendations;
}
Example output:
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
FSD ARCHITECTURE REPORT
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
π Health Score: 85/100 (Good)
π Layer Summary:
βββββββββββββββ¬ββββββββββ¬ββββββββββββ¬ββββββββββ
β Layer β Slices β Files β Status β
βββββββββββββββΌββββββββββΌββββββββββββΌββββββββββ€
β app β - β 12 β β β
β pages β 5 β 45 β β β
β widgets β 3 β 28 β β β
β features β 8 β 156 β β 2 β
β entities β 4 β 67 β β β
β shared β - β 89 β β β
βββββββββββββββ΄ββββββββββ΄ββββββββββββ΄ββββββββββ
β οΈ Issues Found: 2
1. [E201] features/auth β features/user (forbidden cross-slice)
Location: src/features/auth/model/session.ts:15
2. [E202] features/cart β entities/product/internal
Location: src/features/cart/api/addToCart.ts:8
Bypasses public API
π Dependency Graph:
pages βββ widgets βββ features βββ entities βββ shared
β β β β
βββββββββββ΄ββββββββββββ΄βββββββββββββ
π‘ Recommendations:
1. Move shared auth/user logic to entities/session
2. Use entities/product public API (index.ts)
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Action: Save analysis results for incremental updates
1. Build cache object with:
β Timestamp of analysis
β File mtimes for all scanned files
β Violation list
β Layer/slice structure
2. Write to .fsd-architect.cache.json
3. Cache is automatically invalidated when:
β Files are modified (mtime changes)
β Config is changed
β --force flag is used
Cache structure:
{
"version": "1.0.0",
"timestamp": "2025-01-15T10:30:00Z",
"configHash": "abc123...",
"files": {
"src/features/auth/model/session.ts": {
"mtime": 1705312200000,
"imports": ["@entities/user", "@shared/api"]
}
},
"violations": [...],
"score": 85
}
| Flag | Description | Example |
|---|---|---|
--json | JSON νμμΌλ‘ μΆλ ₯ | /fsdarch:analyze --json |
--layer <name> | νΉμ λ μ΄μ΄λ§ λΆμ | /fsdarch:analyze --layer features |
--slice <name> | νΉμ μ¬λΌμ΄μ€λ§ λΆμ | /fsdarch:analyze --slice auth |
--force | μΊμ 무μνκ³ μ 체 μ€μΊ | /fsdarch:analyze --force |
--verbose | μμΈ μ 보 μΆλ ₯ | /fsdarch:analyze --verbose |
μμ μμμ κ°μ νμνλ ν μ€νΈ μΆλ ₯.
--json){
"score": 85,
"layers": {
"app": { "files": 12, "issues": 0 },
"pages": { "slices": 5, "files": 45, "issues": 0 },
"features": { "slices": 8, "files": 156, "issues": 2 }
},
"issues": [
{
"code": "E201",
"type": "forbidden-cross-slice",
"source": "features/auth",
"target": "features/user",
"location": "src/features/auth/model/session.ts:15"
}
],
"recommendations": [
"Move shared auth/user logic to entities/session"
]
}
Use skill: cache-manager
.fsd-architect.cache.jsonμ μΊμ--force νλκ·Έλ‘ μΊμ 무μ κ°λ₯[E104] Configuration not found
Run /fsdarch:init to initialize FSD Architect configuration.
[E105] Invalid layer structure
Layer 'features' contains non-slice directories:
- src/features/utils/ (should be in shared/lib)
- src/features/types/ (should be in shared/types)
/fsdarch:analyze
> Loading configuration...
> Scanning 6 layers, 20 slices...
> Analyzing dependencies...
> Health Score: 85/100
/fsdarch:analyze --layer features
> Analyzing features layer...
> Found 8 slices: auth, cart, checkout, favorites, orders, profile, search, wishlist
> Issues: 2 cross-slice imports
/fsdarch:analyze --json > fsd-report.json
> Report saved to fsd-report.json