Metadata-Version: 2.4
Name: tarnished
Version: 0.3.0
Summary: File state checkpoint tool - has your code tarnished?
Project-URL: Homepage, https://github.com/RasmusGodske/tarnished
Project-URL: Documentation, https://github.com/RasmusGodske/tarnished#readme
Project-URL: Repository, https://github.com/RasmusGodske/tarnished
Project-URL: Issues, https://github.com/RasmusGodske/tarnished/issues
Project-URL: Changelog, https://github.com/RasmusGodske/tarnished/releases
Author: Rasmus Godske
Maintainer: Rasmus Godske
License: MIT
Keywords: checkpoint,checksum,cli,development,file-state,verification
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Quality Assurance
Classifier: Topic :: Software Development :: Testing
Classifier: Topic :: Utilities
Classifier: Typing :: Typed
Requires-Python: >=3.11
Provides-Extra: dev
Requires-Dist: mypy>=1.0; extra == 'dev'
Requires-Dist: pytest>=7.0; extra == 'dev'
Requires-Dist: ruff>=0.1; extra == 'dev'
Description-Content-Type: text/markdown

# tarnished

A CLI tool that tracks which quality checks need re-running after code changes.

## The Problem

AI coding assistants often forget to re-run tests after making changes:

```
Timeline                              What Happens
─────────────────────────────────────────────────────────────────────
1. Modify 5 PHP files
2. Run tests                          Tests pass
3. Modify 2 more PHP files            (to fix a review comment)
4. Say "Done!"                        PROBLEM: Tests not re-run after step 3
─────────────────────────────────────────────────────────────────────
Result: User discovers broken tests later
```

This happens because AI assistants lack persistent memory about which files changed after which checks ran.

## The Solution

**tarnished** tracks file state via checksums. Modified files become "tarnished" until their checks pass again.

```
Action                                Profile Status
─────────────────────────────────────────────────────────────────────
1. Modify 5 PHP files                 test:php = TARNISHED
2. Run tests (pass)                   test:php = CLEAN
3. Modify 2 more PHP files            test:php = TARNISHED
4. Run `tarnished status`             Shows test:php needs re-running
5. Run tests (pass)                   test:php = CLEAN
6. Say "Done!"                        Confident everything works
─────────────────────────────────────────────────────────────────────
```

## Installation

```bash
# Using uv (recommended)
uv tool install tarnished

# Using pip
pip install tarnished

# From source
git clone https://github.com/RasmusGodske/tarnished.git
cd tarnished
uv tool install .
```

## Quick Start

```bash
# Initialize configuration
tarnished init

# Save a checkpoint after a successful check
tarnished save lint:php "app/**/*.php" "tests/**/*.php"

# Check if files have changed since last checkpoint
tarnished check lint:php

# See status of all profiles
tarnished status
```

## Concepts

```
┌─────────────────────────────────────────────────────────────────┐
│  PROFILE                                                        │
│  A named set of file patterns to track                          │
│                                                                 │
│  Example: "test:php" tracks app/**/*.php and tests/**/*.php     │
└─────────────────────────────────────────────────────────────────┘
         │
         │ when files matching patterns are modified
         ▼
┌─────────────────────────────────────────────────────────────────┐
│  TARNISHED                                                      │
│  Files changed since the last checkpoint                        │
│                                                                 │
│  Action required: Run the associated check                      │
└─────────────────────────────────────────────────────────────────┘
         │
         │ when check passes and checkpoint is saved
         ▼
┌─────────────────────────────────────────────────────────────────┐
│  CLEAN                                                          │
│  No changes since the last successful check                     │
│                                                                 │
│  Action required: None - safe to skip                           │
└─────────────────────────────────────────────────────────────────┘
```

## Profile Statuses

| Status | Meaning | Action |
|--------|---------|--------|
| `clean` | No files changed since last pass | Safe to skip |
| `tarnished` | Files changed since last pass | Must re-run check |
| `never_saved` | Check has never passed | Must run check |

## Commands

| Command | Description |
|---------|-------------|
| `tarnished init` | Initialize .tarnished/ directory |
| `tarnished save <profile> [patterns...]` | Save checkpoint for profile |
| `tarnished check <profile>` | Check if profile is tarnished |
| `tarnished status` | Show JSON status of all profiles |

### Exit Codes for `check`

| Code | Meaning |
|------|---------|
| 0 | Clean - no changes since last checkpoint |
| 1 | Tarnished - files have changed |
| 2 | Unknown - profile not found |

## File Structure

```
.tarnished/
├── config.json    # Profile definitions (commit this)
└── state.json     # Checkpoint data (gitignore this)
```

## Implementation

tarnished fingerprints each matched file at save time (size + mtime + content
hash), then checks like git's index does: files whose size and mtime are
unchanged are trusted without reading; files whose stat drifted are verified
by content hash. Operations that rewrite identical bytes — `git pull`,
`git checkout`, code generators — therefore compare **clean** instead of
producing false "tarnished" reports. When a check verifies drifted files by
content, the stored fingerprints are refreshed so subsequent checks return to
the pure-stat fast path.

- **Git-independent** - works regardless of commit history
- **Honest** - changed mtime with identical content is not a change
- **Fast** - file contents are only read at save time and for stat-drifted
  files at check time
- **Simple** - plain JSON files

## Integration Examples

### Test Script Wrapper

```bash
#!/bin/bash
# run-tests.sh

php artisan test "$@"
exit_code=$?

if [ $exit_code -eq 0 ]; then
    tarnished save test:php 2>/dev/null || true
fi

exit $exit_code
```

### Pre-commit Hook

```bash
#!/bin/bash
# .git/hooks/pre-commit

status=$(tarnished status 2>/dev/null)
if echo "$status" | grep -q '"tarnished"'; then
    echo "Tarnished profiles detected. Run checks before committing."
    tarnished status
    exit 1
fi
```

### CI Pipeline

```yaml
- name: Run tests if needed
  run: |
    if ! tarnished check tests; then
      npm test && tarnished save tests
    fi
```

### Claude Code / AI Assistant

Add to your `CLAUDE.md`:

```markdown
Before completing any task, run `tarnished status`:
- If a profile is "tarnished", run that check
- If the check fails, fix it and re-run
- Only complete when relevant profiles are "clean"
```

## Example Configuration

```json
{
  "profiles": {
    "lint:php": {
      "patterns": ["app/**/*.php", "tests/**/*.php", "config/**/*.php"]
    },
    "lint:js": {
      "patterns": ["resources/js/**/*.vue", "resources/js/**/*.ts"]
    },
    "test:php": {
      "patterns": ["app/**/*.php", "tests/**/*.php"]
    },
    "test:e2e": {
      "patterns": ["e2e/**/*.ts", "resources/js/**/*.vue"]
    }
  }
}
```

## Why "Tarnished"?

Like silver that tarnishes when exposed to air, code "tarnishes" when modified. It needs polishing (running checks) before it's ready. Once checks pass, the code is clean again.

## License

MIT
