feat(guardian): add guardian package - code quality analyzer

Add @puaros/guardian package v0.1.0 - code quality guardian for vibe coders and enterprise teams.

Features:
- Hardcode detection (magic numbers, magic strings)
- Circular dependency detection
- Naming convention enforcement (Clean Architecture)
- Architecture violation detection
- CLI tool with comprehensive reporting
- 159 tests with 80%+ coverage
- Smart suggestions for fixes
- Built for AI-assisted development

Built with Clean Architecture and DDD principles.
Works with Claude, GPT, Copilot, Cursor, and any AI coding assistant.
This commit is contained in:
imfozilbek
2025-11-24 02:54:39 +05:00
parent 9f97509b06
commit 03705b5264
96 changed files with 9520 additions and 0 deletions

View File

@@ -0,0 +1,344 @@
import { UseCase } from "./BaseUseCase"
import { ResponseDto } from "../dtos/ResponseDto"
import { IFileScanner } from "../../domain/services/IFileScanner"
import { ICodeParser } from "../../domain/services/ICodeParser"
import { IHardcodeDetector } from "../../domain/services/IHardcodeDetector"
import { INamingConventionDetector } from "../../domain/services/INamingConventionDetector"
import { SourceFile } from "../../domain/entities/SourceFile"
import { DependencyGraph } from "../../domain/entities/DependencyGraph"
import { ProjectPath } from "../../domain/value-objects/ProjectPath"
import {
ERROR_MESSAGES,
HARDCODE_TYPES,
LAYERS,
NAMING_VIOLATION_TYPES,
REGEX_PATTERNS,
RULES,
SEVERITY_LEVELS,
} from "../../shared/constants"
export interface AnalyzeProjectRequest {
rootDir: string
include?: string[]
exclude?: string[]
}
export interface AnalyzeProjectResponse {
files: SourceFile[]
dependencyGraph: DependencyGraph
violations: ArchitectureViolation[]
hardcodeViolations: HardcodeViolation[]
circularDependencyViolations: CircularDependencyViolation[]
namingViolations: NamingConventionViolation[]
metrics: ProjectMetrics
}
export interface ArchitectureViolation {
rule: string
message: string
file: string
line?: number
}
export interface HardcodeViolation {
rule: typeof RULES.HARDCODED_VALUE
type:
| typeof HARDCODE_TYPES.MAGIC_NUMBER
| typeof HARDCODE_TYPES.MAGIC_STRING
| typeof HARDCODE_TYPES.MAGIC_CONFIG
value: string | number
file: string
line: number
column: number
context: string
suggestion: {
constantName: string
location: string
}
}
export interface CircularDependencyViolation {
rule: typeof RULES.CIRCULAR_DEPENDENCY
message: string
cycle: string[]
severity: typeof SEVERITY_LEVELS.ERROR
}
export interface NamingConventionViolation {
rule: typeof RULES.NAMING_CONVENTION
type:
| typeof NAMING_VIOLATION_TYPES.WRONG_SUFFIX
| typeof NAMING_VIOLATION_TYPES.WRONG_PREFIX
| typeof NAMING_VIOLATION_TYPES.WRONG_CASE
| typeof NAMING_VIOLATION_TYPES.FORBIDDEN_PATTERN
| typeof NAMING_VIOLATION_TYPES.WRONG_VERB_NOUN
fileName: string
layer: string
file: string
expected: string
actual: string
message: string
suggestion?: string
}
export interface ProjectMetrics {
totalFiles: number
totalFunctions: number
totalImports: number
layerDistribution: Record<string, number>
}
/**
* Main use case for analyzing a project's codebase
*/
export class AnalyzeProject extends UseCase<
AnalyzeProjectRequest,
ResponseDto<AnalyzeProjectResponse>
> {
constructor(
private readonly fileScanner: IFileScanner,
private readonly codeParser: ICodeParser,
private readonly hardcodeDetector: IHardcodeDetector,
private readonly namingConventionDetector: INamingConventionDetector,
) {
super()
}
public async execute(
request: AnalyzeProjectRequest,
): Promise<ResponseDto<AnalyzeProjectResponse>> {
try {
const filePaths = await this.fileScanner.scan({
rootDir: request.rootDir,
include: request.include,
exclude: request.exclude,
})
const sourceFiles: SourceFile[] = []
const dependencyGraph = new DependencyGraph()
let totalFunctions = 0
for (const filePath of filePaths) {
const content = await this.fileScanner.readFile(filePath)
const projectPath = ProjectPath.create(filePath, request.rootDir)
const imports = this.extractImports(content)
const exports = this.extractExports(content)
const sourceFile = new SourceFile(projectPath, content, imports, exports)
sourceFiles.push(sourceFile)
dependencyGraph.addFile(sourceFile)
if (projectPath.isTypeScript()) {
const tree = this.codeParser.parseTypeScript(content)
const functions = this.codeParser.extractFunctions(tree)
totalFunctions += functions.length
}
for (const imp of imports) {
dependencyGraph.addDependency(
projectPath.relative,
this.resolveImportPath(imp, filePath, request.rootDir),
)
}
}
const violations = this.detectViolations(sourceFiles)
const hardcodeViolations = this.detectHardcode(sourceFiles)
const circularDependencyViolations = this.detectCircularDependencies(dependencyGraph)
const namingViolations = this.detectNamingConventions(sourceFiles)
const metrics = this.calculateMetrics(sourceFiles, totalFunctions, dependencyGraph)
return ResponseDto.ok({
files: sourceFiles,
dependencyGraph,
violations,
hardcodeViolations,
circularDependencyViolations,
namingViolations,
metrics,
})
} catch (error) {
const errorMessage = `${ERROR_MESSAGES.FAILED_TO_ANALYZE}: ${error instanceof Error ? error.message : String(error)}`
return ResponseDto.fail(errorMessage)
}
}
private extractImports(content: string): string[] {
const imports: string[] = []
let match
while ((match = REGEX_PATTERNS.IMPORT_STATEMENT.exec(content)) !== null) {
imports.push(match[1])
}
return imports
}
private extractExports(content: string): string[] {
const exports: string[] = []
let match
while ((match = REGEX_PATTERNS.EXPORT_STATEMENT.exec(content)) !== null) {
exports.push(match[1])
}
return exports
}
private resolveImportPath(importPath: string, _currentFile: string, _rootDir: string): string {
if (importPath.startsWith(".")) {
return importPath
}
return importPath
}
private detectViolations(sourceFiles: SourceFile[]): ArchitectureViolation[] {
const violations: ArchitectureViolation[] = []
const layerRules: Record<string, string[]> = {
[LAYERS.DOMAIN]: [LAYERS.SHARED],
[LAYERS.APPLICATION]: [LAYERS.DOMAIN, LAYERS.SHARED],
[LAYERS.INFRASTRUCTURE]: [LAYERS.DOMAIN, LAYERS.APPLICATION, LAYERS.SHARED],
[LAYERS.SHARED]: [],
}
for (const file of sourceFiles) {
if (!file.layer) {
continue
}
const allowedLayers = layerRules[file.layer]
for (const imp of file.imports) {
const importedLayer = this.detectLayerFromImport(imp)
if (
importedLayer &&
importedLayer !== file.layer &&
!allowedLayers.includes(importedLayer)
) {
violations.push({
rule: RULES.CLEAN_ARCHITECTURE,
message: `Layer "${file.layer}" cannot import from "${importedLayer}"`,
file: file.path.relative,
})
}
}
}
return violations
}
private detectLayerFromImport(importPath: string): string | undefined {
const layers = Object.values(LAYERS)
for (const layer of layers) {
if (importPath.toLowerCase().includes(layer)) {
return layer
}
}
return undefined
}
private detectHardcode(sourceFiles: SourceFile[]): HardcodeViolation[] {
const violations: HardcodeViolation[] = []
for (const file of sourceFiles) {
const hardcodedValues = this.hardcodeDetector.detectAll(
file.content,
file.path.relative,
)
for (const hardcoded of hardcodedValues) {
violations.push({
rule: RULES.HARDCODED_VALUE,
type: hardcoded.type,
value: hardcoded.value,
file: file.path.relative,
line: hardcoded.line,
column: hardcoded.column,
context: hardcoded.context,
suggestion: {
constantName: hardcoded.suggestConstantName(),
location: hardcoded.suggestLocation(file.layer),
},
})
}
}
return violations
}
private detectCircularDependencies(
dependencyGraph: DependencyGraph,
): CircularDependencyViolation[] {
const violations: CircularDependencyViolation[] = []
const cycles = dependencyGraph.findCycles()
for (const cycle of cycles) {
const cycleChain = [...cycle, cycle[0]].join(" → ")
violations.push({
rule: RULES.CIRCULAR_DEPENDENCY,
message: `Circular dependency detected: ${cycleChain}`,
cycle,
severity: SEVERITY_LEVELS.ERROR,
})
}
return violations
}
private detectNamingConventions(sourceFiles: SourceFile[]): NamingConventionViolation[] {
const violations: NamingConventionViolation[] = []
for (const file of sourceFiles) {
const namingViolations = this.namingConventionDetector.detectViolations(
file.path.filename,
file.layer,
file.path.relative,
)
for (const violation of namingViolations) {
violations.push({
rule: RULES.NAMING_CONVENTION,
type: violation.violationType,
fileName: violation.fileName,
layer: violation.layer,
file: violation.filePath,
expected: violation.expected,
actual: violation.actual,
message: violation.getMessage(),
suggestion: violation.suggestion,
})
}
}
return violations
}
private calculateMetrics(
sourceFiles: SourceFile[],
totalFunctions: number,
_dependencyGraph: DependencyGraph,
): ProjectMetrics {
const layerDistribution: Record<string, number> = {}
let totalImports = 0
for (const file of sourceFiles) {
if (file.layer) {
layerDistribution[file.layer] = (layerDistribution[file.layer] || 0) + 1
}
totalImports += file.imports.length
}
return {
totalFiles: sourceFiles.length,
totalFunctions,
totalImports,
layerDistribution,
}
}
}