playbook/antigravity-awesome-skills/skills/ecl-harness-engineer/references/linter-templates.md

9.8 KiB

Linter Templates

Go linter patterns for mechanical architecture enforcement.

Dependency Direction Linter

Validates that package imports respect the layer hierarchy.

// scripts/lint-deps.go
//
// Validates that package dependencies follow the layer hierarchy.
// Each layer can only import from lower layers.
//
// Usage: go run scripts/lint-deps.go
package main

import (
	"fmt"
	"go/parser"
	"go/token"
	"os"
	"path/filepath"
	"strings"
)

// CUSTOMIZE: Set your module path
const modulePath = "your-module-path"

// CUSTOMIZE: Define your layer hierarchy (lower index = lower layer)
var layers = [][]string{
	// Layer 0: No internal dependencies
	{"core/types"},
	// Layer 1: Depends on Layer 0
	{"core/utils"},
	// Layer 2: Depends on Layers 0-1
	{"core/config", "core/logging"},
	// Layer 3: Depends on Layers 0-2
	{"core/business"},
	// Layer 4: Depends on core/ but NOT each other
	{"ui", "sdk", "integrations"},
	// Layer 5: Can depend on everything
	{"cmd"},
}

// CUSTOMIZE: Packages that must not import each other
var mutuallyExclusive = [][]string{
	{"ui", "sdk", "integrations"},
}

type Violation struct {
	File    string
	Package string
	Imports string
	Message string
}

func main() {
	violations := checkDependencies()

	if len(violations) == 0 {
		fmt.Println("✓ All package dependencies follow the layer hierarchy")
		os.Exit(0)
	}

	fmt.Printf("✗ Found %d dependency violations:\n\n", len(violations))
	for _, v := range violations {
		fmt.Printf("%s:\n  Package: %s\n  Imports: %s\n  Error: %s\n\n",
			v.File, v.Package, v.Imports, v.Message)
	}
	os.Exit(1)
}

func checkDependencies() []Violation {
	var violations []Violation
	layerMap := buildLayerMap()

	filepath.Walk(".", func(path string, info os.FileInfo, err error) error {
		if err != nil || info.IsDir() {
			if info != nil && info.IsDir() {
				name := info.Name()
				if strings.HasPrefix(name, ".") || name == "vendor" || name == "dist" {
					return filepath.SkipDir
				}
			}
			return nil
		}

		if !strings.HasSuffix(path, ".go") || strings.HasSuffix(path, "_test.go") {
			return nil
		}

		violations = append(violations, checkFile(path, layerMap)...)
		return nil
	})

	return violations
}

func buildLayerMap() map[string]int {
	m := make(map[string]int)
	for idx, pkgs := range layers {
		for _, pkg := range pkgs {
			m[pkg] = idx
		}
	}
	return m
}

func checkFile(path string, layerMap map[string]int) []Violation {
	var violations []Violation

	fset := token.NewFileSet()
	node, err := parser.ParseFile(fset, path, nil, parser.ImportsOnly)
	if err != nil {
		return violations
	}

	dir := filepath.ToSlash(filepath.Dir(path))
	dir = strings.TrimPrefix(dir, "./")
	pkgLayer := findLayer(dir, layerMap)
	if pkgLayer < 0 {
		return violations
	}

	for _, imp := range node.Imports {
		importPath := strings.Trim(imp.Path.Value, `"`)
		if !strings.HasPrefix(importPath, modulePath) {
			continue
		}

		relImport := strings.TrimPrefix(importPath, modulePath+"/")
		importLayer := findLayer(relImport, layerMap)
		if importLayer < 0 {
			continue
		}

		if importLayer >= pkgLayer {
			// Check if same base package (allowed)
			if getBase(dir) == getBase(relImport) {
				continue
			}
			violations = append(violations, Violation{
				File: path, Package: dir, Imports: relImport,
				Message: fmt.Sprintf("Layer %d cannot import layer %d", pkgLayer, importLayer),
			})
		}
	}

	return violations
}

func findLayer(pkg string, layerMap map[string]int) int {
	if layer, ok := layerMap[pkg]; ok {
		return layer
	}
	for key, layer := range layerMap {
		if strings.HasPrefix(pkg, key+"/") {
			return layer
		}
	}
	return -1
}

func getBase(pkg string) string {
	parts := strings.SplitN(pkg, "/", 3)
	if len(parts) >= 2 {
		return parts[0] + "/" + parts[1]
	}
	return parts[0]
}

Quality Linter

Validates golden principles like structured logging, file sizes, and naming.

// scripts/lint-quality.go
//
// Validates golden principles:
// - No raw log.Printf (use structured logging)
// - File size limits (max 1000 lines)
// - No hardcoded brand strings
//
// Usage: go run scripts/lint-quality.go
package main

import (
	"bufio"
	"fmt"
	"os"
	"path/filepath"
	"regexp"
	"strings"
)

const maxFileLines = 1000

// CUSTOMIZE: Patterns to flag
var rawLogPatterns = []*regexp.Regexp{
	regexp.MustCompile(`\blog\.Printf\b`),
	regexp.MustCompile(`\blog\.Println\b`),
	regexp.MustCompile(`\blog\.Fatalf\b`),
}

// CUSTOMIZE: Directories to skip
var skipDirs = map[string]bool{
	".git": true, "dist": true, "vendor": true,
}

type Violation struct {
	File    string
	Line    int
	Rule    string
	Message string
}

