import * as fs from 'fs'; import * as path from 'path'; import { GuardrailViolation } from './GuardrailViolation'; import { ALLOWED_VIOLATIONS } from './allowed-violations'; /** * Architecture guardrail scanner * * Scans the website codebase for architectural violations * and compares them against an allowlist. */ export class ArchitectureGuardrails { private violations: GuardrailViolation[] = []; /** * Scan all files and detect violations */ scan(): GuardrailViolation[] { this.violations = []; // Check for forbidden hooks directory this.checkForbiddenHooksDirectory(); // Scan specific directories this.scanDirectory('apps/website/app', this.scanAppDirectory.bind(this)); this.scanDirectory('apps/website/lib/page-queries', this.scanPageQueryDirectory.bind(this)); this.scanDirectory('apps/website/templates', this.scanTemplateDirectory.bind(this)); this.scanDirectory('apps/website/lib/services', this.scanServiceDirectory.bind(this)); this.scanDirectory('apps/website/lib/view-models', this.scanViewModelDirectory.bind(this)); this.scanDirectory('apps/website/lib/presenters', this.scanPresenterDirectory.bind(this)); this.scanDirectory('apps/website/lib/display-objects', this.scanDisplayObjectDirectory.bind(this)); this.scanDirectory('apps/website/components', this.scanComponentDirectory.bind(this)); this.scanDirectory('apps/website/lib/utilities', this.scanUtilityDirectory.bind(this)); this.scanDirectory('apps/website/lib/api', this.scanApiDirectory.bind(this)); this.scanDirectory('apps/website/lib/di', this.scanDiDirectory.bind(this)); this.scanDirectory('apps/website/hooks', this.scanHooksDirectory.bind(this)); return this.violations; } /** * Get violations after filtering by allowlist */ getFilteredViolations(): GuardrailViolation[] { const allViolations = this.scan(); return allViolations.filter(violation => { const allowed = ALLOWED_VIOLATIONS[violation.ruleName] || []; return !allowed.includes(violation.filePath); }); } /** * Check if allowlist has stale entries */ findStaleAllowlistEntries(): string[] { const allViolations = this.scan(); const staleEntries: string[] = []; for (const [ruleName, allowedFiles] of Object.entries(ALLOWED_VIOLATIONS)) { for (const allowedFile of allowedFiles) { const stillExists = allViolations.some(v => v.ruleName === ruleName && v.filePath === allowedFile ); if (!stillExists) { staleEntries.push(`${ruleName}: ${allowedFile}`); } } } return staleEntries; } // ============================================================================ // DIRECTORY SCANNERS // ============================================================================ private scanDirectory(dirPath: string, scanner: (filePath: string, content: string) => void): void { if (!fs.existsSync(dirPath)) return; const readDirRecursive = (currentPath: string): void => { const entries = fs.readdirSync(currentPath, { withFileTypes: true }); for (const entry of entries) { const fullPath = path.join(currentPath, entry.name); if (entry.isDirectory()) { readDirRecursive(fullPath); } else if (entry.isFile()) { const relativePath = this.normalizePath(fullPath); if (!relativePath.endsWith('.ts') && !relativePath.endsWith('.tsx')) continue; const content = fs.readFileSync(fullPath, 'utf-8'); scanner(relativePath, content); } } }; readDirRecursive(dirPath); } private scanAppDirectory(filePath: string, content: string): void { // Rule 1: RSC boundary guardrails for page.tsx files if (filePath.match(/app\/.*\/page\.tsx$/)) { this.checkContainerManager(filePath, content); this.checkPageDataFetcherFetch(filePath, content); this.checkViewModelsImport(filePath, content); this.checkPresenterImport(filePath, content); this.checkIntlUsage(filePath, content); this.checkSortingFiltering(filePath, content); this.checkDisplayObjectsImport(filePath, content); this.checkServerSafeServicesImport(filePath, content); this.checkDIImport(filePath, content); this.checkLocalHelpers(filePath, content); this.checkObjectConstruction(filePath, content); this.checkContainerManagerCalls(filePath, content); this.checkNoTemplatesInApp(filePath, content); } // Rule 5: Forbid client-side write fetch if (filePath.match(/app\/.*\/.*\.tsx$/)) { this.checkClientWriteFetch(filePath, content); } // Rule 7: Server actions guardrails if (filePath.match(/app\/.*\/actions\.ts$/)) { this.checkServerActions(filePath, content); } // Rule 8: Forbid 'as any' usage in all files this.checkAsAnyUsage(filePath, content); // Rule 9: Model taxonomy - variable naming this.checkVariableNaming(filePath, content); // Rule 10: Generated DTO isolation this.checkGeneratedDTOImport(filePath, content); // Rule 11: Filename rules for app directory this.checkAppFilenameRules(filePath, content); } private scanPageQueryDirectory(filePath: string, content: string): void { // Rule 1: Forbid ContainerManager in page queries this.checkContainerManager(filePath, content); // Rule 2: Forbid PageDataFetcher.fetch() in page queries this.checkPageDataFetcherFetch(filePath, content); // Rule 3: Forbid view-models imports in page queries this.checkViewModelsImport(filePath, content); // Rule 4: Forbid Intl usage in page queries this.checkIntlUsage(filePath, content); // Rule 4: Page Query guardrails - additional checks this.checkPresenterImport(filePath, content); this.checkDisplayObjectsImport(filePath, content); this.checkDIImport(filePath, content); this.checkSortingFiltering(filePath, content); this.checkNullReturns(filePath, content); // Rule 8: Forbid 'as any' usage this.checkAsAnyUsage(filePath, content); // Rule 9: Model taxonomy - variable naming this.checkVariableNaming(filePath, content); // Rule 10: Generated DTO isolation this.checkGeneratedDTOImport(filePath, content); // Rule 11: Filename rules for page queries this.checkPageQueryFilenameRules(filePath, content); } private scanTemplateDirectory(filePath: string, content: string): void { // Rule 2: Template purity guardrails this.checkTemplateImports(filePath, content); this.checkPresenterImport(filePath, content); this.checkIntlUsage(filePath, content); this.checkTemplateStateHooks(filePath, content); this.checkTemplateComputations(filePath, content); this.checkTemplateImportsFromRestricted(filePath, content); // Rule 8: Forbid 'as any' usage this.checkAsAnyUsage(filePath, content); // Rule 9: Model taxonomy - variable naming and type rules this.checkVariableNaming(filePath, content); this.checkTemplateViewDataSignature(filePath, content); this.checkTemplateExports(filePath, content); // Rule 10: Generated DTO isolation this.checkGeneratedDTOImport(filePath, content); // Rule 11: Filename rules for templates this.checkTemplateFilenameRules(filePath, content); } private scanServiceDirectory(filePath: string, content: string): void { // Rule 3: Services guardrails this.checkViewModelsImport(filePath, content); this.checkDisplayObjectsImport(filePath, content); this.checkServiceStatefulness(filePath, content); this.checkServiceBlockers(filePath, content); // Rule 5: Services should be server-safe this.checkServiceNaming(filePath, content); // Rule 8: Forbid 'as any' usage this.checkAsAnyUsage(filePath, content); // Rule 9: Model taxonomy - variable naming this.checkVariableNaming(filePath, content); // Rule 10: Generated DTO isolation this.checkGeneratedDTOImport(filePath, content); } private scanViewModelDirectory(filePath: string, content: string): void { // Rule 5: Forbid Intl usage in view-models this.checkIntlUsage(filePath, content); // Rule 6: Client-only guardrails this.checkUseClientDirective(filePath, content); this.checkViewModelPageQueryImports(filePath, content); // Rule 8: Forbid 'as any' usage this.checkAsAnyUsage(filePath, content); // Rule 9: Model taxonomy - variable naming this.checkVariableNaming(filePath, content); // Rule 10: Generated DTO isolation this.checkGeneratedDTOImport(filePath, content); } private scanPresenterDirectory(filePath: string, content: string): void { // Rule 5: Forbid Intl usage in presenters this.checkIntlUsage(filePath, content); // Rule 6: Client-only guardrails - presenters should not use HTTP this.checkHttpCalls(filePath, content); // Rule 8: Forbid 'as any' usage this.checkAsAnyUsage(filePath, content); // Rule 9: Model taxonomy - variable naming this.checkVariableNaming(filePath, content); // Rule 10: Generated DTO isolation this.checkGeneratedDTOImport(filePath, content); } private scanDisplayObjectDirectory(filePath: string, content: string): void { // Rule 3: Display Object guardrails this.checkIntlUsage(filePath, content); this.checkDisplayObjectImports(filePath, content); this.checkDisplayObjectExports(filePath, content); // Rule 8: Forbid 'as any' usage this.checkAsAnyUsage(filePath, content); // Rule 9: Model taxonomy - variable naming this.checkVariableNaming(filePath, content); // Rule 10: Generated DTO isolation this.checkGeneratedDTOImport(filePath, content); } private scanComponentDirectory(filePath: string, content: string): void { // Rule 5: Forbid Intl usage in components this.checkIntlUsage(filePath, content); // Rule 7: Client-side write fetch this.checkClientWriteFetch(filePath, content); // Rule 8: Forbid 'as any' usage this.checkAsAnyUsage(filePath, content); // Rule 9: Model taxonomy - variable naming this.checkVariableNaming(filePath, content); // Rule 10: Generated DTO isolation this.checkGeneratedDTOImport(filePath, content); } private scanUtilityDirectory(filePath: string, content: string): void { // Rule 5: Forbid Intl usage in utilities this.checkIntlUsage(filePath, content); // Rule 8: Forbid 'as any' usage this.checkAsAnyUsage(filePath, content); // Rule 9: Model taxonomy - variable naming this.checkVariableNaming(filePath, content); // Rule 10: Generated DTO isolation this.checkGeneratedDTOImport(filePath, content); } private scanApiDirectory(filePath: string, content: string): void { // Rule 8: Forbid 'as any' usage this.checkAsAnyUsage(filePath, content); // Rule 9: Model taxonomy - variable naming this.checkVariableNaming(filePath, content); } private scanDiDirectory(filePath: string, content: string): void { // Rule 8: Forbid 'as any' usage this.checkAsAnyUsage(filePath, content); // Rule 9: Model taxonomy - variable naming this.checkVariableNaming(filePath, content); } private scanHooksDirectory(filePath: string, content: string): void { // Rule 8: Forbid 'as any' usage this.checkAsAnyUsage(filePath, content); // Rule 9: Model taxonomy - variable naming this.checkVariableNaming(filePath, content); // Rule 10: Generated DTO isolation this.checkGeneratedDTOImport(filePath, content); } // ============================================================================ // VIOLATION CHECKS - RSC BOUNDARY GUARDRAILS // ============================================================================ private checkContainerManager(filePath: string, content: string): void { const lines = content.split('\n'); lines.forEach((line, index) => { if (line.includes('ContainerManager') && !line.trim().startsWith('//')) { this.addViolation( 'no-container-manager-in-server', filePath, index + 1, 'ContainerManager usage forbidden in server code' ); } }); } private checkPageDataFetcherFetch(filePath: string, content: string): void { const lines = content.split('\n'); lines.forEach((line, index) => { if (line.includes('PageDataFetcher.fetch(') && !line.trim().startsWith('//')) { this.addViolation( 'no-page-data-fetcher-fetch-in-server', filePath, index + 1, 'PageDataFetcher.fetch() forbidden in server code' ); } }); } private checkViewModelsImport(filePath: string, content: string): void { const lines = content.split('\n'); lines.forEach((line, index) => { // Check for both single and double quotes if ((line.includes("from '@/lib/view-models/") || line.includes('from "@/lib/view-models/') || line.includes("from '@/lib/presenters/") || line.includes('from "@/lib/presenters/')) && !line.trim().startsWith('//')) { this.addViolation( 'no-view-models-in-server', filePath, index + 1, 'ViewModels or Presenters import forbidden in server code' ); } }); } private checkPresenterImport(filePath: string, content: string): void { const lines = content.split('\n'); lines.forEach((line, index) => { if ((line.includes("from '@/lib/presenters/") || line.includes('from "@/lib/presenters/')) && !line.trim().startsWith('//')) { this.addViolation( 'no-presenters-in-server', filePath, index + 1, 'Presenter import forbidden in server code' ); } }); } private checkIntlUsage(filePath: string, content: string): void { const lines = content.split('\n'); lines.forEach((line, index) => { if ((line.includes('Intl.') || line.includes('.toLocale')) && !line.trim().startsWith('//')) { this.addViolation( 'no-intl-in-presentation', filePath, index + 1, 'Intl.* or toLocale* usage forbidden in presentation paths' ); } }); } private checkSortingFiltering(filePath: string, content: string): void { const lines = content.split('\n'); const patterns = [ /\.sort\(/, /\.filter\(/, /\.reduce\(/, /\bsort\(/, /\bfilter\(/, /\breduce\(/ ]; lines.forEach((line, index) => { // Skip comments and trivial null checks if (line.trim().startsWith('//') || line.includes('null check')) return; for (const pattern of patterns) { if (pattern.test(line)) { this.addViolation( 'no-sorting-filtering-in-server', filePath, index + 1, 'Sorting/filtering/reduce operations forbidden in server code' ); break; } } }); } private checkDisplayObjectsImport(filePath: string, content: string): void { const lines = content.split('\n'); lines.forEach((line, index) => { if ((line.includes("from '@/lib/display-objects/") || line.includes('from "@/lib/display-objects/')) && !line.trim().startsWith('//')) { this.addViolation( 'no-display-objects-in-server', filePath, index + 1, 'DisplayObjects import forbidden in server code' ); } }); } private checkServerSafeServicesImport(filePath: string, content: string): void { const lines = content.split('\n'); lines.forEach((line, index) => { if ((line.includes("from '@/lib/services/") || line.includes('from "@/lib/services/')) && !line.trim().startsWith('//')) { // Check if it's explicitly marked as server-safe if (!content.includes('// @server-safe') && !content.includes('/* @server-safe */')) { this.addViolation( 'no-unsafe-services-in-server', filePath, index + 1, 'Services import must be explicitly marked as server-safe' ); } } }); } private checkDIImport(filePath: string, content: string): void { const lines = content.split('\n'); lines.forEach((line, index) => { if ((line.includes("from '@/lib/di/") || line.includes('from "@/lib/di/')) && !line.trim().startsWith('//')) { this.addViolation( 'no-di-in-server', filePath, index + 1, 'DI import forbidden in server code' ); } }); } private checkLocalHelpers(filePath: string, content: string): void { // Look for function definitions that are not assert*/invariant* guards const lines = content.split('\n'); let inComment = false; lines.forEach((line, index) => { const trimmed = line.trim(); // Track comment blocks if (trimmed.startsWith('/*')) inComment = true; if (trimmed.endsWith('*/')) inComment = false; if (trimmed.startsWith('//') || inComment) return; // Check for function definitions if (/^(async\s+)?function\s+\w+/.test(trimmed) || /^(const|let|var)\s+\w+\s*=\s*(async\s+)?function/.test(trimmed) || /^(const|let|var)\s+\w+\s*=\s*\([^)]*\)\s*=>/.test(trimmed)) { // Allow assert* and invariant* functions if (!trimmed.match(/^(const|let|var)\s+(assert|invariant)\w+/) && !trimmed.match(/^function\s+(assert|invariant)\w+/)) { this.addViolation( 'no-local-helpers-in-server', filePath, index + 1, 'Local helper functions forbidden (only assert*/invariant* allowed)' ); } } }); } private checkObjectConstruction(filePath: string, content: string): void { const lines = content.split('\n'); lines.forEach((line, index) => { // Look for 'new SomeClass()' patterns if (/new\s+[A-Z]\w+\(/.test(line) && !line.trim().startsWith('//')) { this.addViolation( 'no-object-construction-in-server', filePath, index + 1, 'Object construction with new forbidden (use PageQueries)' ); } }); } private checkContainerManagerCalls(filePath: string, content: string): void { const lines = content.split('\n'); lines.forEach((line, index) => { if ((line.includes('ContainerManager.getInstance()') || line.includes('ContainerManager.getContainer()')) && !line.trim().startsWith('//')) { this.addViolation( 'no-container-manager-calls-in-server', filePath, index + 1, 'ContainerManager calls forbidden in server code' ); } }); } private checkNoTemplatesInApp(filePath: string, content: string): void { if (filePath.includes('/app/') && filePath.includes('Template.tsx')) { this.addViolation( 'no-templates-in-app', filePath, 1, '*Template.tsx files forbidden under app/' ); } } // ============================================================================ // VIOLATION CHECKS - TEMPLATE PURITY GUARDRAILS // ============================================================================ private checkTemplateImports(filePath: string, content: string): void { const lines = content.split('\n'); lines.forEach((line, index) => { if (line.includes("from '@/lib/view-models/") || line.includes("from '@/lib/presenters/") || line.includes("from '@/lib/display-objects/")) { if (!line.trim().startsWith('//')) { this.addViolation( 'no-view-models-in-templates', filePath, index + 1, 'ViewModels or DisplayObjects import forbidden in templates' ); } } }); } private checkTemplateStateHooks(filePath: string, content: string): void { const lines = content.split('\n'); lines.forEach((line, index) => { if ((line.includes('useMemo') || line.includes('useEffect') || line.includes('useState') || line.includes('useReducer')) && !line.trim().startsWith('//')) { this.addViolation( 'no-state-hooks-in-templates', filePath, index + 1, 'State hooks forbidden in templates (use *PageClient.tsx)' ); } }); } private checkTemplateComputations(filePath: string, content: string): void { const lines = content.split('\n'); lines.forEach((line, index) => { if ((line.includes('.filter(') || line.includes('.sort(') || line.includes('.reduce(')) && !line.trim().startsWith('//')) { this.addViolation( 'no-computations-in-templates', filePath, index + 1, 'Derived computations forbidden in templates' ); } }); } private checkTemplateImportsFromRestricted(filePath: string, content: string): void { const lines = content.split('\n'); const restrictedPaths = [ "from '@/lib/page-queries/", 'from "@/lib/page-queries/', "from '@/lib/services/", 'from "@/lib/services/', "from '@/lib/api/", 'from "@/lib/api/', "from '@/lib/di/", 'from "@/lib/di/', "from '@/lib/contracts/", 'from "@/lib/contracts/' ]; lines.forEach((line, index) => { for (const path of restrictedPaths) { if (line.includes(path) && !line.trim().startsWith('//')) { this.addViolation( 'no-restricted-imports-in-templates', filePath, index + 1, 'Templates cannot import from page-queries, services, api, di, or contracts' ); break; } } }); } private checkTemplateViewDataSignature(filePath: string, content: string): void { // Look for component function declarations const componentRegex = /export\s+(default\s+)?function\s+(\w+)\s*\(([^)]*)\)/g; let match; while ((match = componentRegex.exec(content)) !== null) { const params = match[3]?.trim(); if (params && !params.includes('ViewData') && !params.includes('viewData')) { this.addViolation( 'no-invalid-template-signature', filePath, 1, 'Template component must accept *ViewData type as first parameter' ); } } } private checkTemplateExports(filePath: string, content: string): void { // Look for exported functions that are not the main component const lines = content.split('\n'); let inComment = false; lines.forEach((line, index) => { const trimmed = line.trim(); if (trimmed.startsWith('/*')) inComment = true; if (trimmed.endsWith('*/')) inComment = false; if (trimmed.startsWith('//') || inComment) return; // Check for export function/const that's not the default component if ((trimmed.startsWith('export function') || trimmed.startsWith('export const') || trimmed.startsWith('export default function')) && !trimmed.includes('export default function')) { this.addViolation( 'no-template-helper-exports', filePath, index + 1, 'Templates must not export helper functions' ); } }); } private checkTemplateFilenameRules(filePath: string, content: string): void { if (filePath.includes('/templates/') && !filePath.endsWith('Template.tsx')) { this.addViolation( 'invalid-template-filename', filePath, 1, 'Template files must end with Template.tsx' ); } } // ============================================================================ // VIOLATION CHECKS - DISPLAY OBJECT GUARDRAILS // ============================================================================ private checkDisplayObjectImports(filePath: string, content: string): void { const lines = content.split('\n'); const forbiddenPaths = [ "from '@/lib/api/", 'from "@/lib/api/', "from '@/lib/services/", 'from "@/lib/services/', "from '@/lib/page-queries/", 'from "@/lib/page-queries/', "from '@/lib/view-models/", 'from "@/lib/view-models/', "from '@/lib/presenters/", 'from "@/lib/presenters/' ]; lines.forEach((line, index) => { for (const path of forbiddenPaths) { if (line.includes(path) && !line.trim().startsWith('//')) { this.addViolation( 'no-io-in-display-objects', filePath, index + 1, 'DisplayObjects cannot import from api, services, page-queries, or view-models' ); break; } } }); } private checkDisplayObjectExports(filePath: string, content: string): void { // Check that Display Objects only export class members const lines = content.split('\n'); lines.forEach((line, index) => { const trimmed = line.trim(); if ((trimmed.startsWith('export function') || trimmed.startsWith('export const') || trimmed.startsWith('export default function')) && !trimmed.includes('export default class') && !trimmed.includes('export class')) { this.addViolation( 'no-non-class-display-exports', filePath, index + 1, 'Display Objects must be class-based and export only classes' ); } }); } // ============================================================================ // VIOLATION CHECKS - PAGE QUERY GUARDRAILS // ============================================================================ private checkNullReturns(filePath: string, content: string): void { const lines = content.split('\n'); lines.forEach((line, index) => { if (line.includes('return null') && !line.trim().startsWith('//')) { this.addViolation( 'no-null-returns-in-page-queries', filePath, index + 1, 'PageQueries must return PageQueryResult union, not null' ); } }); } private checkPageQueryFilenameRules(filePath: string, content: string): void { if (filePath.includes('/page-queries/') && !filePath.endsWith('PageQuery.ts')) { this.addViolation( 'invalid-page-query-filename', filePath, 1, 'PageQuery files must end with PageQuery.ts' ); } } // ============================================================================ // VIOLATION CHECKS - SERVICES GUARDRAILS // ============================================================================ private checkServiceStatefulness(filePath: string, content: string): void { // Look for state storage on 'this' beyond injected dependencies const lines = content.split('\n'); lines.forEach((line, index) => { if (line.includes('this.') && !line.includes('this.') && // Basic check for this.property !line.trim().startsWith('//')) { // This is a simplified check - in practice would need more sophisticated analysis if (line.match(/this\.\w+\s*=\s*(?!inject|constructor)/)) { this.addViolation( 'no-service-state', filePath, index + 1, 'Services must be stateless (no state on this beyond dependencies)' ); } } }); } private checkServiceBlockers(filePath: string, content: string): void { const lines = content.split('\n'); lines.forEach((line, index) => { if (line.includes('blocker') && !line.trim().startsWith('//')) { this.addViolation( 'no-blockers-in-services', filePath, index + 1, 'Services cannot use blockers (client-only UX helpers)' ); } }); } private checkServiceNaming(filePath: string, content: string): void { // Check for proper variable naming in service methods const lines = content.split('\n'); lines.forEach((line, index) => { // Look for 'dto' variable name (forbidden) if (line.includes(' dto ') || line.includes(' dto=') || line.includes('(dto)')) { this.addViolation( 'no-dto-variable-name', filePath, index + 1, 'Variable name "dto" forbidden - use apiDto, pageDto, viewData, or commandDto' ); } }); } // ============================================================================ // VIOLATION CHECKS - CLIENT-ONLY GUARDRAILS // ============================================================================ private checkUseClientDirective(filePath: string, content: string): void { if (filePath.includes('/lib/view-models/') || filePath.includes('/lib/presenters/')) { const hasUseClient = content.includes("'use client'") || content.includes('"use client"'); if (!hasUseClient) { this.addViolation( 'no-use-client-directive', filePath, 1, 'ViewModels must have \'use client\' directive at top-level' ); } } } private checkViewModelPageQueryImports(filePath: string, content: string): void { const lines = content.split('\n'); lines.forEach((line, index) => { if ((line.includes("from '@/lib/page-queries/") || line.includes('from "@/lib/page-queries/') || line.includes("from '@/app/") || line.includes('from "@/app/')) && !line.trim().startsWith('//')) { this.addViolation( 'no-viewmodel-imports-from-server', filePath, index + 1, 'ViewModels cannot import from page-queries or app' ); } }); } private checkHttpCalls(filePath: string, content: string): void { const lines = content.split('\n'); lines.forEach((line, index) => { if ((line.includes('fetch(') || line.includes('axios.') || line.includes('apiClient.') || line.includes('http.')) && !line.trim().startsWith('//')) { this.addViolation( 'no-http-in-presenters', filePath, index + 1, 'Presenters/ViewModels cannot use HTTP calls' ); } }); } // ============================================================================ // VIOLATION CHECKS - WRITE BOUNDARY GUARDRAILS // ============================================================================ private checkClientWriteFetch(filePath: string, content: string): void { // Check for 'use client' directive const hasUseClient = content.includes("'use client'") || content.includes('"use client"'); if (!hasUseClient) return; // Check for fetch with write methods const writeMethods = ['POST', 'PUT', 'PATCH', 'DELETE']; const lines = content.split('\n'); lines.forEach((line, index) => { writeMethods.forEach(method => { if (line.includes(`method: '${method}'`) || line.includes(`method: "${method}"`)) { this.addViolation( 'no-client-write-fetch', filePath, index + 1, `Client-side fetch with ${method} method forbidden` ); } }); }); } private checkServerActions(filePath: string, content: string): void { const lines = content.split('\n'); lines.forEach((line, index) => { // Check for view-models or templates imports if ((line.includes("from '@/lib/view-models/") || line.includes('from "@/lib/view-models/') || line.includes("from '@/lib/presenters/") || line.includes('from "@/lib/presenters/') || line.includes("from '@/templates/") || line.includes('from "@/templates/')) && !line.trim().startsWith('//')) { this.addViolation( 'no-server-action-imports-from-client', filePath, index + 1, 'Server actions cannot import ViewModels or Templates' ); } // Check for ViewModel returns if (line.includes('return') && (line.includes('ViewModel') || line.includes('viewModel')) && !line.trim().startsWith('//')) { this.addViolation( 'no-server-action-viewmodel-returns', filePath, index + 1, 'Server actions must return primitives/redirect/revalidate, not ViewModels' ); } }); } // ============================================================================ // VIOLATION CHECKS - MODEL TAXONOMY GUARDRAILS // ============================================================================ private checkVariableNaming(filePath: string, content: string): void { const lines = content.split('\n'); lines.forEach((line, index) => { // Check for forbidden variable name 'dto' if (/\bdto\b/.test(line) && !line.trim().startsWith('//')) { this.addViolation( 'no-dto-variable-name', filePath, index + 1, 'Variable name "dto" forbidden - use apiDto, pageDto, viewData, or commandDto' ); } }); } private checkGeneratedDTOImport(filePath: string, content: string): void { const lines = content.split('\n'); const forbiddenPaths = [ "from '@/lib/types/generated/", 'from "@/lib/types/generated/' ]; // Check if file is in forbidden locations const isForbiddenLocation = filePath.includes('/templates/') || filePath.includes('/components/') || filePath.includes('/hooks/') || filePath.includes('/lib/hooks/'); if (isForbiddenLocation) { lines.forEach((line, index) => { for (const path of forbiddenPaths) { if (line.includes(path) && !line.trim().startsWith('//')) { this.addViolation( 'no-generated-dto-in-ui', filePath, index + 1, 'Generated DTOs forbidden in templates, components, or hooks' ); break; } } }); } // Check templates for any types imports if (filePath.includes('/templates/')) { lines.forEach((line, index) => { if ((line.includes("from '@/lib/types/") || line.includes('from "@/lib/types/')) && !line.trim().startsWith('//')) { this.addViolation( 'no-types-in-templates', filePath, index + 1, 'Templates cannot import from lib/types' ); } }); } } // ============================================================================ // VIOLATION CHECKS - FILENAME RULES // ============================================================================ private checkAppFilenameRules(filePath: string, content: string): void { if (!filePath.includes('/app/')) return; // Allowed extensions under app/ const allowedFiles = [ 'page.tsx', 'layout.tsx', 'loading.tsx', 'error.tsx', 'not-found.tsx', 'actions.ts' ]; const isAllowed = allowedFiles.some(allowed => filePath.endsWith(allowed)); if (!isAllowed) { // Check for forbidden patterns if (filePath.includes('Template.tsx') || filePath.includes('ViewModel.ts') || filePath.includes('Presenter.ts')) { this.addViolation( 'invalid-app-filename', filePath, 1, 'app/ directory can only contain page.tsx, layout.tsx, loading.tsx, error.tsx, not-found.tsx, or actions.ts' ); } } } // ============================================================================ // VIOLATION CHECKS - FORBIDDEN DIRECTORIES // ============================================================================ private checkForbiddenHooksDirectory(): void { const hooksPath = 'apps/website/hooks'; if (fs.existsSync(hooksPath)) { this.addViolation( 'no-hooks-directory', hooksPath, 1, 'apps/website/hooks directory forbidden - hooks must be in apps/website/lib/hooks' ); } } // ============================================================================ // VIOLATION CHECKS - GENERAL // ============================================================================ private checkAsAnyUsage(filePath: string, content: string): void { const lines = content.split('\n'); lines.forEach((line, index) => { // Check for 'as any' pattern if (/\bas any\b/.test(line) && !line.trim().startsWith('//')) { this.addViolation( 'no-as-any', filePath, index + 1, 'Type assertion "as any" is forbidden' ); } }); } // ============================================================================ // HELPERS // ============================================================================ private addViolation(ruleName: string, filePath: string, lineNumber: number, description: string): void { this.violations.push(new GuardrailViolation(ruleName, filePath, lineNumber, description)); } private normalizePath(filePath: string): string { // Convert absolute path to relative from workspace root const workspaceRoot = path.resolve('/Users/marcmintel/Projects/gridpilot'); let relative = path.relative(workspaceRoot, filePath); // Normalize to forward slashes return relative.replace(/\\/g, '/'); } }