Skip to main content

File state checkpoint tool - has your code tarnished?

Project description

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

# 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

# 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

#!/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

#!/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

- 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:

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

{
  "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

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

tarnished-0.3.0.tar.gz (10.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

tarnished-0.3.0-py3-none-any.whl (12.8 kB view details)

Uploaded Python 3

File details

Details for the file tarnished-0.3.0.tar.gz.

File metadata

  • Download URL: tarnished-0.3.0.tar.gz
  • Upload date:
  • Size: 10.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for tarnished-0.3.0.tar.gz
Algorithm Hash digest
SHA256 b1360e76e2b4999c70c4d416a556390069f7fa145dce4621394c1cf795257197
MD5 4dbbb81dea1aa095626ba0a12b3fdf87
BLAKE2b-256 e078e795fc64d821ca0abceea9c10bcd16c53a86a7ceb3cffe4925b1414730aa

See more details on using hashes here.

Provenance

The following attestation bundles were made for tarnished-0.3.0.tar.gz:

Publisher: publish.yml on RasmusGodske/tarnished

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file tarnished-0.3.0-py3-none-any.whl.

File metadata

  • Download URL: tarnished-0.3.0-py3-none-any.whl
  • Upload date:
  • Size: 12.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for tarnished-0.3.0-py3-none-any.whl
Algorithm Hash digest
SHA256 ace6633293bf7c33628c5c53a0ad0eec7dda4bf6b8e9d551c6a46774d0a53c1a
MD5 b7f74332cb78b7d0ac43438d780d3899
BLAKE2b-256 7878910823f4ed7cfa24de5aa5ce4992264bc0960812974f76e824550965fca3

See more details on using hashes here.

Provenance

The following attestation bundles were made for tarnished-0.3.0-py3-none-any.whl:

Publisher: publish.yml on RasmusGodske/tarnished

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page