feat: add database foundation for code quality dashboard (Phase 1/4)
Implemented database models and migration for tracking architecture violations in the admin dashboard. This is the foundation for the code quality UI. Database Models Added (app/models/architecture_scan.py): - ArchitectureScan: Tracks validator runs with statistics - ArchitectureViolation: Individual violations with status tracking - ArchitectureRule: Rule configuration with enable/disable support - ViolationAssignment: Assign violations to developers - ViolationComment: Collaboration on violations Schema Features: - Full violation history tracking - Status workflow (open → assigned → resolved/ignored/technical_debt) - Assignment and due date tracking - Comments for collaboration - Rule management (enable/disable, severity override) - Scan statistics (errors, warnings, duration) Migration: - Created 5 tables with proper indexes and foreign keys - SQLite-compatible datetime defaults - Supports relationships between scans, violations, users Next Steps (Remaining Phases): - Phase 2: Service layer (code_quality_service.py) - Phase 3: API endpoints (/admin/code-quality/*) - Phase 4: Frontend dashboard and UI Benefits: - Track technical debt over time - Assign violations to developers - Prioritize fixes by severity - Measure code quality improvements - Collaborative violation resolution 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,147 @@
|
||||
"""add_architecture_quality_tracking_tables
|
||||
|
||||
Revision ID: 7a7ce92593d5
|
||||
Revises: a2064e1dfcd4
|
||||
Create Date: 2025-11-28 09:21:16.545203
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = '7a7ce92593d5'
|
||||
down_revision: Union[str, None] = 'a2064e1dfcd4'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# Create architecture_scans table
|
||||
op.create_table(
|
||||
'architecture_scans',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('timestamp', sa.DateTime(timezone=True), server_default=sa.text("(datetime('now'))"), nullable=False),
|
||||
sa.Column('total_files', sa.Integer(), nullable=True),
|
||||
sa.Column('total_violations', sa.Integer(), nullable=True),
|
||||
sa.Column('errors', sa.Integer(), nullable=True),
|
||||
sa.Column('warnings', sa.Integer(), nullable=True),
|
||||
sa.Column('duration_seconds', sa.Float(), nullable=True),
|
||||
sa.Column('triggered_by', sa.String(length=100), nullable=True),
|
||||
sa.Column('git_commit_hash', sa.String(length=40), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index(op.f('ix_architecture_scans_id'), 'architecture_scans', ['id'], unique=False)
|
||||
op.create_index(op.f('ix_architecture_scans_timestamp'), 'architecture_scans', ['timestamp'], unique=False)
|
||||
|
||||
# Create architecture_rules table
|
||||
op.create_table(
|
||||
'architecture_rules',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('rule_id', sa.String(length=20), nullable=False),
|
||||
sa.Column('category', sa.String(length=50), nullable=False),
|
||||
sa.Column('name', sa.String(length=200), nullable=False),
|
||||
sa.Column('description', sa.Text(), nullable=True),
|
||||
sa.Column('severity', sa.String(length=10), nullable=False),
|
||||
sa.Column('enabled', sa.Boolean(), nullable=False, server_default='1'),
|
||||
sa.Column('custom_config', sa.JSON(), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text("(datetime('now'))"), nullable=False),
|
||||
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text("(datetime('now'))"), nullable=False),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('rule_id')
|
||||
)
|
||||
op.create_index(op.f('ix_architecture_rules_id'), 'architecture_rules', ['id'], unique=False)
|
||||
op.create_index(op.f('ix_architecture_rules_rule_id'), 'architecture_rules', ['rule_id'], unique=True)
|
||||
|
||||
# Create architecture_violations table
|
||||
op.create_table(
|
||||
'architecture_violations',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('scan_id', sa.Integer(), nullable=False),
|
||||
sa.Column('rule_id', sa.String(length=20), nullable=False),
|
||||
sa.Column('rule_name', sa.String(length=200), nullable=False),
|
||||
sa.Column('severity', sa.String(length=10), nullable=False),
|
||||
sa.Column('file_path', sa.String(length=500), nullable=False),
|
||||
sa.Column('line_number', sa.Integer(), nullable=False),
|
||||
sa.Column('message', sa.Text(), nullable=False),
|
||||
sa.Column('context', sa.Text(), nullable=True),
|
||||
sa.Column('suggestion', sa.Text(), nullable=True),
|
||||
sa.Column('status', sa.String(length=20), server_default='open', nullable=True),
|
||||
sa.Column('assigned_to', sa.Integer(), nullable=True),
|
||||
sa.Column('resolved_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column('resolved_by', sa.Integer(), nullable=True),
|
||||
sa.Column('resolution_note', sa.Text(), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text("(datetime('now'))"), nullable=False),
|
||||
sa.ForeignKeyConstraint(['assigned_to'], ['users.id'], ),
|
||||
sa.ForeignKeyConstraint(['resolved_by'], ['users.id'], ),
|
||||
sa.ForeignKeyConstraint(['scan_id'], ['architecture_scans.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index(op.f('ix_architecture_violations_file_path'), 'architecture_violations', ['file_path'], unique=False)
|
||||
op.create_index(op.f('ix_architecture_violations_id'), 'architecture_violations', ['id'], unique=False)
|
||||
op.create_index(op.f('ix_architecture_violations_rule_id'), 'architecture_violations', ['rule_id'], unique=False)
|
||||
op.create_index(op.f('ix_architecture_violations_scan_id'), 'architecture_violations', ['scan_id'], unique=False)
|
||||
op.create_index(op.f('ix_architecture_violations_severity'), 'architecture_violations', ['severity'], unique=False)
|
||||
op.create_index(op.f('ix_architecture_violations_status'), 'architecture_violations', ['status'], unique=False)
|
||||
|
||||
# Create violation_assignments table
|
||||
op.create_table(
|
||||
'violation_assignments',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('violation_id', sa.Integer(), nullable=False),
|
||||
sa.Column('user_id', sa.Integer(), nullable=False),
|
||||
sa.Column('assigned_at', sa.DateTime(timezone=True), server_default=sa.text("(datetime('now'))"), nullable=False),
|
||||
sa.Column('assigned_by', sa.Integer(), nullable=True),
|
||||
sa.Column('due_date', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column('priority', sa.String(length=10), server_default='medium', nullable=True),
|
||||
sa.ForeignKeyConstraint(['assigned_by'], ['users.id'], ),
|
||||
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
|
||||
sa.ForeignKeyConstraint(['violation_id'], ['architecture_violations.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index(op.f('ix_violation_assignments_id'), 'violation_assignments', ['id'], unique=False)
|
||||
op.create_index(op.f('ix_violation_assignments_violation_id'), 'violation_assignments', ['violation_id'], unique=False)
|
||||
|
||||
# Create violation_comments table
|
||||
op.create_table(
|
||||
'violation_comments',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('violation_id', sa.Integer(), nullable=False),
|
||||
sa.Column('user_id', sa.Integer(), nullable=False),
|
||||
sa.Column('comment', sa.Text(), nullable=False),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text("(datetime('now'))"), nullable=False),
|
||||
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
|
||||
sa.ForeignKeyConstraint(['violation_id'], ['architecture_violations.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index(op.f('ix_violation_comments_id'), 'violation_comments', ['id'], unique=False)
|
||||
op.create_index(op.f('ix_violation_comments_violation_id'), 'violation_comments', ['violation_id'], unique=False)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# Drop tables in reverse order (to respect foreign key constraints)
|
||||
op.drop_index(op.f('ix_violation_comments_violation_id'), table_name='violation_comments')
|
||||
op.drop_index(op.f('ix_violation_comments_id'), table_name='violation_comments')
|
||||
op.drop_table('violation_comments')
|
||||
|
||||
op.drop_index(op.f('ix_violation_assignments_violation_id'), table_name='violation_assignments')
|
||||
op.drop_index(op.f('ix_violation_assignments_id'), table_name='violation_assignments')
|
||||
op.drop_table('violation_assignments')
|
||||
|
||||
op.drop_index(op.f('ix_architecture_violations_status'), table_name='architecture_violations')
|
||||
op.drop_index(op.f('ix_architecture_violations_severity'), table_name='architecture_violations')
|
||||
op.drop_index(op.f('ix_architecture_violations_scan_id'), table_name='architecture_violations')
|
||||
op.drop_index(op.f('ix_architecture_violations_rule_id'), table_name='architecture_violations')
|
||||
op.drop_index(op.f('ix_architecture_violations_id'), table_name='architecture_violations')
|
||||
op.drop_index(op.f('ix_architecture_violations_file_path'), table_name='architecture_violations')
|
||||
op.drop_table('architecture_violations')
|
||||
|
||||
op.drop_index(op.f('ix_architecture_rules_rule_id'), table_name='architecture_rules')
|
||||
op.drop_index(op.f('ix_architecture_rules_id'), table_name='architecture_rules')
|
||||
op.drop_table('architecture_rules')
|
||||
|
||||
op.drop_index(op.f('ix_architecture_scans_timestamp'), table_name='architecture_scans')
|
||||
op.drop_index(op.f('ix_architecture_scans_id'), table_name='architecture_scans')
|
||||
op.drop_table('architecture_scans')
|
||||
120
app/models/architecture_scan.py
Normal file
120
app/models/architecture_scan.py
Normal file
@@ -0,0 +1,120 @@
|
||||
"""
|
||||
Architecture Scan Models
|
||||
Database models for tracking code quality scans and violations
|
||||
"""
|
||||
|
||||
from sqlalchemy import Column, Integer, String, Float, DateTime, Text, Boolean, ForeignKey, JSON
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.sql import func
|
||||
from app.database import Base
|
||||
|
||||
|
||||
class ArchitectureScan(Base):
|
||||
"""Represents a single run of the architecture validator"""
|
||||
__tablename__ = "architecture_scans"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
timestamp = Column(DateTime(timezone=True), server_default=func.now(), nullable=False, index=True)
|
||||
total_files = Column(Integer, default=0)
|
||||
total_violations = Column(Integer, default=0)
|
||||
errors = Column(Integer, default=0)
|
||||
warnings = Column(Integer, default=0)
|
||||
duration_seconds = Column(Float, default=0.0)
|
||||
triggered_by = Column(String(100)) # 'manual', 'scheduled', 'ci/cd'
|
||||
git_commit_hash = Column(String(40))
|
||||
|
||||
# Relationship to violations
|
||||
violations = relationship("ArchitectureViolation", back_populates="scan", cascade="all, delete-orphan")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<ArchitectureScan(id={self.id}, violations={self.total_violations}, errors={self.errors})>"
|
||||
|
||||
|
||||
class ArchitectureViolation(Base):
|
||||
"""Represents a single architectural violation found during a scan"""
|
||||
__tablename__ = "architecture_violations"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
scan_id = Column(Integer, ForeignKey("architecture_scans.id"), nullable=False, index=True)
|
||||
rule_id = Column(String(20), nullable=False, index=True) # e.g., 'API-001'
|
||||
rule_name = Column(String(200), nullable=False)
|
||||
severity = Column(String(10), nullable=False, index=True) # 'error', 'warning', 'info'
|
||||
file_path = Column(String(500), nullable=False, index=True)
|
||||
line_number = Column(Integer, nullable=False)
|
||||
message = Column(Text, nullable=False)
|
||||
context = Column(Text) # Code snippet
|
||||
suggestion = Column(Text)
|
||||
status = Column(String(20), default='open', index=True) # 'open', 'assigned', 'resolved', 'ignored', 'technical_debt'
|
||||
assigned_to = Column(Integer, ForeignKey("users.id"))
|
||||
resolved_at = Column(DateTime(timezone=True))
|
||||
resolved_by = Column(Integer, ForeignKey("users.id"))
|
||||
resolution_note = Column(Text)
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now(), nullable=False)
|
||||
|
||||
# Relationships
|
||||
scan = relationship("ArchitectureScan", back_populates="violations")
|
||||
assigned_user = relationship("User", foreign_keys=[assigned_to], backref="assigned_violations")
|
||||
resolver = relationship("User", foreign_keys=[resolved_by], backref="resolved_violations")
|
||||
assignments = relationship("ViolationAssignment", back_populates="violation", cascade="all, delete-orphan")
|
||||
comments = relationship("ViolationComment", back_populates="violation", cascade="all, delete-orphan")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<ArchitectureViolation(id={self.id}, rule={self.rule_id}, file={self.file_path}:{self.line_number})>"
|
||||
|
||||
|
||||
class ArchitectureRule(Base):
|
||||
"""Architecture rules configuration (from YAML with database overrides)"""
|
||||
__tablename__ = "architecture_rules"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
rule_id = Column(String(20), unique=True, nullable=False, index=True) # e.g., 'API-001'
|
||||
category = Column(String(50), nullable=False) # 'api_endpoint', 'service_layer', etc.
|
||||
name = Column(String(200), nullable=False)
|
||||
description = Column(Text)
|
||||
severity = Column(String(10), nullable=False) # Can override default from YAML
|
||||
enabled = Column(Boolean, default=True, nullable=False)
|
||||
custom_config = Column(JSON) # For rule-specific settings
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now(), nullable=False)
|
||||
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now(), nullable=False)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<ArchitectureRule(id={self.rule_id}, name={self.name}, enabled={self.enabled})>"
|
||||
|
||||
|
||||
class ViolationAssignment(Base):
|
||||
"""Tracks assignment of violations to developers"""
|
||||
__tablename__ = "violation_assignments"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
violation_id = Column(Integer, ForeignKey("architecture_violations.id"), nullable=False, index=True)
|
||||
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||||
assigned_at = Column(DateTime(timezone=True), server_default=func.now(), nullable=False)
|
||||
assigned_by = Column(Integer, ForeignKey("users.id"))
|
||||
due_date = Column(DateTime(timezone=True))
|
||||
priority = Column(String(10), default='medium') # 'low', 'medium', 'high', 'critical'
|
||||
|
||||
# Relationships
|
||||
violation = relationship("ArchitectureViolation", back_populates="assignments")
|
||||
user = relationship("User", foreign_keys=[user_id], backref="violation_assignments")
|
||||
assigner = relationship("User", foreign_keys=[assigned_by], backref="assigned_by_me")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<ViolationAssignment(id={self.id}, violation_id={self.violation_id}, user_id={self.user_id})>"
|
||||
|
||||
|
||||
class ViolationComment(Base):
|
||||
"""Comments on violations for collaboration"""
|
||||
__tablename__ = "violation_comments"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
violation_id = Column(Integer, ForeignKey("architecture_violations.id"), nullable=False, index=True)
|
||||
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||||
comment = Column(Text, nullable=False)
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now(), nullable=False)
|
||||
|
||||
# Relationships
|
||||
violation = relationship("ArchitectureViolation", back_populates="comments")
|
||||
user = relationship("User", backref="violation_comments")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<ViolationComment(id={self.id}, violation_id={self.violation_id}, user_id={self.user_id})>"
|
||||
Reference in New Issue
Block a user