---
name: arch-guard
user-invocable: false
invocation: [auto]
tier: core
description: "Generate architecture boundary tests that mechanically enforce layer constraints. Use when adding new modules, refactoring layers, or after detecting circular dependencies. Creates import-rule tests (e.g., 'UI must not import DB') that fail CI on violation. Must use this skill when user mentions layer enforcement, dependency rules, or architectural boundaries — even casually like 'make sure services don't import controllers'."
triggers: [arch guard, architecture test, layer test, boundary test, structural test, arch validation]
priority: 60
---

# Arch Guard — Architecture Boundary Test Generator

> **Principle**: "Mechanical enforcement over documentation." If a rule exists only in docs, it will be violated. Turn architecture constraints into failing tests.

## When to Use

| Scenario | Signal |
|----------|--------|
| `vibe init` / `vibe update` | Auto-generate for detected stack |
| New layer/module added | Boundaries need enforcement |
| Architecture violation found in review | Prevent recurrence with test |
| "Services should not import UI" type rules | Turn into automated check |

## Core Flow

```
DETECT → INFER → GENERATE → VERIFY
```

### Step 1: DETECT — Identify Project Architecture

Analyze the project to determine its layer structure:

```
Parallel exploration:
- Agent 1: Scan directory structure (src/, app/, lib/, etc.)
- Agent 2: Read existing architecture docs (CLAUDE.md, README, ADR)
- Agent 3: Analyze import graph (which files import what)
```

**Common patterns to detect:**

| Pattern | Layers | Typical Stacks |
|---------|--------|----------------|
| MVC | Controller → Service → Model | Rails, NestJS, Spring |
| Clean Architecture | UI → Application → Domain → Infrastructure | General |
| Feature-based | Feature A ↛ Feature B internals | Next.js, React |
| Hexagonal | Adapters → Ports → Domain | DDD projects |
| Component hierarchy | Page → Feature → Shared → UI Primitives | Frontend |

### Step 2: INFER — Define Boundary Rules

From detected structure, generate rules:

```typescript
// Rule format
interface ArchRule {
  name: string;           // "service-no-ui-import"
  from: string;           // Glob pattern: "src/services/**"
  cannotImport: string[]; // ["src/components/**", "src/pages/**"]
  canImport: string[];    // ["src/models/**", "src/utils/**"]
  reason: string;         // "Services must be UI-agnostic"
}
```

**Default rules by stack:**

| Stack | Rule |
|-------|------|
| Next.js / React | `components/` cannot import from `pages/` or `app/` |
| Next.js / React | `lib/` cannot import from `components/` |
| NestJS | `*.service.ts` cannot import from `*.controller.ts` |
| NestJS | `*.module.ts` is the only valid cross-boundary import |
| General TS | `src/domain/` cannot import from `src/infra/` |
| General TS | No circular dependencies between top-level dirs |
| Python Django | `models.py` cannot import from `views.py` |
| Python FastAPI | `schemas/` cannot import from `routers/` |

### Step 3: GENERATE — Create Test File

Output: `tests/arch-guard.test.ts` (or equivalent for stack)

```typescript
/**
 * Architecture Boundary Tests
 * Generated by arch-guard skill
 *
 * These tests enforce architectural constraints mechanically.
 * If a test fails, it means an import violates the intended architecture.
 */
import { describe, it, expect } from 'vitest';
import fs from 'fs';
import path from 'path';

// Helper: extract imports from a file
function extractImports(filePath: string): string[] {
  const content = fs.readFileSync(filePath, 'utf-8');
  const importRegex = /(?:import|require)\s*\(?['"]([^'"]+)['"]\)?/g;
  const imports: string[] = [];
  let match;
  while ((match = importRegex.exec(content)) !== null) {
    imports.push(match[1]);
  }
  return imports;
}

// Helper: resolve relative import to absolute path
function resolveImport(fromFile: string, importPath: string): string {
  if (importPath.startsWith('.')) {
    return path.resolve(path.dirname(fromFile), importPath);
  }
  return importPath; // external package
}

// Helper: glob files matching pattern
function globFiles(pattern: string, baseDir: string): string[] {
  // Use fast-glob or manual recursive scan
  // Implementation depends on available dependencies
}

describe('Architecture Boundaries', () => {
  // GENERATED RULES GO HERE
  // Each rule becomes a test case:

  it('services cannot import UI components', () => {
    const serviceFiles = globFiles('src/services/**/*.ts', process.cwd());
    const violations: string[] = [];

    for (const file of serviceFiles) {
      const imports = extractImports(file);
      for (const imp of imports) {
        const resolved = resolveImport(file, imp);
        if (resolved.includes('/components/') || resolved.includes('/pages/')) {
          violations.push(`${file} imports ${imp}`);
        }
      }
    }

    expect(violations).toEqual([]);
  });
});
```

### Step 4: VERIFY — Run and Fix

1. Run the generated tests: `npx vitest run tests/arch-guard.test.ts`
2. If violations found:
   - Report each violation with file:line
   - Suggest fix (move shared code to appropriate layer)
   - Do NOT auto-fix — violations need human review

## Output Files

| File | Purpose |
|------|---------|
| `tests/arch-guard.test.ts` | Executable boundary tests |
| `.vibe/arch-rules.json` | Machine-readable rules (for CI) |

## Customization

Users can add custom rules to `.vibe/arch-rules.json`:

```json
{
  "rules": [
    {
      "name": "no-direct-db-in-handlers",
      "from": "src/handlers/**",
      "cannotImport": ["src/db/**"],
      "reason": "Handlers must use services, not direct DB access"
    }
  ]
}
```

The test generator reads this file and adds custom rules to the test suite.

## Integration

- `vibe init` → auto-detect and generate initial arch-guard tests
- `vibe update` → refresh rules if directory structure changed
- Pre-commit hook → run arch-guard tests before commit
- `vibe.review` (skill) → architecture-reviewer checks against arch-rules.json
