ratchets 0.2.6

Progressive lint enforcement tool with budgeted violations that can only decrease over time
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
# Ratchets Design Specification

## Overview

Ratchets is a progressive lint enforcement tool that allows codebases to contain existing violations while preventing new ones. Unlike traditional linters that enforce binary pass/fail, Ratchets permits a budgeted number of violations per rule per region. These budgets can only decrease over time (the "ratchet" mechanism), ensuring technical debt monotonically decreases.

## Core Concepts

### Rules

A **rule** is a pattern that matches undesirable code constructs. Rules are identified by a unique `rule-id` (e.g., `no-unwrap`, `no-todo-comments`).

Rules come in two forms:

1. **Regex rules**: Match text patterns in source files
2. **AST rules**: Tree-sitter queries that match syntactic structures

### Regions

A **region** is a directory path explicitly configured in `ratchet-counts.toml` for a specific rule. Regions are scoped per-rule: the same directory may be a region for one rule but not another.

Key principles:

- Regions exist **only** when explicitly listed in configuration
- The root region `"."` is always implicitly available
- Files in unconfigured directories are counted toward their nearest configured ancestor region
- The same directory path may be a region for some rules but not others (per-rule scoping)

Region paths are always relative to the repository root and use forward slashes (e.g., `src/parser`, `tests`).

### Counts

A **count** (or **budget**) is the maximum number of tolerated violations for a specific rule in a specific region. Counts are stored in version control and represent a contract: the code must not exceed these limits.

Semantics:
- Each configured region has its own explicit budget
- Files in unconfigured directories count toward their nearest configured ancestor region's budget
- The root region `"."` defaults to count `0` (no violations permitted) if not explicitly set
- A count of `0` means the rule is strictly enforced (no violations allowed in that region)

### The Ratchet Mechanism

The tool enforces monotonic improvement:

1. **Check**: Violations exceeding the budget fail the build
2. **Tighten**: Budgets can be reduced to match current (lower) violation counts
3. **Bump**: Budgets can be increased only by explicit human action with justification in the commit message

Agents and automated processes may tighten but never bump.

### Region Creation Policy

**Regions are created only by humans, never by ratchet commands.**

- `ratchets init`: Creates default configuration with only the root region `"."`
- `ratchets check`: Read-only; never modifies configuration
- `ratchets bump`: Updates budgets for existing regions only; fails if region doesn't exist
- `ratchets tighten`: Updates budgets for existing regions only; never adds new regions

To create a new region, a human must manually edit `ratchet-counts.toml` and add the region path as a key under the relevant rule section. This ensures that region structure is an intentional architectural decision, not an artifact of tool behavior.

## File Structure

A ratchets-enabled repository contains:

```
project/
├── ratchets.toml           # Configuration: enabled rules, languages, options
├── ratchet-counts.toml    # Violation budgets per rule per region
├── ratchets/              # Custom rule definitions
│   ├── regex/             # Custom regex rules (*.toml)
│   └── ast/               # Custom AST rules (*.toml with tree-sitter queries)
└── src/                   # Source code to be checked
```

### ratchets.toml

The configuration file specifies which rules are enabled and global settings.

```toml
# Ratchets configuration

[ratchets]
version = "1"

# Languages to analyze (determines which parsers to load)
languages = ["rust", "typescript", "python"]

# File patterns to include (glob syntax)
include = ["src/**", "tests/**"]

# File patterns to exclude (glob syntax)
exclude = ["**/generated/**", "**/vendor/**"]

[rules]
# Enable built-in rules by ID
# Values: true (enable with defaults), false (disable), or table (enable with options)

no-unwrap = true
no-expect = true
no-todo-comments = { severity = "warning" }
no-fixme-comments = false

[rules.custom]
# Enable custom rules from ratchets/ directory
# Reference by filename (without .toml extension)

my-company-rule = true
legacy-api-usage = { regions = ["src/legacy/**"] }

[output]
# Default output format: "human" or "jsonl"
format = "human"

# Colorize human output (auto-detected if not specified)
color = "auto"
```

### ratchet-counts.toml

The counts file stores violation budgets. Structure is `[rule-id.region-path]`.

```toml
# Ratchets violation budgets
# These counts represent the maximum tolerated violations.
# Counts can only be reduced (tightened) or explicitly bumped with justification.

[no-unwrap]
# Root default: 0 (inherited by all regions unless overridden)
"." = 0
"src/legacy" = 15
"src/legacy/parser" = 7
"tests" = 50

[no-todo-comments]
"." = 0
"src" = 23

[my-company-rule]
"src/experimental" = 5
```

