Metadata-Version: 2.4
Name: sheridan-diffract
Version: 1.0.1
Summary: A short description of your project
Project-URL: Repository, https://github.com/andrewasheridan/diffract
Project-URL: Issues, https://github.com/andrewasheridan/diffract/issues
Project-URL: Changelog, https://github.com/andrewasheridan/diffract/blob/main/CHANGELOG.md
Author-email: sheridan <andrewasheridan@gmail.com>
License: MIT
License-File: LICENSE
Requires-Python: >=3.14
Requires-Dist: gitpython
Requires-Dist: sheridan-iceberg>=2.0.0
Provides-Extra: dev
Requires-Dist: bandit[toml]>=1.9.4; extra == 'dev'
Requires-Dist: commitizen>=4.13.9; extra == 'dev'
Requires-Dist: mkdocstrings[python]>=1.0.3; extra == 'dev'
Requires-Dist: mypy>=1.19.1; extra == 'dev'
Requires-Dist: pre-commit>=4.5.1; extra == 'dev'
Requires-Dist: pytest-cov>=7.1.0; extra == 'dev'
Requires-Dist: pytest-mock>=3.14.0; extra == 'dev'
Requires-Dist: pytest>=9.0.2; extra == 'dev'
Requires-Dist: ruff>=0.15.7; extra == 'dev'
Requires-Dist: zensical; extra == 'dev'
Description-Content-Type: text/markdown

# sheridan-diffract

**Detects how a Python package's public API changed between two git commits and classifies that change as a conventional commit type.**

Most semver tools trust the developer to classify their own changes correctly. `diffract` verifies that classification against the actual diff — catching the common case where someone writes `fix:` but actually removed a public function.

## How it works

1. Uses [sheridan-iceberg](https://github.com/andrewasheridan/iceberg) to extract the public API surface at two points in git history
2. Diffs those two surfaces to find what was added or removed
3. Maps the diff to a conventional commit classification:

| Change | Commit type |
|---|---|
| Public name removed | `feat!` (breaking) |
| Public name added | `feat` |
| Only internal/private changes | `refactor` |
| No changes detected | `fix` |

## Installation

```bash
pip install sheridan-diffract
```

## Usage

### CLI

```bash
# Compare the last two commits (default)
diffract

# Compare specific refs
diffract HEAD~3 HEAD

# Custom source directory (default: src/)
diffract --src lib/

# Emit JSON for CI consumption
diffract --json

# Exit non-zero if a breaking change is detected (for CI gates)
diffract --exit-code
```

**Exit codes with `--exit-code`:**
- `0` — no API surface changes
- `1` — breaking change (public name removed)
- `2` — non-breaking API change (public name added)
- `3` — error (git or surface extraction failure)

**Example output:**

```
Detected: feat!  (breaking change)

Removed public names:
  sheridan.diffract.enums:
    - OldHelper

Suggested commit prefix: feat!:
```

```
Detected: fix

No public API changes detected.

Suggested commit prefix: fix:
```

Scopes are passed through to the suggestion — if your commit message includes `(parser)`, the suggested prefix will too:

```
Suggested commit prefix: feat(parser):
```

### Python API

```python
from sheridan.diffract import check

result = check(base_ref="v1.2.0", head_ref="HEAD")
print(result.commit_type)   # e.g. "feat!"
print(result.summary)       # human-readable description
print(result.diff.removed)  # tuple of NameChange objects
print(result.diff.added)

# JSON-serialisable dict for CI output
import json
print(json.dumps(result.to_dict(), indent=2))
```

To compare `HEAD` against the **staging area** (what the next commit will contain) — the same comparison the pre-commit hook uses:

```python
from sheridan.diffract import check_staged

result = check_staged()
print(result.commit_type)  # reflects what is staged, not the last commit
```

### Validate commit messages with pre-commit

`diffract` ships a `commit-msg` hook that rejects commits whose conventional commit type doesn't match the detected API change — catching `fix:` when you actually removed a public name.

```yaml
# .pre-commit-config.yaml
repos:
  - repo: https://github.com/sheridan/diffract
    rev: v<VERSION>
    hooks:
      - id: diffract-validate
```
- make sure to `pre-commit install --hook-type commit-msg` first

The hook compares `HEAD` against the **git staging area** (not `HEAD~1 → HEAD`), so it correctly detects what is about to be committed rather than what was committed last. Explicit `BASE`/`HEAD` refs can be added to override this (see the GitHub Actions example below).

Scopes are preserved in all output — if you write `fix(parser): …`, the mismatch message will show `fix(parser):` as written and `feat(parser):` as the suggested replacement:

```
diffract: commit type mismatch
  written:  fix(parser):
  detected: feat(parser):
```

Non-conventional commit types (`docs:`, `chore:`, `test:`, etc.) are never blocked.

### Configuration

If your source code isn't in `src/`, tell diffract once in a config file rather than repeating it on every command:

**`diffract.toml`** (takes precedence):
```toml
src = "python/src"
```

**`pyproject.toml`**:
```toml
[tool.diffract]
src = "python/src"
```

Priority: explicit `--src` flag → `diffract.toml` → `pyproject.toml` → default (`src/`).

### As a GitHub Actions check

Validate the **PR title** against detected API changes — the CI equivalent of the pre-commit hook.
The refs to diff are the PR branch HEAD versus the target branch HEAD:

```yaml
# .github/workflows/diffract.yml
name: diffract

on:
  pull_request:
    types: [opened, synchronize, reopened, edited]  # 'edited' catches title-only changes

jobs:
  api-change-check:
    name: Validate PR title against API changes
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0  # required to access both commit SHAs

      - name: Install diffract
        run: pip install sheridan-diffract

      - name: Validate PR title type matches API change
        env:
          PR_TITLE: ${{ github.event.pull_request.title }}
        run: |
          printf '%s' "$PR_TITLE" > /tmp/pr-title.txt
          diffract \
            ${{ github.event.pull_request.base.sha }} \
            ${{ github.event.pull_request.head.sha }} \
            --validate-msg-file /tmp/pr-title.txt
```

- `base.sha` — HEAD of the target branch (e.g. `main`) at the time of the PR event
- `head.sha` — HEAD of the PR branch
- Non-conventional title prefixes (`docs:`, `chore:`, `test:`, etc.) are never blocked
- Scopes are passed through: a title of `feat(parser): …` will suggest `feat(parser):` on mismatch

## What it does not do

- `diffract` has **no AST logic of its own**. It delegates all API surface extraction to `sheridan-iceberg`.
- It compares public names only (additions and removals). **Signature changes** (argument renames, return type changes) are not currently detected — iceberg returns name lists, not signatures.
- It does not modify commits or rewrite history. It only reports.

## Development

```bash
task install        # install all dependencies
task check          # lint + format + typecheck + tests (must all pass)
task test           # pytest with coverage (≥90% required)
```

See [CONTRIBUTING.md](CONTRIBUTING.md) for the full workflow.
