Skip to content
AI CraftsmanSUPERPOWERS

Rules Engine

3-level rule inheritance system: Global → Project → Directory. Built-in rules, custom rules, and severity levels.

Last updated: Edit on GitHub

Overview

The Rules Engine (hooks/lib/rules-engine.sh) is the core of Craftsman’s validation system. It implements a 3-level configuration hierarchy with file-based key-value storage (Bash 3.2 compatible, no associative arrays).

Inheritance order (highest wins):

Directory (.craft-rules.yml)
    ↑ overrides
Project (.craft-config.yml)
    ↑ overrides
Global (~/.claude/.craft-config.yml)

Severity Levels

Value Behavior
true or "block" Blocks the write: Claude must fix
"warn" Allows write, logs violation to metrics
"ignore" or false Rule disabled

Built-in Rules

PHP Rules

Rule ID Pattern Default Description
PHP001 ^(abstract|readonly|)?\s*class\s block Class must be final
PHP002 public\s+function\s+__construct block Constructor must be private
PHP003 public\s+function\s+set[A-Z] block No setters (use behavioral methods)
PHP004 ^<\?php without declare(strict_types=1) block strict_types=1 required
PHP005 new\s+\\DateTime\(\) block Use Clock abstraction
PHP006 catch\s*\([^)]+\)\s*\{\s*\} block No empty catch blocks

TypeScript Rules

Rule ID Pattern Default Description
TS001 :\s*any\b block No any type
TS002 (?<!readonly\s)(?:public|private|protected)\s+\w+: warn Properties should be readonly
TS003 export default block No default exports

Architecture Rules

Rule ID Check Default Description
LAYER001 Domain imports Infrastructure block Domain must not import Infrastructure
LAYER002 Domain imports Application block Domain must not import Application
LAYER003 Application imports Infrastructure warn Application should not import Infrastructure directly

Short-Form Rules (.craft-config.yml)

Short-form rules reference built-in rule IDs:

rules:
  php:
    final_classes: true        # PHP001 → block
    private_constructors: true # PHP002 → block
    no_setters: warn           # PHP003 → warn
    strict_types: true         # PHP004 → block
    no_datetime_direct: true   # PHP005 → block
    no_empty_catch: ignore     # PHP006 → disabled

Long-Form Custom Rules (.craft-config.yml)

Define custom regex-based rules for your project:

custom_rules:
  - id: "CUSTOM001"
    name: "no-doctrine-find-by-id"
    description: "Use Repository method instead of find()"
    severity: "warn"
    pattern: '->find\(\$id\)'
    languages: ["php"]
    message: "Use ->findById() repository method for better encapsulation"

  - id: "CUSTOM002"
    name: "no-hardcoded-urls"
    description: "No hardcoded production URLs in code"
    severity: "block"
    pattern: 'https://api\.production\.com'
    languages: ["php", "typescript"]
    message: "Use environment variables for URLs"

Directory-Level Overrides (.craft-rules.yml)

Place a .craft-rules.yml in any directory:

# src/Legacy/.craft-rules.yml
# Relax rules for legacy code
rules:
  php:
    final_classes: ignore     # Legacy classes are not final
    private_constructors: ignore
    strict_types: warn        # Warn but don't block legacy files
# src/Tests/.craft-rules.yml
# Different rules for test files
rules:
  php:
    final_classes: ignore     # Test classes don't need final
    no_setters: ignore        # Test builders use setters
  custom_rules:
    no-doctrine-find-by-id:
      severity: ignore        # Tests can use find() directly

Directory walking

The rules engine walks up the directory tree from the current file, collecting .craft-rules.yml files. The closest file to the edited file wins. Results are cached per directory for performance.

Rules Engine Public API

The rules engine exposes these shell functions for use in hooks:

# Initialize the engine (load all configs)
rules_init

# Get severity for a named rule (returns: block|warn|ignore)
rules_severity "php.final_classes"
# → "block"

# Get severity for a rule, scoped to a specific file
rules_severity_for_file "php.final_classes" "/path/to/file.php"
# → "ignore"  (if file is in a directory with .craft-rules.yml that disables it)

# List all custom rules
rules_custom_list
# → CUSTOM001 CUSTOM002

# Get the regex pattern for a custom rule
rules_pattern "CUSTOM001"
# → '->find\(\$id\)'

# Get the message for a custom rule
rules_message "CUSTOM001"
# → "Use ->findById() repository method..."

Validation Flow

On every Write or Edit tool call:

File write triggered

Level 1: Regex validation (<50ms)
    → rules_severity_for_file() for each applicable rule
    → Regex match against file content
    → BLOCK if severity=block and match found

Level 2: Static analysis (<2s)
    → PHPStan (PHP files)
    → ESLint (TypeScript files)
    → BLOCK on errors

Level 3: Architecture check (<2s)
    → deptrac (layer violations)
    → dependency-cruiser (TypeScript)
    → BLOCK on violations

Metrics recorded (SQLite)
    → violations count
    → rule IDs triggered
    → file path (anonymized)

Performance

Level 1 (regex) runs on every write in <50ms. Levels 2 and 3 are skipped if the file hasn’t changed significantly (content hash comparison). Total overhead: <3 seconds for a typical write.

Disable Rules Inline

For specific lines or blocks, use craftsman-ignore comments:

// craftsman-ignore-next-line PHP001
class LegacyAdapter implements ThirdPartyInterface
{
    // ...
}
// craftsman-ignore-next-line TS003
export default function handler() {} // Next.js requires default export