**Region membership example**: For rule `no-unwrap` with configured regions `"."`, `"src/legacy"`, and `"src/legacy/parser"`:
- `src/foo/bar.rs` → belongs to region `"."` (no configured region matches `src/foo`) → budget 0
- `src/legacy/foo.rs` → belongs to region `"src/legacy"` → budget 15
- `src/legacy/parser/x.rs` → belongs to region `"src/legacy/parser"` → budget 7
- `src/legacy/parser/nested/deep.rs` → belongs to region `"src/legacy/parser"` (most specific match) → budget 7

Note: `tests/test.rs` would also belong to region `"."` since `"tests"` is not configured for this rule.

### Custom Rule Definitions

#### Regex Rules (`ratchets/regex/*.toml`)

```toml
[rule]
id = "no-console-log"
description = "Disallow console.log statements"
severity = "error"

[match]
# Regex pattern (Rust regex syntax)
pattern = "console\\.log\\s*\\("

# File types this rule applies to (optional, defaults to all)
languages = ["javascript", "typescript"]

# Additional file glob filter (optional)
include = ["src/**"]
exclude = ["src/debug/**"]
```

#### AST Rules (`ratchets/ast/*.toml`)

```toml
[rule]
id = "no-unwrap-custom"
description = "Disallow .unwrap() calls in production code"
severity = "error"

[match]
# Tree-sitter query (S-expression syntax)
# Captures are used for reporting location
query = """
(call_expression
  function: (field_expression
    field: (field_identifier) @method)
  (#eq? @method "unwrap")) @violation
"""

# Language this query applies to
language = "rust"

# File patterns
include = ["src/**"]
exclude = ["tests/**", "benches/**"]
```

## Commands

### `ratchets init`

Initialize a repository for use with Ratchets.

```
ratchets init [--force]
```

Behavior:
- Creates `ratchets.toml` with sensible defaults
- Creates empty `ratchet-counts.toml`
- Creates `ratchets/regex/` and `ratchets/ast/` directories
- If files exist: skip without `--force`, overwrite with `--force`
- Idempotent: safe to run multiple times

### `ratchets check`

Verify that the codebase complies with all enabled rules within budgets.

```
ratchets check [--format human|jsonl] [PATH...]
```

Behavior:
- Parses configuration and counts
- Loads necessary parsers (lazy: only languages present in matched files)
- Runs all enabled rules in parallel
- Aggregates violations per rule per region
- Compares against budgets
- Reports violations and budget status

Exit codes:
- `0`: All rules within budget
- `1`: At least one rule exceeded budget
- `2`: Configuration or usage error (invalid config, missing files, bad arguments)

### `ratchets bump <rule-id> [--region <path>] [--count <n>]`

Increase the violation budget for a rule.

```
ratchets bump no-unwrap --region src/legacy --count 20
ratchets bump no-unwrap --region src/legacy  # Auto-detect current count
```

Behavior:
- If `--count` provided: set budget to that value
- If `--count` omitted: run check for that rule/region, use current violation count
- Updates `ratchet-counts.toml`
- Fails if new count is lower than current violations (use `tighten` instead)
- **Never creates new regions**: the specified region must already exist in configuration

**Important**: Bumping should be accompanied by justification in the git commit message. This is a social contract, not enforced by the tool.

### `ratchets tighten [<rule-id>] [--region <path>]`

Reduce budgets to match current violation counts.

```
ratchets tighten                    # Tighten all rules, all regions
ratchets tighten no-unwrap          # Tighten specific rule, all regions
ratchets tighten --region src/      # Tighten all rules in region
```

Behavior:
- Runs check to get current violation counts
- For each **configured** rule/region: if current < budget, reduce budget to current
- Fails if any current > budget (violations exist beyond budget)
- Updates `ratchet-counts.toml`
- **Never creates new regions**: only updates budgets for regions already in configuration

### `ratchets merge-driver`

Git merge driver for `ratchet-counts.toml` that resolves conflicts by taking the minimum count.

```
# In .gitattributes:
ratchet-counts.toml merge=ratchets

# In .git/config or ~/.gitconfig:
[merge "ratchets"]
    name = Ratchets counts merge driver (minimum wins)
    driver = ratchets merge-driver %O %A %B
```

Behavior:
- Parses base (`%O`), ours (`%A`), and theirs (`%B`)
- For each rule/region: result = min(ours, theirs)
- Writes merged result to `%A`
- Exit `0` on success, non-zero on parse failure

### `ratchets list`

List all enabled rules and their current status.

```
ratchets list [--format human|jsonl]
```

Output includes:
- Rule ID
- Source (built-in, custom regex, custom AST)
- Languages
- Current violation count
- Budget
- Status (ok, exceeded, warning)

## Output Formats

### Human Format (default)

Designed for terminal display with optional color.

```
✗ no-unwrap: 4 violations (budget: 3) in src/legacy/parser
  src/legacy/parser/lexer.rs:42:10  .unwrap()
  src/legacy/parser/lexer.rs:87:15  .unwrap()
  src/legacy/parser/ast.rs:23:8     .unwrap()
  src/legacy/parser/ast.rs:156:12   .unwrap()

✓ no-todo-comments: 21 violations (budget: 23) in src

Summary: 1 rule exceeded budget, 1 rule within budget
```

