diff --git a/alembic/versions/7a7ce92593d5_add_architecture_quality_tracking_tables.py b/alembic/versions/7a7ce92593d5_add_architecture_quality_tracking_tables.py new file mode 100644 index 00000000..6fc55203 --- /dev/null +++ b/alembic/versions/7a7ce92593d5_add_architecture_quality_tracking_tables.py @@ -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') diff --git a/app/models/architecture_scan.py b/app/models/architecture_scan.py new file mode 100644 index 00000000..033250a3 --- /dev/null +++ b/app/models/architecture_scan.py @@ -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"" + + +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"" + + +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"" + + +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"" + + +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""