Loading...
Loading...
Audits project physical structure: file hygiene, ignore file quality, framework convention compliance, domain/layer organization, naming conventions. Stack-adaptive via auto-detection.
npx skill4agent add levnikolaevich/claude-code-skills ln-646-project-structure-auditorPaths: File paths (,shared/,references/) are relative to skills repo root. If not found at CWD, locate this SKILL.md directory and go up one level for repo root.../ln-*
audit_scoring.md{output_dir}/646-structure[-{domain}].md- codebase_root: string # Root directory to scan
- output_dir: string # e.g., "docs/project/.audit/ln-640/{YYYY-MM-DD}"
# Domain-aware (optional, from coordinator)
- domain_mode: "global" | "domain-aware" # Default: "global"
- current_domain: string # e.g., "users", "billing" (only if domain-aware)
- scan_path: string # e.g., "src/users/" (only if domain-aware)shared/references/two_layer_detection.md../ln-700-project-bootstrap/references/stack_detection.mdscan_root = scan_path IF domain_mode == "domain-aware" ELSE codebase_root
# Priority 1: Read docs/project/tech_stack.md if exists
IF exists(docs/project/tech_stack.md):
tech_stack = parse(tech_stack.md)
# Priority 2: Auto-detect from project files
ELSE:
Check package.json -> React/Vue/Angular/Express/NestJS
Check *.csproj/*.sln -> .NET
Check pyproject.toml/requirements.txt -> Python/FastAPI/Django
Check go.mod -> Go
Check Cargo.toml -> Rust
Check pnpm-workspace.yaml/turbo.json -> Monorepo
tech_stack = {
language: "typescript" | "python" | "csharp" | "go" | ...,
framework: "react" | "fastapi" | "aspnetcore" | ...,
structure: "monolith" | "clean-architecture" | "monorepo" | ...
}references/structure_rules.md../ln-724-artifact-cleaner/references/platform_artifacts.md# Check 2.1: Build artifacts tracked in git
FOR EACH artifact_dir IN structure_rules.build_artifact_dirs:
IF Glob("{scan_root}/**/{artifact_dir}"):
findings.append(severity: "HIGH", issue: "Build artifact directory in repo",
location: path, recommendation: "Add to .gitignore, remove from tracking")
# Check 2.2: Temp/log files
FOR EACH pattern IN structure_rules.temp_junk_patterns:
IF Glob("{scan_root}/**/{pattern}"):
findings.append(severity: "MEDIUM", ...)
# Check 2.3: Platform remnants (from platform_artifacts.md)
FOR EACH platform IN [Replit, StackBlitz, CodeSandbox, Glitch]:
IF platform indicator files found:
findings.append(severity: "MEDIUM", issue: "Platform remnant: {file}",
principle: "File Hygiene / Platform Artifacts")
# Check 2.4: Multiple lock files
lock_files = Glob("{scan_root}/{package-lock.json,yarn.lock,pnpm-lock.yaml,bun.lockb}")
IF len(lock_files) > 1:
findings.append(severity: "HIGH", issue: "Multiple lock files: {lock_files}",
recommendation: "Keep one lock file matching your package manager")
# Check 2.5: .env files committed
env_files = Glob("{scan_root}/**/.env") + Glob("{scan_root}/**/.env.local")
+ Glob("{scan_root}/**/.env.*.local")
IF len(env_files) > 0:
findings.append(severity: "CRITICAL", issue: ".env file(s) committed",
recommendation: "Remove from tracking, add to .gitignore")
# Check 2.6: Large binaries tracked by git
FOR EACH file IN Glob("{scan_root}/**/*.{zip,tar,gz,rar,exe,dll,so,dylib,jar,war}"):
findings.append(severity: "MEDIUM", issue: "Binary file tracked: {file}",
recommendation: "Use Git LFS or remove from repository")references/structure_rules.md../ln-733-env-configurator/references/gitignore_secrets.template../ln-731-docker-generator/references/dockerignore.template# Check 3.1: .gitignore exists
IF NOT exists(.gitignore):
findings.append(severity: "HIGH", issue: "No .gitignore file")
ELSE:
content = Read(.gitignore)
# Check 3.1a: Stack-specific entries present
required_entries = get_required_gitignore(tech_stack)
FOR EACH entry IN required_entries:
IF entry NOT covered in .gitignore:
findings.append(severity: "MEDIUM", issue: ".gitignore missing: {entry}")
# Check 3.1b: Secrets protection (compare with gitignore_secrets.template)
secrets_patterns = [".env", ".env.local", "*.pem", "*.key", "secrets/"]
FOR EACH pattern IN secrets_patterns:
IF pattern NOT in .gitignore:
findings.append(severity: "HIGH", issue: ".gitignore missing secrets: {pattern}",
principle: "Ignore Files / Secrets")
# Check 3.1c: IDE/OS entries
ide_patterns = [".vscode/", ".idea/", "*.swp", ".DS_Store", "Thumbs.db"]
missing_ide = [p for p in ide_patterns if p NOT covered in .gitignore]
IF len(missing_ide) > 2:
findings.append(severity: "LOW", issue: ".gitignore missing IDE/OS: {missing_ide}")
# Check 3.2: .dockerignore
IF exists(Dockerfile) AND NOT exists(.dockerignore):
findings.append(severity: "MEDIUM", issue: "Dockerfile exists but no .dockerignore",
recommendation: "Create .dockerignore to reduce build context")
ELIF exists(.dockerignore):
FOR EACH required IN [node_modules, .git, .env, "*.log"]:
IF required NOT in .dockerignore:
findings.append(severity: "LOW", ...)references/structure_rules.mdrules = get_framework_rules(tech_stack, structure_rules.md)
# Returns: {expected_dirs, forbidden_placements, co_location_rules}
# Check 4.1: Expected directories exist
FOR EACH dir IN rules.expected_dirs WHERE dir.required == true:
IF NOT exists(dir.path):
findings.append(severity: "MEDIUM", issue: "Expected directory missing: {dir.path}",
principle: "Framework Convention / {tech_stack.framework}")
# Check 4.2: Source code in wrong locations
FOR EACH rule IN rules.forbidden_placements:
matches = Glob(rule.glob_pattern, scan_root)
FOR EACH match IN matches:
IF match NOT IN rules.exceptions:
findings.append(severity: "HIGH", issue: "Source file in wrong location: {match}",
recommendation: "Move to {rule.expected_location}")
# Check 4.3: Co-location rules (React feature folders)
IF tech_stack.framework IN ["react", "vue", "angular", "svelte"]:
component_dirs = Glob("{scan_root}/**/components/*/")
colocation_count = 0
FOR EACH dir IN component_dirs:
has_test = Glob("{dir}/*.{test,spec}.{ts,tsx,js,jsx}")
IF has_test: colocation_count += 1
# Only flag if project already uses co-location (>50%)
IF colocation_count > len(component_dirs) * 0.5:
FOR EACH dir IN component_dirs:
IF NOT has_test_for(dir):
findings.append(severity: "LOW", issue: "Component missing co-located test: {dir}")# Check 5.1: Junk drawer detection
junk_thresholds = structure_rules.junk_drawer_thresholds
FOR EACH dir IN Glob("{scan_root}/**/"):
dir_name = basename(dir)
IF dir_name IN junk_thresholds:
file_count = len(Glob("{dir}/*.*"))
IF file_count > junk_thresholds[dir_name].max_files:
findings.append(severity: junk_thresholds[dir_name].severity,
issue: "Junk drawer directory: {dir} ({file_count} files)",
principle: "Organization / Module Cohesion",
recommendation: "Split into domain-specific modules or feature folders")
# Check 5.2: Root directory cleanliness
root_files = Glob("{scan_root}/*") # Direct children only
source_in_root = [f for f in root_files
if f.ext IN source_extensions AND basename(f) NOT IN allowed_root_files]
IF len(source_in_root) > 0:
findings.append(severity: "MEDIUM", issue: "Source files in project root: {source_in_root}",
recommendation: "Move to src/ or appropriate module directory")
config_in_root = [f for f in root_files if is_config_file(f)]
IF len(config_in_root) > 15:
findings.append(severity: "LOW",
issue: "Excessive config files in root ({len(config_in_root)})",
recommendation: "Move non-essential configs to config/ directory")
# Check 5.3: Consistent module structure across domains
IF domain_mode == "global" AND len(detect_domains(scan_root)) >= 2:
domains = detect_domains(scan_root)
structures = {d.name: set(subdirectory_names(d.path)) for d in domains}
all_subdirs = union(structures.values())
FOR EACH domain IN domains:
missing = all_subdirs - structures[domain]
IF 0 < len(missing) < len(all_subdirs) * 0.5:
findings.append(severity: "LOW",
issue: "Inconsistent domain structure: {domain.name} missing {missing}",
recommendation: "Align domain module structures for consistency")references/structure_rules.mdnaming_rules = get_naming_rules(tech_stack)
# Returns: {file_case, dir_case, test_pattern, component_case}
# Check 6.1: File naming consistency
violations = []
FOR EACH file IN Glob("{scan_root}/**/*.{ts,tsx,js,jsx,py,cs,go}"):
expected_case = naming_rules.file_case
IF is_component(file) AND NOT matches_case(basename(file), expected_case):
violations.append(file)
IF len(violations) > 0:
pct = len(violations) / total_source_files * 100
severity = "HIGH" if pct > 30 else "MEDIUM" if pct > 10 else "LOW"
findings.append(severity, issue: "Naming violations: {len(violations)} files ({pct}%)",
principle: "Naming / {naming_rules.file_case}")
# Check 6.2: Directory naming consistency
dirs = get_all_source_dirs(scan_root)
dir_cases = classify_cases(dirs) # Count per case style
dominant = max(dir_cases)
inconsistent = [d for d in dirs if case_of(d) != dominant]
IF len(inconsistent) > 0:
findings.append(severity: "LOW",
issue: "Inconsistent directory naming: {len(inconsistent)} dirs use mixed case")
# Check 6.3: Test file naming pattern
test_files = Glob("{scan_root}/**/*.{test,spec}.{ts,tsx,js,jsx}")
+ Glob("{scan_root}/**/*_test.{py,go}")
IF len(test_files) > 0:
patterns_used = detect_test_patterns(test_files) # .test. vs .spec. vs _test
IF len(patterns_used) > 1:
findings.append(severity: "LOW", issue: "Mixed test naming patterns: {patterns_used}",
recommendation: "Standardize to {dominant_test_pattern}")shared/references/audit_scoring.mdshared/templates/audit_worker_report_template.md# 7a: Calculate Score
penalty = (critical * 2.0) + (high * 1.0) + (medium * 0.5) + (low * 0.2)
score = max(0, 10 - penalty)
# 7b: Build Report in Memory
report = """
# Project Structure Audit Report
<!-- AUDIT-META
worker: ln-646
category: Project Structure
domain: {domain_name|global}
scan_path: {scan_path|.}
score: {score}
total_issues: {total}
critical: {critical}
high: {high}
medium: {medium}
low: {low}
status: complete
-->
## Checks
| ID | Check | Status | Details |
|----|-------|--------|---------|
| file_hygiene | File Hygiene | {status} | Build artifacts, temp files, env files, binaries |
| ignore_files | Ignore File Quality | {status} | .gitignore completeness, secrets, .dockerignore |
| framework_conventions | Framework Conventions | {status} | {framework} structure compliance |
| domain_organization | Domain/Layer Organization | {status} | Junk drawers, root cleanliness, consistency |
| naming_conventions | Naming Conventions | {status} | File/dir/test naming patterns |
## Findings
| Severity | Location | Issue | Principle | Recommendation | Effort |
|----------|----------|-------|-----------|----------------|--------|
{sorted by severity: CRITICAL first, then HIGH, MEDIUM, LOW}
<!-- DATA-EXTENDED
{
"tech_stack": {"language": "...", "framework": "...", "structure": "..."},
"dimensions": {
"file_hygiene": {"checks": 6, "issues": N},
"ignore_files": {"checks": 4, "issues": N},
"framework_conventions": {"checks": 3, "issues": N},
"domain_organization": {"checks": 3, "issues": N},
"naming_conventions": {"checks": 3, "issues": N}
},
"junk_drawers": [{"path": "src/utils", "file_count": 23}],
"naming_dominant_case": "PascalCase",
"naming_violations_pct": 5
}
-->
"""
# 7c: Write Report (atomic single Write call)
IF domain_mode == "domain-aware":
Write to {output_dir}/646-structure-{current_domain}.md
ELSE:
Write to {output_dir}/646-structure.md
# 7d: Return Summary
Report written: docs/project/.audit/ln-640/{YYYY-MM-DD}/646-structure[-{domain}].md
Score: X.X/10 | Issues: N (C:N H:N M:N L:N)shared/references/audit_scoring.mdpenalty = (critical x 2.0) + (high x 1.0) + (medium x 0.5) + (low x 0.2)
score = max(0, 10 - penalty)docs/project/tech_stack.mdscan_pathaudit_scoring.md{output_dir}/646-structure[-{domain}].mdshared/templates/audit_worker_report_template.mdshared/references/audit_scoring.mdreferences/structure_rules.md../ln-700-project-bootstrap/references/stack_detection.md../ln-724-artifact-cleaner/references/platform_artifacts.md../ln-733-env-configurator/references/gitignore_secrets.template../ln-731-docker-generator/references/dockerignore.template