### JSONL Format

Machine-readable output for agent consumption. One JSON object per line.

#### Violation Record

```json
{"type":"violation","rule":"no-unwrap","file":"src/legacy/parser/lexer.rs","line":42,"column":10,"end_line":42,"end_column":18,"snippet":".unwrap()","message":"Disallow .unwrap() calls","region":"src/legacy/parser"}
```

#### Summary Record

```json
{"type":"summary","rule":"no-unwrap","region":"src/legacy/parser","violations":4,"budget":3,"status":"exceeded"}
```

#### Final Status Record

```json
{"type":"status","passed":false,"rules_checked":2,"rules_exceeded":1,"total_violations":25}
```

### Output Schema (for evolvability)

All JSONL records include:
- `type`: Discriminator for record type
- `version`: Schema version (omitted = v1, future versions will include explicitly)

Reserved fields for future use:
- `severity`: warning, error, info
- `fix`: Suggested automatic fix
- `related`: Related violations or locations
- `metadata`: Rule-specific additional data

## Exit Codes

| Code | Meaning |
|------|---------|
| 0 | Success: all rules within budget |
| 1 | Failure: at least one rule exceeded budget |
| 2 | Error: configuration, usage, or I/O error |
| 3 | Error: parse failure (invalid source file for AST rule) |

Codes 4-127 are reserved for future use.

## Rule Registry

### Single Point of Interface

The **RuleRegistry** is the canonical interface for loading and managing rules in Ratchets. All rule loading in normal operation MUST go through the `RuleRegistry::build_from_config()` method. This single point of interface ensures:

- **Consistency**: All components load rules the same way
- **No duplicates**: Rules with the same ID are properly deduplicated through override behavior
- **Proper filtering**: Configuration-based and language-based filters are applied uniformly

### Loading Order

`RuleRegistry::build_from_config()` loads rules in a specific order that supports overrides:

1. **Embedded rules**: Built-in rules compiled into the binary (from `include_str!` macros)
2. **Filesystem builtin rules**: Rules from `builtin-ratchets/` directory (for development/overrides)
3. **Custom rules**: User-defined rules from `ratchets/` directory
4. **Config filter**: Remove disabled rules based on `ratchets.toml` settings
5. **Language filter**: Remove rules for unconfigured languages

Later rules override earlier rules with the same ID. This allows filesystem rules to override embedded rules, and custom rules to override builtin rules.

### Rule Structure

Rules are organized by type:

- **Regex rules**: Language-agnostic pattern matching in `common/regex/` and per-language `regex/` directories
- **AST rules**: Tree-sitter queries in per-language `ast/` directories

Builtin rules use a language-first directory structure:
```
builtin-ratchets/
├── common/regex/           # Language-agnostic regex rules
├── rust/ast/               # Rust AST rules
├── python/ast/             # Python AST rules
└── typescript/ast/         # TypeScript AST rules
```

Custom rules use a type-first structure:
```
ratchets/
├── regex/                  # Custom regex rules (all languages)
└── ast/                    # Custom AST rules (all languages)
```

### Usage in Commands

All commands that need rules use `RuleRegistry::build_from_config()`:

- `ratchets check`: Loads all enabled rules filtered by config
- `ratchets bump`: Validates rule exists before bumping budget
- `ratchets tighten`: Loads all enabled rules to count violations
- `ratchets list`: Lists all enabled rules with their status

This centralization eliminates rule loading duplication and ensures all commands see the same set of rules.

## Design Principles

### Performance

1. **Parallel execution**: Parse files and run rules using all available cores
2. **Lazy parser loading**: Only load tree-sitter grammars for languages actually present
3. **Early termination**: Option to stop on first budget exceeded (for fast CI feedback)
4. **Incremental potential**: Design allows future support for only checking changed files

### Agent-First Design

1. **Structured output**: JSONL format with stable schema for programmatic parsing
2. **Actionable messages**: Include file, line, column, snippet for precise location
3. **Clear status**: Unambiguous pass/fail with exit codes
4. **Deterministic**: Same input produces same output (sorted, no timing-dependent order)

### Unix Philosophy

1. **Do one thing well**: Check code against rules with budgets
2. **Composable**: Exit codes and structured output integrate with other tools
3. **No network**: Runs entirely locally, no telemetry or remote calls
4. **Text configuration**: TOML files are human-readable and diff-friendly

### Safety

1. **Read-only by default**: `check` never modifies files
2. **Explicit mutations**: Only `init`, `bump`, `tighten` modify config files
3. **No code execution**: Rules are declarative (regex, tree-sitter queries), not arbitrary code
4. **Auditable**: All budget changes are visible in version control diffs