diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..0c0393a --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,320 @@ +--- +title: Architecture +description: Internal design of core/lint -- types, data flow, and extension points +--- + +# Architecture + +This document explains how `core/lint` works internally. It covers the core library (`pkg/lint`), the PHP quality pipeline (`pkg/php`), and the QA command layer (`cmd/qa`). + +## Overview + +The system is organised into three layers: + +``` +cmd/core-lint CLI entry point (lint check, lint catalog) +cmd/qa QA workflow commands (watch, review, health, issues, PHP tools) + | +pkg/lint Core library: rules, catalog, matcher, scanner, reporting +pkg/php PHP tool wrappers: format, analyse, audit, security, test +pkg/detect Project type detection + | +catalog/*.yaml Embedded rule definitions +``` + +The root `lint.go` file ties the catalog layer to the library: + +```go +//go:embed catalog/*.yaml +var catalogFS embed.FS + +func LoadEmbeddedCatalog() (*lintpkg.Catalog, error) { + return lintpkg.LoadFS(catalogFS, "catalog") +} +``` + +This means all YAML rules are baked into the binary at compile time. There are no runtime file lookups. + +## Core Types (pkg/lint) + +### Rule + +A `Rule` represents a single lint check loaded from YAML. Key fields: + +```go +type Rule struct { + ID string `yaml:"id"` + Title string `yaml:"title"` + Severity string `yaml:"severity"` // info, low, medium, high, critical + Languages []string `yaml:"languages"` // e.g. ["go"], ["go", "php"] + Tags []string `yaml:"tags"` // e.g. ["security", "injection"] + Pattern string `yaml:"pattern"` // Regex pattern to match + ExcludePattern string `yaml:"exclude_pattern"` // Regex to suppress false positives + Fix string `yaml:"fix"` // Human-readable remediation + Detection string `yaml:"detection"` // "regex" (extensible to other types) + AutoFixable bool `yaml:"auto_fixable"` + ExampleBad string `yaml:"example_bad"` + ExampleGood string `yaml:"example_good"` + FoundIn []string `yaml:"found_in"` // Repos where pattern was observed + FirstSeen string `yaml:"first_seen"` +} +``` + +Each rule validates itself via `Validate()`, which checks required fields and compiles regex patterns. Severity is constrained to five levels: `info`, `low`, `medium`, `high`, `critical`. + +### Catalog + +A `Catalog` is a flat collection of rules with query methods: + +- `ForLanguage(lang)` -- returns rules targeting a specific language +- `AtSeverity(threshold)` -- returns rules at or above a severity level +- `ByID(id)` -- looks up a single rule + +Loading is done via `LoadDir(dir)` for filesystem paths or `LoadFS(fsys, dir)` for embedded filesystems. Both read all `.yaml` files in the directory and parse them into `[]Rule`. + +### Matcher + +The `Matcher` is the regex execution engine. It pre-compiles all regex-detection rules into `compiledRule` structs: + +```go +type compiledRule struct { + rule Rule + pattern *regexp.Regexp + exclude *regexp.Regexp +} +``` + +`NewMatcher(rules)` compiles patterns once. `Match(filename, content)` then scans line by line: + +1. For each compiled rule, check if the filename itself matches the exclude pattern (e.g., skip `_test.go` files). +2. For each line, test against the rule's pattern. +3. If the line matches, check the exclude pattern to suppress false positives. +4. Emit a `Finding` with file, line number, matched text, and remediation advice. + +Non-regex detection types are silently skipped, allowing the catalog schema to support future detection mechanisms (AST, semantic) without breaking the matcher. + +### Scanner + +The `Scanner` orchestrates directory walking and language-aware matching: + +1. Walk the directory tree, skipping excluded directories (`vendor`, `node_modules`, `.git`, `testdata`, `.core`). +2. For each file, detect its language from the file extension using `DetectLanguage()`. +3. Filter the rule set to only rules targeting that language. +4. Build a language-scoped `Matcher` and run it against the file content. + +Supported language extensions: + +| Extension | Language | +|-----------|----------| +| `.go` | go | +| `.php` | php | +| `.ts`, `.tsx` | ts | +| `.js`, `.jsx` | js | +| `.cpp`, `.cc`, `.c`, `.h` | cpp | +| `.py` | py | + +### Finding + +A `Finding` is the output of a match: + +```go +type Finding struct { + RuleID string `json:"rule_id"` + Title string `json:"title"` + Severity string `json:"severity"` + File string `json:"file"` + Line int `json:"line"` + Match string `json:"match"` + Fix string `json:"fix"` + Repo string `json:"repo,omitempty"` +} +``` + +### Report + +The `report.go` file provides three output formats: + +- `WriteText(w, findings)` -- human-readable: `file:line [severity] title (rule-id)` +- `WriteJSON(w, findings)` -- pretty-printed JSON array +- `WriteJSONL(w, findings)` -- newline-delimited JSON (one object per line) + +`Summarise(findings)` aggregates counts by severity. + +## Data Flow + +A typical scan follows this path: + +``` +YAML files ──> LoadFS() ──> Catalog{Rules} + | + ForLanguage() / AtSeverity() + | + []Rule (filtered) + | + NewScanner(rules) + | + ScanDir(root) / ScanFile(path) + | + ┌───────────────┼───────────────┐ + │ Walk tree │ Detect lang │ + │ Skip dirs │ Filter rules │ + │ │ NewMatcher() │ + │ │ Match() │ + └───────────────┴───────────────┘ + | + []Finding + | + WriteText() / WriteJSON() / WriteJSONL() +``` + +## Cyclomatic Complexity Analysis (pkg/lint/complexity.go) + +The module includes a native Go AST-based cyclomatic complexity analyser. It uses `go/parser` and `go/ast` -- no external tools required. + +```go +results, err := lint.AnalyseComplexity(lint.ComplexityConfig{ + Threshold: 15, + Path: "./pkg/...", +}) +``` + +Complexity is calculated by starting at 1 and incrementing for each branching construct: +- `if`, `for`, `range`, `case` (non-default), `comm` (non-default) +- `&&`, `||` binary expressions +- `type switch`, `select` + +There is also `AnalyseComplexitySource(src, filename, threshold)` for testing without file I/O. + +## Coverage Tracking (pkg/lint/coverage.go) + +The coverage subsystem supports: + +- **Parsing** Go coverage output (`ParseCoverProfile` for `-coverprofile` format, `ParseCoverOutput` for `-cover` output) +- **Snapshotting** via `CoverageSnapshot` (timestamp, per-package percentages, metadata) +- **Persistence** via `CoverageStore` (JSON file-backed append-only store) +- **Regression detection** via `CompareCoverage(previous, current)` which returns a `CoverageComparison` with regressions, improvements, new packages, and removed packages + +## Vulnerability Checking (pkg/lint/vulncheck.go) + +`VulnCheck` wraps `govulncheck -json` and parses its newline-delimited JSON output into structured `VulnFinding` objects. The parser handles three message types from govulncheck's wire format: + +- `config` -- extracts the module path +- `osv` -- stores vulnerability metadata (ID, aliases, summary, affected ranges) +- `finding` -- maps OSV IDs to call traces and affected packages + +## Toolkit (pkg/lint/tools.go) + +The `Toolkit` struct wraps common developer commands into structured Go APIs. It executes subprocesses and parses their output: + +| Method | Wraps | Returns | +|--------|-------|---------| +| `FindTODOs(dir)` | `git grep` | `[]TODO` | +| `Lint(pkg)` | `go vet` | `[]ToolFinding` | +| `Coverage(pkg)` | `go test -cover` | `[]CoverageReport` | +| `RaceDetect(pkg)` | `go test -race` | `[]RaceCondition` | +| `AuditDeps()` | `govulncheck` (text) | `[]Vulnerability` | +| `ScanSecrets(dir)` | `gitleaks` | `[]SecretLeak` | +| `GocycloComplexity(threshold)` | `gocyclo` | `[]ComplexFunc` | +| `DepGraph(pkg)` | `go mod graph` | `*Graph` | +| `GitLog(n)` | `git log` | `[]Commit` | +| `DiffStat()` | `git diff --stat` | `DiffSummary` | +| `UncommittedFiles()` | `git status` | `[]string` | +| `Build(targets...)` | `go build` | `[]BuildResult` | +| `TestCount(pkg)` | `go test -list` | `int` | +| `CheckPerms(dir)` | `filepath.Walk` | `[]PermIssue` | +| `ModTidy()` | `go mod tidy` | `error` | + +All methods use the `Run(name, args...)` helper which captures stdout, stderr, and exit code. + +## PHP Quality Pipeline (pkg/php) + +The `pkg/php` package provides structured wrappers around PHP ecosystem tools. Each tool has: + +1. **Detection** -- checks for config files and vendor binaries (e.g., `DetectAnalyser`, `DetectPsalm`, `DetectRector`) +2. **Options struct** -- configures the tool run +3. **Execution function** -- builds the command, runs it, and returns structured results + +### Supported Tools + +| Function | Tool | Purpose | +|----------|------|---------| +| `Format()` | Laravel Pint | Code style formatting | +| `Analyse()` | PHPStan / Larastan | Static analysis | +| `RunPsalm()` | Psalm | Type-level static analysis | +| `RunAudit()` | Composer audit + npm audit | Dependency vulnerability scanning | +| `RunSecurityChecks()` | Built-in checks | .env exposure, debug mode, filesystem security | +| `RunRector()` | Rector | Automated code refactoring | +| `RunInfection()` | Infection | Mutation testing | +| `RunTests()` | Pest / PHPUnit | Test execution | + +### QA Pipeline + +The pipeline system (`pipeline.go` + `runner.go`) organises checks into three stages: + +- **Quick** -- audit, fmt, stan (fast, run on every push) +- **Standard** -- psalm (if available), test +- **Full** -- rector, infection (slow, run in full QA) + +The `QARunner` builds `process.RunSpec` objects with dependency ordering (e.g., `stan` runs after `fmt`, `test` runs after `stan`). This allows future parallelisation while respecting ordering constraints. + +### Project Detection (pkg/detect) + +The `detect` package identifies project types by checking for marker files: + +- `go.mod` present => Go project +- `composer.json` present => PHP project + +`DetectAll(dir)` returns all detected types, enabling polyglot project support. + +## QA Command Layer (cmd/qa) + +The `cmd/qa` package provides workflow-level commands that integrate with GitHub via the `gh` CLI: + +- **watch** -- polls GitHub Actions for a specific commit, shows real-time status, drills into failure details (failed job, step, error line from logs) +- **review** -- fetches open PRs, analyses CI status, review decisions, and merge readiness, suggests next actions +- **health** -- scans all repos in a `repos.yaml` registry, reports aggregate CI health with pass rates +- **issues** -- fetches issues across repos, categorises them (needs response, ready, blocked, triage), prioritises by labels and activity +- **docblock** -- parses Go source with `go/ast`, counts exported symbols with and without doc comments, enforces a coverage threshold + +Commands register themselves via `cli.RegisterCommands` in an `init()` function, making them available when the package is imported. + +## Extension Points + +### Adding New Rules + +Create a new YAML file in `catalog/` following the schema: + +```yaml +- id: go-xxx-001 + title: "Description of the issue" + severity: medium # info, low, medium, high, critical + languages: [go] + tags: [security] + pattern: 'regex-pattern' + exclude_pattern: 'false-positive-filter' + fix: "How to fix the issue" + detection: regex + auto_fixable: false + example_bad: 'problematic code' + example_good: 'corrected code' +``` + +The file will be embedded automatically on the next build. + +### Adding New Detection Types + +The `Detection` field on `Rule` currently supports `"regex"`. The `Matcher` skips non-regex rules, so adding a new detection type (e.g., `"ast"` for Go AST patterns) requires: + +1. Adding the new type to the `Validate()` method +2. Creating a new matcher implementation +3. Integrating it into `Scanner.ScanDir()` + +### Loading External Catalogs + +Use `LoadDir(path)` to load rules from a directory on disk rather than the embedded catalog: + +```go +cat, err := lintpkg.LoadDir("/path/to/custom/rules") +``` + +This allows organisations to maintain private rule sets alongside the built-in catalog. diff --git a/docs/development.md b/docs/development.md new file mode 100644 index 0000000..5247a3b --- /dev/null +++ b/docs/development.md @@ -0,0 +1,271 @@ +--- +title: Development Guide +description: How to build, test, and contribute to core/lint +--- + +# Development Guide + +## Prerequisites + +- Go 1.26 or later +- `core` CLI (for build and QA commands) +- `gh` CLI (only needed for the `qa watch`, `qa review`, `qa health`, and `qa issues` commands) + +## Building + +The project uses the `core` build system. Configuration lives in `.core/build.yaml`. + +```bash +# Build the binary (outputs to ./bin/core-lint) +core build + +# Build targets: linux/amd64, linux/arm64, darwin/arm64, windows/amd64 +# CGO is disabled; the binary is fully static. +``` + +To build manually with `go build`: + +```bash +go build -trimpath -ldflags="-s -w" -o bin/core-lint ./cmd/core-lint +``` + +## Running Tests + +```bash +# Run all tests +core go test + +# Run a single test by name +core go test --run TestRule_Validate_Good + +# Generate coverage report +core go cov +core go cov --open # Opens HTML report in browser +``` + +The test suite covers all packages: + +| Package | Test count | Focus | +|---------|-----------|-------| +| `pkg/lint` | ~89 | Rule validation, catalog loading, matcher, scanner, report, complexity, coverage, vulncheck, toolkit | +| `pkg/detect` | 6 | Project type detection | +| `pkg/php` | ~125 | All PHP tool wrappers (format, analyse, audit, security, refactor, mutation, test, pipeline, runner) | + +Tests follow the `_Good`, `_Bad`, `_Ugly` suffix convention: +- `_Good` -- happy path +- `_Bad` -- expected error conditions +- `_Ugly` -- edge cases and panics + +### Test Examples + +Testing rules against source content: + +```go +func TestMatcher_Match_Good(t *testing.T) { + rules := []Rule{ + { + ID: "test-001", + Title: "TODO found", + Severity: "low", + Pattern: `TODO`, + Detection: "regex", + }, + } + m, err := NewMatcher(rules) + require.NoError(t, err) + + findings := m.Match("example.go", []byte("// TODO: fix this")) + assert.Len(t, findings, 1) + assert.Equal(t, "test-001", findings[0].RuleID) + assert.Equal(t, 1, findings[0].Line) +} +``` + +Testing complexity analysis without file I/O: + +```go +func TestAnalyseComplexitySource_Good(t *testing.T) { + src := `package example +func simple() { if true {} } +func complex() { + if a {} else if b {} else if c {} + for i := range items { + switch { + case x: if y {} + case z: + } + } +}` + results, err := AnalyseComplexitySource(src, "test.go", 3) + require.NoError(t, err) + assert.NotEmpty(t, results) +} +``` + +## Quality Assurance + +```bash +# Full QA pipeline: format, vet, lint, test +core go qa + +# Extended QA: includes race detection, vulnerability scan, security checks +core go qa full + +# Individual checks +core go fmt # Format code +core go vet # Run go vet +core go lint # Run linter +``` + +## Project Structure + +``` +lint/ +├── .core/ +│ └── build.yaml # Build configuration +├── bin/ # Build output (gitignored) +├── catalog/ +│ ├── go-correctness.yaml # Correctness rules (7 rules) +│ ├── go-modernise.yaml # Modernisation rules (5 rules) +│ └── go-security.yaml # Security rules (6 rules) +├── cmd/ +│ ├── core-lint/ +│ │ └── main.go # CLI binary entry point +│ └── qa/ +│ ├── cmd_qa.go # QA command group registration +│ ├── cmd_watch.go # GitHub Actions monitoring +│ ├── cmd_review.go # PR review status +│ ├── cmd_health.go # Aggregate CI health +│ ├── cmd_issues.go # Issue triage +│ ├── cmd_docblock.go # Docblock coverage +│ └── cmd_php.go # PHP QA subcommands +├── pkg/ +│ ├── detect/ +│ │ ├── detect.go # Project type detection +│ │ └── detect_test.go +│ ├── lint/ +│ │ ├── catalog.go # Catalog loading and querying +│ │ ├── complexity.go # Cyclomatic complexity (native AST) +│ │ ├── coverage.go # Coverage tracking and comparison +│ │ ├── matcher.go # Regex matching engine +│ │ ├── report.go # Output formatters (text, JSON, JSONL) +│ │ ├── rule.go # Rule type and validation +│ │ ├── scanner.go # Directory walking and file scanning +│ │ ├── tools.go # Toolkit (subprocess wrappers) +│ │ ├── vulncheck.go # govulncheck JSON parser +│ │ ├── testdata/ +│ │ │ └── catalog/ +│ │ │ └── test-rules.yaml +│ │ └── *_test.go +│ └── php/ +│ ├── analyse.go # PHPStan/Larastan/Psalm wrappers +│ ├── audit.go # Composer audit + npm audit +│ ├── format.go # Laravel Pint wrapper +│ ├── mutation.go # Infection wrapper +│ ├── pipeline.go # QA stage definitions +│ ├── refactor.go # Rector wrapper +│ ├── runner.go # Process spec builder +│ ├── security.go # Security checks (.env, filesystem) +│ ├── test.go # Pest/PHPUnit wrapper +│ └── *_test.go +├── lint.go # Root package: embedded catalog loader +├── go.mod +├── go.sum +├── CLAUDE.md +└── README.md +``` + +## Writing New Rules + +### Rule Schema + +Each YAML file in `catalog/` contains an array of rule objects. Required fields: + +| Field | Type | Description | +|-------|------|-------------| +| `id` | string | Unique identifier (convention: `{lang}-{category}-{number}`, e.g., `go-sec-001`) | +| `title` | string | Short human-readable description | +| `severity` | string | One of: `info`, `low`, `medium`, `high`, `critical` | +| `languages` | []string | Target languages (e.g., `[go]`, `[go, php]`) | +| `pattern` | string | Detection pattern (regex for `detection: regex`) | +| `fix` | string | Remediation guidance | +| `detection` | string | Detection type (currently only `regex`) | + +Optional fields: + +| Field | Type | Description | +|-------|------|-------------| +| `tags` | []string | Categorisation tags (e.g., `[security, injection]`) | +| `exclude_pattern` | string | Regex to suppress false positives | +| `found_in` | []string | Repos where the pattern was originally observed | +| `example_bad` | string | Code example that triggers the rule | +| `example_good` | string | Corrected code example | +| `first_seen` | string | Date the pattern was first catalogued | +| `auto_fixable` | bool | Whether automated fixing is feasible | + +### Naming Convention + +Rule IDs follow the pattern `{lang}-{category}-{number}`: + +- `go-sec-*` -- Security rules +- `go-cor-*` -- Correctness rules +- `go-mod-*` -- Modernisation rules + +### Testing a New Rule + +Create a test that verifies the pattern matches expected code and does not match exclusions: + +```go +func TestNewRule_Matches(t *testing.T) { + rules := []Rule{ + { + ID: "go-xxx-001", + Title: "My new rule", + Severity: "medium", + Languages: []string{"go"}, + Pattern: `my-pattern`, + ExcludePattern: `safe-variant`, + Detection: "regex", + }, + } + + m, err := NewMatcher(rules) + require.NoError(t, err) + + // Should match + findings := m.Match("example.go", []byte("code with my-pattern here")) + assert.Len(t, findings, 1) + + // Should not match (exclusion) + findings = m.Match("example.go", []byte("code with safe-variant here")) + assert.Empty(t, findings) +} +``` + +## Adding PHP Tool Support + +To add support for a new PHP tool: + +1. Create a new file in `pkg/php/` (e.g., `newtool.go`). +2. Add a detection function that checks for config files or vendor binaries. +3. Add an options struct and an execution function. +4. Add a command in `cmd/qa/cmd_php.go` that wires the tool to the CLI. +5. Add the tool to the pipeline stages in `pipeline.go` if appropriate. +6. Write tests in a corresponding `*_test.go` file. + +Follow the existing pattern -- each tool module exports: +- `Detect*()` -- returns whether the tool is available +- `Run*()` or the tool function -- executes the tool with options +- A `*Options` struct -- configures behaviour + +## Coding Standards + +- **UK English** throughout: `colour`, `organisation`, `centre`, `modernise`, `analyse`, `serialise` +- **Strict typing**: All function parameters and return values must have explicit types +- **Testing**: Use `testify` assertions (`assert`, `require`) +- **Error wrapping**: Use `fmt.Errorf("context: %w", err)` for error chains +- **Formatting**: Standard Go formatting via `gofmt` / `core go fmt` + +## Licence + +This project is licenced under the EUPL-1.2. diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..e5cc322 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,141 @@ +--- +title: core/lint +description: Pattern catalog, regex-based code checker, and quality assurance toolkit for Go and PHP projects +--- + +# core/lint + +`forge.lthn.ai/core/lint` is a standalone pattern catalog and code quality toolkit. It ships a YAML-based rule catalog for detecting security issues, correctness bugs, and modernisation opportunities in Go source code. It also provides a full PHP quality assurance pipeline and a suite of developer tooling wrappers. + +The library is designed to be embedded into other tools. The YAML rule files are compiled into the binary at build time via `go:embed`, so there are no runtime file dependencies. + +## Module Path + +``` +forge.lthn.ai/core/lint +``` + +Requires Go 1.26+. + +## Quick Start + +### As a Library + +```go +import ( + lint "forge.lthn.ai/core/lint" + lintpkg "forge.lthn.ai/core/lint/pkg/lint" +) + +// Load the embedded rule catalog. +cat, err := lint.LoadEmbeddedCatalog() +if err != nil { + log.Fatal(err) +} + +// Filter rules for Go, severity medium and above. +rules := cat.ForLanguage("go") +filtered := (&lintpkg.Catalog{Rules: rules}).AtSeverity("medium") + +// Create a scanner and scan a directory. +scanner, err := lintpkg.NewScanner(filtered) +if err != nil { + log.Fatal(err) +} + +findings, err := scanner.ScanDir("./src") +if err != nil { + log.Fatal(err) +} + +// Output results. +lintpkg.WriteText(os.Stdout, findings) +``` + +### As a CLI + +```bash +# Build the binary +core build # produces ./bin/core-lint + +# Scan the current directory with all rules +core-lint lint check + +# Scan with filters +core-lint lint check --lang go --severity high ./pkg/... + +# Output as JSON +core-lint lint check --format json . + +# Browse the catalog +core-lint lint catalog list +core-lint lint catalog list --lang go +core-lint lint catalog show go-sec-001 +``` + +### QA Commands + +The `qa` command group provides workflow-level quality assurance: + +```bash +# Go-focused +core qa watch # Monitor GitHub Actions after a push +core qa review # PR review status with actionable next steps +core qa health # Aggregate CI health across all repos +core qa issues # Intelligent issue triage +core qa docblock # Check Go docblock coverage + +# PHP-focused +core qa fmt # Format PHP code with Laravel Pint +core qa stan # Run PHPStan/Larastan static analysis +core qa psalm # Run Psalm static analysis +core qa audit # Audit composer and npm dependencies +core qa security # Security checks (.env, filesystem, deps) +core qa rector # Automated code refactoring +core qa infection # Mutation testing +core qa test # Run Pest or PHPUnit tests +``` + +## Package Layout + +| Package | Path | Description | +|---------|------|-------------| +| `lint` (root) | `lint.go` | Embeds YAML catalogs and exposes `LoadEmbeddedCatalog()` | +| `pkg/lint` | `pkg/lint/` | Core library: Rule, Catalog, Matcher, Scanner, Report, Complexity, Coverage, VulnCheck, Toolkit | +| `pkg/detect` | `pkg/detect/` | Project type detection (Go, PHP) by filesystem markers | +| `pkg/php` | `pkg/php/` | PHP quality tools: format, analyse, audit, security, refactor, mutation, test, pipeline, runner | +| `cmd/core-lint` | `cmd/core-lint/` | CLI binary (`core-lint lint check`, `core-lint lint catalog`) | +| `cmd/qa` | `cmd/qa/` | QA workflow commands (watch, review, health, issues, docblock, PHP tools) | +| `catalog/` | `catalog/` | YAML rule definitions (embedded at compile time) | + +## Rule Catalogs + +Three built-in YAML catalogs ship with the module: + +| File | Rules | Focus | +|------|-------|-------| +| `go-security.yaml` | 6 | SQL injection, path traversal, XSS, timing attacks, log injection, secret leaks | +| `go-correctness.yaml` | 7 | Unsynchronised goroutines, silent error swallowing, panics in library code, file deletion | +| `go-modernise.yaml` | 5 | Replace legacy patterns with modern stdlib (`slices.Clone`, `slices.Sort`, `maps.Keys`, `errgroup`) | + +Total: **18 rules** across 3 severity tiers (info, medium, high, critical). All rules target Go. The catalog is extensible -- add more YAML files to `catalog/` and they will be embedded automatically. + +## Dependencies + +Direct dependencies: + +| Module | Purpose | +|--------|---------| +| `forge.lthn.ai/core/cli` | CLI framework (`cli.Main()`, command registration, TUI styles) | +| `forge.lthn.ai/core/go-i18n` | Internationalisation for CLI strings | +| `forge.lthn.ai/core/go-io` | Filesystem abstraction for registry loading | +| `forge.lthn.ai/core/go-log` | Structured logging and error wrapping | +| `forge.lthn.ai/core/go-scm` | Repository registry (`repos.yaml`) for multi-repo commands | +| `github.com/stretchr/testify` | Test assertions | +| `gopkg.in/yaml.v3` | YAML parsing for rule catalogs | + +The `pkg/lint` sub-package has minimal dependencies (only `gopkg.in/yaml.v3` and standard library). The heavier CLI and SCM dependencies live in `cmd/`. + +## Licence + +EUPL-1.2