refactor: migrate remaining routes to modules and enforce auto-discovery
MIGRATION: - Delete app/api/v1/vendor/analytics.py (duplicate - analytics module already auto-discovered) - Move usage routes from app/api/v1/vendor/usage.py to billing module - Move onboarding routes from app/api/v1/vendor/onboarding.py to marketplace module - Move features routes to billing module (admin + vendor) - Move inventory routes to inventory module (admin + vendor) - Move marketplace/letzshop routes to marketplace module - Move orders routes to orders module - Delete legacy letzshop service files (moved to marketplace module) DOCUMENTATION: - Add docs/development/migration/module-autodiscovery-migration.md with full migration history - Update docs/architecture/module-system.md with Entity Auto-Discovery Reference section - Add detailed sections for each entity type: routes, services, models, schemas, tasks, exceptions, templates, static files, locales, configuration ARCHITECTURE VALIDATION: - Add MOD-016: Routes must be in modules, not app/api/v1/ - Add MOD-017: Services must be in modules, not app/services/ - Add MOD-018: Tasks must be in modules, not app/tasks/ - Add MOD-019: Schemas must be in modules, not models/schema/ - Update scripts/validate_architecture.py with _validate_legacy_locations method - Update .architecture-rules/module.yaml with legacy location rules These rules enforce that all entities must be in self-contained modules. Legacy locations now trigger ERROR severity violations. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -224,6 +224,9 @@ class ArchitectureValidator:
|
||||
# Validate module structure
|
||||
self._validate_modules(target)
|
||||
|
||||
# Validate legacy locations (must be in modules)
|
||||
self._validate_legacy_locations(target)
|
||||
|
||||
return self.result
|
||||
|
||||
def validate_file(self, file_path: Path, quiet: bool = False) -> ValidationResult:
|
||||
@@ -4348,6 +4351,193 @@ class ArchitectureValidator:
|
||||
suggestion="Create 'exceptions.py' or 'exceptions/__init__.py'",
|
||||
)
|
||||
|
||||
def _validate_legacy_locations(self, target_path: Path):
|
||||
"""
|
||||
Validate that code is not in legacy locations (MOD-016 to MOD-019).
|
||||
|
||||
All routes, services, tasks, and schemas should be in module directories,
|
||||
not in the legacy centralized locations.
|
||||
"""
|
||||
print("🚫 Checking legacy locations...")
|
||||
|
||||
# MOD-016: Routes must be in modules, not app/api/v1/
|
||||
self._check_legacy_routes(target_path)
|
||||
|
||||
# MOD-017: Services must be in modules, not app/services/
|
||||
self._check_legacy_services(target_path)
|
||||
|
||||
# MOD-018: Tasks must be in modules, not app/tasks/
|
||||
self._check_legacy_tasks(target_path)
|
||||
|
||||
# MOD-019: Schemas must be in modules, not models/schema/
|
||||
self._check_legacy_schemas(target_path)
|
||||
|
||||
def _check_legacy_routes(self, target_path: Path):
|
||||
"""MOD-016: Check for routes in legacy app/api/v1/ locations."""
|
||||
# Check vendor routes
|
||||
vendor_api_path = target_path / "app" / "api" / "v1" / "vendor"
|
||||
if vendor_api_path.exists():
|
||||
for py_file in vendor_api_path.glob("*.py"):
|
||||
if py_file.name == "__init__.py":
|
||||
continue
|
||||
# Allow auth.py for now (core authentication)
|
||||
if py_file.name == "auth.py":
|
||||
continue
|
||||
|
||||
# Check for noqa comment
|
||||
content = py_file.read_text()
|
||||
if "noqa: mod-016" in content.lower():
|
||||
continue
|
||||
|
||||
self._add_violation(
|
||||
rule_id="MOD-016",
|
||||
rule_name="Routes must be in modules, not app/api/v1/",
|
||||
severity=Severity.ERROR,
|
||||
file_path=py_file,
|
||||
line_number=1,
|
||||
message=f"Route file '{py_file.name}' in legacy location - should be in module",
|
||||
context="app/api/v1/vendor/",
|
||||
suggestion="Move to app/modules/{module}/routes/api/vendor.py",
|
||||
)
|
||||
|
||||
# Check admin routes
|
||||
admin_api_path = target_path / "app" / "api" / "v1" / "admin"
|
||||
if admin_api_path.exists():
|
||||
for py_file in admin_api_path.glob("*.py"):
|
||||
if py_file.name == "__init__.py":
|
||||
continue
|
||||
# Allow auth.py for now (core authentication)
|
||||
if py_file.name == "auth.py":
|
||||
continue
|
||||
|
||||
# Check for noqa comment
|
||||
content = py_file.read_text()
|
||||
if "noqa: mod-016" in content.lower():
|
||||
continue
|
||||
|
||||
self._add_violation(
|
||||
rule_id="MOD-016",
|
||||
rule_name="Routes must be in modules, not app/api/v1/",
|
||||
severity=Severity.ERROR,
|
||||
file_path=py_file,
|
||||
line_number=1,
|
||||
message=f"Route file '{py_file.name}' in legacy location - should be in module",
|
||||
context="app/api/v1/admin/",
|
||||
suggestion="Move to app/modules/{module}/routes/api/admin.py",
|
||||
)
|
||||
|
||||
def _check_legacy_services(self, target_path: Path):
|
||||
"""MOD-017: Check for services in legacy app/services/ location."""
|
||||
services_path = target_path / "app" / "services"
|
||||
if not services_path.exists():
|
||||
return
|
||||
|
||||
for py_file in services_path.glob("*.py"):
|
||||
if py_file.name == "__init__.py":
|
||||
continue
|
||||
|
||||
# Check for noqa comment
|
||||
content = py_file.read_text()
|
||||
if "noqa: mod-017" in content.lower():
|
||||
continue
|
||||
|
||||
# Check if file is a pure re-export (only imports, no class/def)
|
||||
lines = content.split("\n")
|
||||
has_definitions = any(
|
||||
re.match(r"^(class|def|async def)\s+\w+", line)
|
||||
for line in lines
|
||||
)
|
||||
|
||||
# If it's a re-export only file, it's a warning not error
|
||||
if not has_definitions:
|
||||
# Check if it imports from modules
|
||||
imports_from_module = "from app.modules." in content
|
||||
if imports_from_module:
|
||||
# Re-export from module - this is acceptable during migration
|
||||
continue
|
||||
|
||||
self._add_violation(
|
||||
rule_id="MOD-017",
|
||||
rule_name="Services must be in modules, not app/services/",
|
||||
severity=Severity.ERROR,
|
||||
file_path=py_file,
|
||||
line_number=1,
|
||||
message=f"Service file '{py_file.name}' in legacy location - should be in module",
|
||||
context="app/services/",
|
||||
suggestion="Move to app/modules/{module}/services/",
|
||||
)
|
||||
|
||||
def _check_legacy_tasks(self, target_path: Path):
|
||||
"""MOD-018: Check for tasks in legacy app/tasks/ location."""
|
||||
tasks_path = target_path / "app" / "tasks"
|
||||
if not tasks_path.exists():
|
||||
return
|
||||
|
||||
for py_file in tasks_path.glob("*.py"):
|
||||
if py_file.name == "__init__.py":
|
||||
continue
|
||||
# Allow dispatcher.py (infrastructure)
|
||||
if py_file.name == "dispatcher.py":
|
||||
continue
|
||||
|
||||
# Check for noqa comment
|
||||
content = py_file.read_text()
|
||||
if "noqa: mod-018" in content.lower():
|
||||
continue
|
||||
|
||||
self._add_violation(
|
||||
rule_id="MOD-018",
|
||||
rule_name="Tasks must be in modules, not app/tasks/",
|
||||
severity=Severity.ERROR,
|
||||
file_path=py_file,
|
||||
line_number=1,
|
||||
message=f"Task file '{py_file.name}' in legacy location - should be in module",
|
||||
context="app/tasks/",
|
||||
suggestion="Move to app/modules/{module}/tasks/",
|
||||
)
|
||||
|
||||
def _check_legacy_schemas(self, target_path: Path):
|
||||
"""MOD-019: Check for schemas in legacy models/schema/ location."""
|
||||
schemas_path = target_path / "models" / "schema"
|
||||
if not schemas_path.exists():
|
||||
return
|
||||
|
||||
for py_file in schemas_path.glob("*.py"):
|
||||
if py_file.name == "__init__.py":
|
||||
continue
|
||||
# Allow auth.py (core authentication schemas)
|
||||
if py_file.name == "auth.py":
|
||||
continue
|
||||
|
||||
# Check for noqa comment
|
||||
content = py_file.read_text()
|
||||
if "noqa: mod-019" in content.lower():
|
||||
continue
|
||||
|
||||
# Check if file is a pure re-export
|
||||
lines = content.split("\n")
|
||||
has_definitions = any(
|
||||
re.match(r"^class\s+\w+", line)
|
||||
for line in lines
|
||||
)
|
||||
|
||||
# If it's a re-export only file, allow it during migration
|
||||
if not has_definitions:
|
||||
imports_from_module = "from app.modules." in content
|
||||
if imports_from_module:
|
||||
continue
|
||||
|
||||
self._add_violation(
|
||||
rule_id="MOD-019",
|
||||
rule_name="Schemas must be in modules, not models/schema/",
|
||||
severity=Severity.ERROR,
|
||||
file_path=py_file,
|
||||
line_number=1,
|
||||
message=f"Schema file '{py_file.name}' in legacy location - should be in module",
|
||||
context="models/schema/",
|
||||
suggestion="Move to app/modules/{module}/schemas/",
|
||||
)
|
||||
|
||||
def _get_rule(self, rule_id: str) -> dict[str, Any]:
|
||||
"""Get rule configuration by ID"""
|
||||
# Look in different rule categories
|
||||
|
||||
Reference in New Issue
Block a user