Metadata-Version: 2.4
Name: grain-lint
Version: 0.2.0
Summary: Anti-slop linter for AI-assisted codebases
Project-URL: Homepage, https://grainlinter.com
Project-URL: Repository, https://github.com/mmartoccia/grain
Requires-Python: >=3.11
Description-Content-Type: text/markdown
Provides-Extra: dev
Requires-Dist: pytest>=7; extra == "dev"

# grain

Anti-slop linter for AI-assisted codebases. Detects AI-generated code and documentation patterns before they land in version control.

## What it does

AI code has tells. `grain` flags them so a human can decide whether to keep, rewrite, or suppress. It does not auto-fix -- fixing requires judgment.

## Quick start

```bash
pip install grain-lint   # from PyPI
pip install -e .         # from source
```

## Usage

```bash
grain check [files...]      # check specific files
grain check --all           # check entire repo
grain install               # install git hooks into .git/hooks/
grain status                # show current config and enabled checks
grain suppress FILE:LINE RULE  # add inline suppression comment
```

## Checks

### Python

| Rule | Severity | Description |
|------|----------|-------------|
| OBVIOUS_COMMENT | error | comment restates the following line |
| NAKED_EXCEPT | error | broad except clause with no re-raise |
| RESTATED_DOCSTRING | warn | docstring just expands the function/class name |
| VAGUE_TODO | error | TODO without specific approach or reason |
| SINGLE_IMPL_ABC | warn | ABC with exactly one concrete implementation |
| GENERIC_VARNAME | error | function named with AI filler (process_data, etc.) |
| TAG_COMMENT | warn | untagged comment -- requires `# TAG: description` format (opt-in) |

### Markdown

| Rule | Severity | Description |
|------|----------|-------------|
| HEDGE_WORD | error | AI filler words -- see `hedge_words` in config |
| THANKS_OPENER | error | README/CONTRIBUTING opens with "Thanks for contributing" |
| OBVIOUS_HEADER | warn | header content fully restated in following paragraph |
| BULLET_PROSE | warn | short bullet list that reads better as a sentence |
| TABLE_OVERKILL | warn | table with 1 row or constant column |

### Commit messages

| Rule | Severity | Description |
|------|----------|-------------|
| VAGUE_COMMIT | error | subject too generic (update, fix bug, wip...) |
| AND_COMMIT | error | subject contains "and" -- do one thing per commit |
| NO_CONTEXT | error | fix/feat with no description of what changed |

## Config

Create `.grain.toml` in your repo root:

```toml
[grain]
fail_on = ["OBVIOUS_COMMENT", "NAKED_EXCEPT", "HEDGE_WORD", "VAGUE_TODO", "VAGUE_COMMIT"]
warn_only = ["RESTATED_DOCSTRING", "SINGLE_IMPL_ABC", "BULLET_PROSE"]
ignore = []
exclude = ["tests/*", "migrations/*"]

[grain.python]
generic_varnames = ["process_data", "handle_response", "get_result", "do_thing"]
# allowed_comment_tags = ["TODO", "BUG", "FIX", "PERF", "NOTE", "HACK", "FIXME", "XXX", "SAFETY", "REVIEW"]

[grain.markdown]
hedge_words = ["robust", "seamless", "leverage", "cutting-edge", "powerful",
               "you might want to", "consider using", "it's worth noting", "note that"]
```

## Custom Rules

Define your own pattern-matching rules in `.grain.toml`. Each custom rule has a name, a regex pattern, a file glob, a message, and an optional severity. grain evaluates them alongside built-in rules.

```toml
[[grain.custom_rules]]
name = "CONST_SETTING"
pattern = '^\s*[A-Z_]{2,}\s*=\s*\d+'
files = "*.py"
message = "top-level constant assignment -- use config or env vars"
severity = "warn"

[[grain.custom_rules]]
name = "PRINT_DEBUG"
pattern = '^\s*print\s*\('
files = "*.py"
message = "print() call -- use logging instead"
severity = "error"

[[grain.custom_rules]]
name = "FIXME_DEADLINE"
pattern = 'FIXME(?!.*\d{4}-\d{2}-\d{2})'
files = "*.py"
message = "FIXME without a deadline date (YYYY-MM-DD)"
severity = "warn"
```

**Fields:**

| Field | Required | Description |
|-------|----------|-------------|
| `name` | yes | Uppercase + underscores (e.g. `MY_RULE`) |
| `pattern` | yes | Python regex, matched per-line |
| `files` | yes | File glob (e.g. `*.py`, `*.md`) |
| `message` | yes | Human-readable violation message |
| `severity` | no | `"warn"` (default) or `"error"` |

Custom rule names work with `ignore`, `fail_on`, and `warn_only` just like built-in rules. Invalid rules (bad regex, missing fields) are skipped with a warning.

## Opt-in rules

Some rules are strict enough that they're off by default. Add them to `warn_only` or `fail_on` in `.grain.toml` to activate:

```toml
[grain]
warn_only = ["TAG_COMMENT"]
```

**TAG_COMMENT** requires every comment to use a structured tag format (`# TODO: ...`, `# NOTE: ...`, etc.). Section headers, dividers, shebangs, `type: ignore`, and `noqa` are automatically skipped.

## Suppression

Add `# grain: ignore RULE_NAME` to the offending line:

```python
except Exception as e:  # grain: ignore NAKED_EXCEPT
    pass  # intentional -- this is a top-level catch
```

Or use the CLI:

```bash
grain suppress src/main.py:42 NAKED_EXCEPT
```

## pre-commit framework

```yaml
# .pre-commit-config.yaml
repos:
  - repo: https://github.com/mmartoccia/grain
    rev: v0.2.0
    hooks:
      - id: grain
```

## Output format

```
path/to/file.py:42  [FAIL] OBVIOUS_COMMENT  "# return result" restates the following line
path/to/README.md:7  [FAIL] HEDGE_WORD  "robust" signals AI-generated prose
```

Exit 0 = clean. Exit 1 = errors found (pre-commit blocks the commit).