func main() {
	var violations []Violation

	walkGoFiles(func(path string, content []byte) {
		if strings.HasSuffix(path, "_test.go") {
			return
		}

		// Check structured logging
		lines := strings.Split(string(content), "\n")
		for lineNum, line := range lines {
			trimmed := strings.TrimSpace(line)
			if strings.HasPrefix(trimmed, "//") {
				continue
			}
			for _, pattern := range rawLogPatterns {
				if pattern.MatchString(line) {
					violations = append(violations, Violation{
						File: path, Line: lineNum + 1,
						Rule:    "structured-logging",
						Message: "Use structured logging instead of raw log calls",
					})
				}
			}
		}

		// Check file size
		scanner := bufio.NewScanner(strings.NewReader(string(content)))
		lineCount := 0
		for scanner.Scan() {
			lineCount++
		}
		if lineCount > maxFileLines {
			violations = append(violations, Violation{
				File: path, Rule: "file-size",
				Message: fmt.Sprintf("File has %d lines (max %d)", lineCount, maxFileLines),
			})
		}
	})

	if len(violations) == 0 {
		fmt.Println("✓ All quality checks passed")
		os.Exit(0)
	}

	fmt.Printf("✗ Found %d quality violations:\n\n", len(violations))
	for _, v := range violations {
		fmt.Printf("%s:%d [%s]: %s\n", v.File, v.Line, v.Rule, v.Message)
	}
	os.Exit(1)
}

func walkGoFiles(fn func(path string, content []byte)) {
	filepath.Walk(".", func(path string, info os.FileInfo, err error) error {
		if err != nil || info.IsDir() {
			if info != nil && info.IsDir() && skipDirs[filepath.Base(path)] {
				return filepath.SkipDir
			}
			return nil
		}
		if !strings.HasSuffix(path, ".go") {
			return nil
		}
		content, err := os.ReadFile(path)
		if err != nil {
			return nil
		}
		fn(path, content)
		return nil
	})
}

Template Linter

Validates template files (.tpl) are valid and referenced.

// scripts/lint-prompts.go
//
// Validates template files: parseable, referenced, no orphans.
//
// Usage: go run scripts/lint-prompts.go
package main

import (
	"fmt"
	"os"
	"path/filepath"
	"regexp"
	"strings"
	"text/template"
)

// CUSTOMIZE: Directories containing template files
var templateDirs = []string{"templates", "prompts"}

func main() {
	var violations int

	for _, dir := range templateDirs {
		filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
			if err != nil || info == nil || info.IsDir() || !strings.HasSuffix(path, ".tpl") {
				return nil
			}

			content, err := os.ReadFile(path)
			if err != nil {
				fmt.Printf("%s: cannot read: %v\n", path, err)
				violations++
				return nil
			}

			_, err = template.New(filepath.Base(path)).
				Option("missingkey=zero").
				Parse(string(content))
			if err != nil {
				fmt.Printf("%s: invalid template: %v\n", path, err)
				violations++
			}

			return nil
		})
	}

	// Check for orphan templates
	embedPattern := regexp.MustCompile(`//go:embed\s+(\S+\.tpl)`)
	tplFiles := make(map[string]bool)

	for _, dir := range templateDirs {
		filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
			if err != nil || info == nil {
				return nil
			}
			if !info.IsDir() && strings.HasSuffix(path, ".tpl") {
				tplFiles[filepath.Base(path)] = false
			}
			return nil
		})
	}

	filepath.Walk(".", func(path string, info os.FileInfo, err error) error {
		if err != nil || info.IsDir() || !strings.HasSuffix(path, ".go") {
			return nil
		}
		content, _ := os.ReadFile(path)
		for _, match := range embedPattern.FindAllSubmatch(content, -1) {
			tplFiles[string(match[1])] = true
		}
		return nil
	})

	for name, referenced := range tplFiles {
		if !referenced {
			fmt.Printf("WARNING: %s may be orphaned\n", name)
		}
	}

	if violations == 0 {
		fmt.Printf("✓ All template files are valid\n")
		os.Exit(0)
	}
	os.Exit(1)
}

Makefile Integration

# Architecture checks
.PHONY: lint-arch
lint-arch:
	@echo "Checking architecture constraints..."
	@go run scripts/lint-deps.go
	@go run scripts/lint-prompts.go
	@go run scripts/lint-quality.go
	@echo "✓ Architecture checks passed"

# Combined lint
.PHONY: lint
lint: lint-arch
	@if command -v golangci-lint >/dev/null 2>&1; then \
		golangci-lint run; \
	fi

Adaptation Notes

For TypeScript Projects

Read references/adapters/typescript.md before creating files. Prefer package-manager scripts and Node-native harness linters over Go examples:

  • Create scripts/lint-deps.mjs or an equivalent Node script for layer/import checks.
  • Create scripts/lint-quality.mjs for project-specific quality checks.
  • Add npm/pnpm/yarn/bun scripts such as lint:deps, lint:quality, lint:arch, and lint:harness.
  • Keep CI strict: include existing lint, typecheck, test, and build gates when available.
  • Do not weaken CI because baseline commands are red; report those as pre-existing project debt.

For small projects, ESLint import restrictions may be enough:

// .eslintrc.js - import restrictions
module.exports = {
  rules: {
    'no-restricted-imports': ['error', {
      patterns: [
        { group: ['../ui/*'], message: 'Core cannot import UI' },
        { group: ['../cmd/*'], message: 'Core cannot import CLI' },
      ]
    }]
  }
};

For Python Projects

Use custom pylint checkers or ruff rules:

# scripts/lint_deps.py
import ast, sys, pathlib

LAYER_ORDER = {
    'models': 0,
    'utils': 1,
    'services': 2,
    'api': 3,
    'cli': 4,
}
# ... validate imports against layer order