blame.md
markdown
| 1 | # `muse blame` — line-level attribution for text files |
| 2 | |
| 3 | `muse blame` shows which commit last modified each line of a text file tracked in `state/`. It is the universal, domain-agnostic attribution tool — the core VCS equivalent of `git blame`. |
| 4 | |
| 5 | For domain-specific attribution, see: |
| 6 | - `muse midi note-blame` — per-bar attribution in MIDI tracks |
| 7 | - `muse code blame` — per-symbol attribution in code files |
| 8 | |
| 9 | ## Usage |
| 10 | |
| 11 | ```bash |
| 12 | muse blame README.md |
| 13 | muse blame --ref v1.0.0 docs/design.md |
| 14 | muse blame --porcelain state/config.toml | jq '.commit_id' |
| 15 | muse blame --short 8 src/main.py |
| 16 | ``` |
| 17 | |
| 18 | ## Options |
| 19 | |
| 20 | | Flag | Default | Description | |
| 21 | |------|---------|-------------| |
| 22 | | `--ref`, `-r` | HEAD | Commit or branch to blame from | |
| 23 | | `--porcelain`, `-p` | false | Emit JSON objects instead of human-readable output | |
| 24 | | `--short` | 12 | Length of commit SHA prefix to display | |
| 25 | |
| 26 | ## Output (human-readable) |
| 27 | |
| 28 | ``` |
| 29 | cccccccc0000 (Test User 2026-03-19) 1 hello world |
| 30 | ``` |
| 31 | |
| 32 | Columns: |
| 33 | - Commit SHA prefix (12 chars by default) |
| 34 | - Author name (padded) |
| 35 | - Date (`YYYY-MM-DD`) |
| 36 | - Line number (right-aligned) |
| 37 | - Line content |
| 38 | |
| 39 | ## Output (`--porcelain`) |
| 40 | |
| 41 | One JSON object per line: |
| 42 | |
| 43 | ```json |
| 44 | {"lineno": 1, "commit_id": "cccccccc…", "author": "Test User", "committed_at": "2026-03-19T14:22:01+00:00", "message": "initial commit", "content": "hello world"} |
| 45 | ``` |
| 46 | |
| 47 | ## How attribution works |
| 48 | |
| 49 | The blame algorithm walks the commit graph backwards from the requested ref: |
| 50 | |
| 51 | 1. Every line in the file at HEAD is initially attributed to HEAD. |
| 52 | 2. For each ancestor commit, a unified diff is computed between the ancestor's version and its child's. |
| 53 | 3. Lines that appear unchanged in both versions are **re-attributed to the ancestor** — the older commit that first introduced them. |
| 54 | 4. Lines that were added by a commit remain attributed to that commit. |
| 55 | |
| 56 | This is equivalent to Git's blame algorithm for linear histories. For merge commits, the algorithm chooses the first parent to keep attribution clean. |
| 57 | |
| 58 | ## Agent workflows |
| 59 | |
| 60 | ### Find who introduced a bug |
| 61 | |
| 62 | ```bash |
| 63 | muse blame --porcelain src/parser.py \ |
| 64 | | jq 'select(.content | test("legacy_mode")) | .commit_id' \ |
| 65 | | sort -u |
| 66 | ``` |
| 67 | |
| 68 | ### Audit a configuration file |
| 69 | |
| 70 | ```bash |
| 71 | muse blame config/production.toml |
| 72 | ``` |
| 73 | |
| 74 | ### Annotate every line with its commit |
| 75 | |
| 76 | ```bash |
| 77 | muse blame --porcelain README.md > blame-report.jsonl |
| 78 | ``` |
| 79 | |
| 80 | ## Limitations |
| 81 | |
| 82 | - Only text files are supported. Binary files (images, MIDI, etc.) should use domain-specific blame commands. |
| 83 | - The walk follows the first-parent chain for merge commits. To attribute across both parents, use `muse midi note-blame` or `muse code blame` which are domain-aware. |
| 84 | |
| 85 | ## Exit codes |
| 86 | |
| 87 | | Code | Meaning | |
| 88 | |------|---------| |
| 89 | | 0 | Success | |
| 90 | | 1 | File not found at the specified ref, or ref not found | |