sheridan-diffract 1.0.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. sheridan_diffract-1.0.1/.github/workflows/bump.yaml +99 -0
  2. sheridan_diffract-1.0.1/.github/workflows/ci.yaml +37 -0
  3. sheridan_diffract-1.0.1/.github/workflows/docs.yaml +35 -0
  4. sheridan_diffract-1.0.1/.github/workflows/pr-title.yaml +24 -0
  5. sheridan_diffract-1.0.1/.github/workflows/publish.yaml +68 -0
  6. sheridan_diffract-1.0.1/.github/workflows/tag-on-merge.yaml +51 -0
  7. sheridan_diffract-1.0.1/.gitignore +41 -0
  8. sheridan_diffract-1.0.1/.pre-commit-config.yaml +20 -0
  9. sheridan_diffract-1.0.1/.pre-commit-hooks.yaml +23 -0
  10. sheridan_diffract-1.0.1/CHANGELOG.md +12 -0
  11. sheridan_diffract-1.0.1/CLAUDE.md +146 -0
  12. sheridan_diffract-1.0.1/CONTRIBUTING.md +63 -0
  13. sheridan_diffract-1.0.1/LICENSE +21 -0
  14. sheridan_diffract-1.0.1/PKG-INFO +228 -0
  15. sheridan_diffract-1.0.1/README.md +202 -0
  16. sheridan_diffract-1.0.1/Taskfile.yaml +113 -0
  17. sheridan_diffract-1.0.1/ci/__init__.py +0 -0
  18. sheridan_diffract-1.0.1/ci/pyproject.toml +12 -0
  19. sheridan_diffract-1.0.1/ci/src/__init__.py +0 -0
  20. sheridan_diffract-1.0.1/ci/src/main/__init__.py +128 -0
  21. sheridan_diffract-1.0.1/dagger.json +9 -0
  22. sheridan_diffract-1.0.1/docs/decisions/.gitkeep +0 -0
  23. sheridan_diffract-1.0.1/docs/decisions/001-delegate-to-iceberg.md +33 -0
  24. sheridan_diffract-1.0.1/docs/decisions/002-name-level-comparison.md +42 -0
  25. sheridan_diffract-1.0.1/docs/decisions/003-gitpython.md +46 -0
  26. sheridan_diffract-1.0.1/docs/decisions/004-pytest-mock.md +34 -0
  27. sheridan_diffract-1.0.1/docs/decisions/005-staged-index-comparison.md +38 -0
  28. sheridan_diffract-1.0.1/pyproject.toml +126 -0
  29. sheridan_diffract-1.0.1/src/sheridan/diffract/__init__.py +32 -0
  30. sheridan_diffract-1.0.1/src/sheridan/diffract/checker.py +106 -0
  31. sheridan_diffract-1.0.1/src/sheridan/diffract/classifier.py +74 -0
  32. sheridan_diffract-1.0.1/src/sheridan/diffract/cli.py +316 -0
  33. sheridan_diffract-1.0.1/src/sheridan/diffract/config.py +100 -0
  34. sheridan_diffract-1.0.1/src/sheridan/diffract/differ.py +48 -0
  35. sheridan_diffract-1.0.1/src/sheridan/diffract/enums.py +36 -0
  36. sheridan_diffract-1.0.1/src/sheridan/diffract/exceptions.py +17 -0
  37. sheridan_diffract-1.0.1/src/sheridan/diffract/git_utils.py +172 -0
  38. sheridan_diffract-1.0.1/src/sheridan/diffract/models.py +90 -0
  39. sheridan_diffract-1.0.1/src/sheridan/diffract/py.typed +0 -0
  40. sheridan_diffract-1.0.1/tests/__init__.py +0 -0
  41. sheridan_diffract-1.0.1/tests/conftest.py +41 -0
  42. sheridan_diffract-1.0.1/tests/test_checker.py +213 -0
  43. sheridan_diffract-1.0.1/tests/test_classifier.py +53 -0
  44. sheridan_diffract-1.0.1/tests/test_cli.py +827 -0
  45. sheridan_diffract-1.0.1/tests/test_config.py +98 -0
  46. sheridan_diffract-1.0.1/tests/test_differ.py +77 -0
  47. sheridan_diffract-1.0.1/tests/test_enums.py +42 -0
  48. sheridan_diffract-1.0.1/tests/test_exceptions.py +51 -0
  49. sheridan_diffract-1.0.1/tests/test_git_utils.py +299 -0
  50. sheridan_diffract-1.0.1/tests/test_models.py +137 -0
  51. sheridan_diffract-1.0.1/tests/test_package.py +60 -0
  52. sheridan_diffract-1.0.1/zensical.toml +24 -0
@@ -0,0 +1,99 @@
1
+ name: Bump version
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+
7
+ jobs:
8
+ bump:
9
+ name: Commitizen bump
10
+ runs-on: ubuntu-latest
11
+ permissions:
12
+ contents: write
13
+ pull-requests: write
14
+
15
+ steps:
16
+ - name: Check for bump commit (infinite loop guard)
17
+ id: guard
18
+ env:
19
+ HEAD_MSG: ${{ github.event.head_commit.message }}
20
+ run: |
21
+ if [[ "$HEAD_MSG" == bump:* ]]; then
22
+ echo "skip=true" >> "$GITHUB_OUTPUT"
23
+ else
24
+ echo "skip=false" >> "$GITHUB_OUTPUT"
25
+ fi
26
+
27
+ - uses: actions/checkout@v6
28
+ if: steps.guard.outputs.skip == 'false'
29
+ with:
30
+ fetch-depth: 0
31
+
32
+ - name: Set up Python 3.14
33
+ if: steps.guard.outputs.skip == 'false'
34
+ uses: actions/setup-python@v6
35
+ with:
36
+ python-version: "3.14"
37
+
38
+ - name: Set up uv
39
+ if: steps.guard.outputs.skip == 'false'
40
+ uses: astral-sh/setup-uv@v7
41
+
42
+ - name: Install dependencies
43
+ if: steps.guard.outputs.skip == 'false'
44
+ run: uv sync --all-extras --dev
45
+
46
+ - name: Configure git identity
47
+ if: steps.guard.outputs.skip == 'false'
48
+ run: |
49
+ git config user.name "github-actions[bot]"
50
+ git config user.email "github-actions[bot]@users.noreply.github.com"
51
+
52
+ - name: Bump version
53
+ if: steps.guard.outputs.skip == 'false'
54
+ id: bump
55
+ run: |
56
+ set +e
57
+ uv run cz bump --yes
58
+ code=$?
59
+ set -e
60
+ if [[ $code -eq 0 ]]; then
61
+ NEW_VERSION=$(uv run python -c "import tomllib; f=open('pyproject.toml','rb'); d=tomllib.load(f); print(d['project']['version'])")
62
+ echo "bumped=true" >> "$GITHUB_OUTPUT"
63
+ echo "new_version=$NEW_VERSION" >> "$GITHUB_OUTPUT"
64
+ elif [[ $code -eq 21 ]]; then
65
+ echo "No releasable commits — nothing to bump."
66
+ echo "bumped=false" >> "$GITHUB_OUTPUT"
67
+ else
68
+ echo "cz bump failed with exit code $code."
69
+ exit $code
70
+ fi
71
+ env:
72
+ FORCE_COLOR: "1"
73
+
74
+ - name: Push bump branch
75
+ if: steps.guard.outputs.skip == 'false' && steps.bump.outputs.bumped == 'true'
76
+ env:
77
+ NEW_VERSION: ${{ steps.bump.outputs.new_version }}
78
+ run: |
79
+ git push origin --delete "chore/version-bump-$NEW_VERSION" || true
80
+ git checkout -b "chore/version-bump-$NEW_VERSION"
81
+ git push origin "chore/version-bump-$NEW_VERSION"
82
+
83
+ - name: Open and auto-merge version bump PR
84
+ if: steps.guard.outputs.skip == 'false' && steps.bump.outputs.bumped == 'true'
85
+ env:
86
+ GH_TOKEN: ${{ secrets.RELEASE_TOKEN }}
87
+ NEW_VERSION: ${{ steps.bump.outputs.new_version }}
88
+ run: |
89
+ if gh pr list --head "chore/version-bump-$NEW_VERSION" --state open | grep -q .; then
90
+ echo "PR already open for chore/version-bump-$NEW_VERSION — skipping."
91
+ else
92
+ gh pr create \
93
+ --title "bump: $NEW_VERSION" \
94
+ --body "Automated version bump to $NEW_VERSION generated by Commitizen." \
95
+ --base main \
96
+ --head "chore/version-bump-$NEW_VERSION"
97
+ gh pr edit "chore/version-bump-$NEW_VERSION" --add-label automated || true
98
+ gh pr merge "chore/version-bump-$NEW_VERSION" --squash --auto
99
+ fi
@@ -0,0 +1,37 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ ci:
10
+ name: All gates
11
+ runs-on: ubuntu-latest
12
+ if: |
13
+ !startsWith(github.event.head_commit.message, 'bump:') &&
14
+ !startsWith(github.head_ref, 'chore/version-bump-')
15
+ steps:
16
+ - uses: actions/checkout@v6
17
+
18
+ - name: Restore Dagger engine cache
19
+ id: cache-dagger
20
+ uses: actions/cache@v5
21
+ with:
22
+ path: /tmp/dagger-engine.tar
23
+ key: dagger-engine-v0.20.3
24
+
25
+ - name: Load Dagger engine image
26
+ if: steps.cache-dagger.outputs.cache-hit == 'true'
27
+ run: docker load -i /tmp/dagger-engine.tar
28
+
29
+ - name: Run CI pipeline
30
+ uses: dagger/dagger-for-github@v8.4.0
31
+ with:
32
+ version: "0.20.3"
33
+ call: check --source=.
34
+
35
+ - name: Save Dagger engine image
36
+ if: steps.cache-dagger.outputs.cache-hit != 'true'
37
+ run: docker save registry.dagger.io/engine:v0.20.3 -o /tmp/dagger-engine.tar
@@ -0,0 +1,35 @@
1
+ name: Documentation
2
+ on:
3
+ push:
4
+ branches:
5
+ - master
6
+ - main
7
+ concurrency:
8
+ group: "pages"
9
+ cancel-in-progress: false
10
+ env:
11
+ FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
12
+ permissions:
13
+ contents: read
14
+ pages: write
15
+ id-token: write
16
+ jobs:
17
+ deploy:
18
+ environment:
19
+ name: github-pages
20
+ url: ${{ steps.deployment.outputs.page_url }}
21
+ runs-on: ubuntu-latest
22
+ steps:
23
+ - uses: actions/configure-pages@v5
24
+ - uses: actions/checkout@v6
25
+ - uses: actions/setup-python@v6
26
+ with:
27
+ python-version: 3.14
28
+ - run: pip install zensical "mkdocstrings[python]"
29
+ - run: cp README.md docs/index.md
30
+ - run: zensical build --clean
31
+ - uses: actions/upload-pages-artifact@v4
32
+ with:
33
+ path: site
34
+ - uses: actions/deploy-pages@v4
35
+ id: deployment
@@ -0,0 +1,24 @@
1
+ # Enforce Conventional Commits format on pull request titles.
2
+ #
3
+ # Uses pull_request_target so the workflow runs with repo permissions even for
4
+ # forks. Only pull-requests: read is needed — no write access required.
5
+
6
+ name: PR title
7
+
8
+ on:
9
+ pull_request_target:
10
+ types: [opened, reopened, edited, synchronize]
11
+
12
+ jobs:
13
+ lint-pr-title:
14
+ name: Lint PR title
15
+ runs-on: ubuntu-latest
16
+ if: "!startsWith(github.event.pull_request.head.ref, 'chore/version-bump-')"
17
+ permissions:
18
+ pull-requests: read
19
+
20
+ steps:
21
+ - name: Validate conventional commit format
22
+ uses: amannn/action-semantic-pull-request@48f256284bd46cdaab1048c3721360e808335d50 # v6.1.1
23
+ env:
24
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -0,0 +1,68 @@
1
+ # Publish sheridan-diffract to PyPI on version tag push.
2
+ #
3
+ # Prerequisites — register this repo as a trusted publisher on pypi.org:
4
+ # pypi.org → Account Settings → Publishing → Add a new pending publisher
5
+ # Owner: andrewasheridan
6
+ # Repository: diffract
7
+ # Workflow: publish.yaml
8
+ # Environment: pypi (must match the `environment:` name on the publish job)
9
+ #
10
+ # Once registered, PyPI trusts GitHub's OIDC token — no API token secrets needed.
11
+ #
12
+ # Triggered by tag-on-merge.yaml pushing a v* tag. Using push.tags (not
13
+ # workflow_call) so GitHub issues a first-party OIDC token — workflow_call
14
+ # produces a reusable-workflow token that PyPI's trusted publisher rejects.
15
+
16
+ name: Publish
17
+
18
+ on:
19
+ push:
20
+ tags:
21
+ - "v*"
22
+ workflow_dispatch: # Allows manual re-runs via gh CLI or UI
23
+
24
+ jobs:
25
+ build:
26
+ name: Build distribution
27
+ runs-on: ubuntu-latest
28
+ permissions:
29
+ contents: read
30
+
31
+ steps:
32
+ - uses: actions/checkout@v6
33
+
34
+ - uses: actions/setup-python@v6
35
+ with:
36
+ python-version: "3.14"
37
+
38
+ - uses: astral-sh/setup-uv@v7
39
+
40
+ - name: Build sdist and wheel
41
+ run: uv build
42
+
43
+ - name: Upload dist artifacts
44
+ uses: actions/upload-artifact@v4
45
+ with:
46
+ name: dist
47
+ path: dist/
48
+
49
+ publish:
50
+ name: Publish to PyPI
51
+ needs: build
52
+ runs-on: ubuntu-latest
53
+ environment:
54
+ name: pypi
55
+ url: https://pypi.org/p/sheridan-diffract
56
+ permissions:
57
+ contents: read
58
+ id-token: write # required for OIDC trusted publishing
59
+
60
+ steps:
61
+ - name: Download dist artifacts
62
+ uses: actions/download-artifact@v8
63
+ with:
64
+ name: dist
65
+ path: dist/
66
+
67
+ - name: Publish to PyPI
68
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,51 @@
1
+ # When a version-bump PR is squash-merged into main, create an annotated tag on
2
+ # the merge commit. publish.yaml listens for the resulting tag push and handles
3
+ # the PyPI release — workflow_call cannot be used because it produces a
4
+ # reusable-workflow OIDC token that PyPI's trusted publisher rejects.
5
+ #
6
+ # RELEASE_TOKEN is passed to actions/checkout so that checkout's git credential
7
+ # helper is wired to the PAT rather than GITHUB_TOKEN. Without this, the helper
8
+ # installed by checkout overrides any credential embedded in the remote URL, the
9
+ # push is attributed to GITHUB_TOKEN, and GitHub suppresses the push.tags event
10
+ # that publish.yaml depends on.
11
+
12
+ name: Tag on merge
13
+
14
+ on:
15
+ pull_request:
16
+ types: [closed]
17
+ branches: [main]
18
+
19
+ jobs:
20
+ tag-merge-commit:
21
+ name: Tag merge commit
22
+ runs-on: ubuntu-latest
23
+ if: github.event.pull_request.merged == true && startsWith(github.event.pull_request.head.ref, 'chore/version-bump-')
24
+ permissions:
25
+ contents: write
26
+
27
+ steps:
28
+ - name: Checkout
29
+ uses: actions/checkout@v6
30
+ with:
31
+ fetch-depth: 0
32
+ token: ${{ secrets.RELEASE_TOKEN }} # wire credential helper to PAT so tag push triggers publish.yaml
33
+
34
+ - name: Configure git identity
35
+ run: |
36
+ git config user.name "github-actions[bot]"
37
+ git config user.email "github-actions[bot]@users.noreply.github.com"
38
+
39
+ - name: Extract version from branch name
40
+ id: extract
41
+ run: |
42
+ BRANCH="${{ github.event.pull_request.head.ref }}"
43
+ VERSION="${BRANCH#chore/version-bump-}"
44
+ echo "version=$VERSION" >> "$GITHUB_OUTPUT"
45
+
46
+ - name: Create and push tag
47
+ env:
48
+ VERSION: ${{ steps.extract.outputs.version }}
49
+ run: |
50
+ git tag -a "v$VERSION" "${{ github.event.pull_request.merge_commit_sha }}" -m "Version $VERSION"
51
+ git push origin "v$VERSION"
@@ -0,0 +1,41 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.pyo
5
+ *.pyd
6
+ *.so
7
+
8
+ # Distribution / packaging
9
+ dist/
10
+ build/
11
+ *.egg-info/
12
+ *.egg
13
+
14
+ # Virtual environments
15
+ .venv/
16
+ venv/
17
+
18
+ # Testing & coverage
19
+ .coverage
20
+ .coverage.*
21
+ htmlcov/
22
+ .pytest_cache/
23
+
24
+ # Type checking
25
+ .mypy_cache/
26
+
27
+ # uv
28
+ uv.lock
29
+
30
+ # Editors
31
+ .idea/
32
+ .vscode/
33
+ *.swp
34
+ *.swo
35
+
36
+ # macOS
37
+ .DS_Store
38
+
39
+ # Claude
40
+ .claude/worktrees/
41
+ .claude/settings.local.json
@@ -0,0 +1,20 @@
1
+ repos:
2
+ - repo: https://github.com/astral-sh/ruff-pre-commit
3
+ rev: v0.15.7
4
+ hooks:
5
+ - id: ruff-check
6
+ args: [--fix]
7
+ - id: ruff-format
8
+
9
+ - repo: https://github.com/pre-commit/mirrors-mypy
10
+ rev: v1.19.1
11
+ hooks:
12
+ - id: mypy
13
+ args: [--strict]
14
+ files: ^src/
15
+ additional_dependencies: [gitpython, sheridan-iceberg]
16
+
17
+ - repo: https://github.com/andrewasheridan/diffract
18
+ rev: v1.0.1 # diffract
19
+ hooks:
20
+ - id: diffract-validate
@@ -0,0 +1,23 @@
1
+ # Pre-commit hook definitions for sheridan-diffract.
2
+ #
3
+ # To use these hooks in another project, add the following to your
4
+ # .pre-commit-config.yaml:
5
+ #
6
+ # repos:
7
+ # - repo: https://github.com/sheridan/diffract
8
+ # rev: v<VERSION>
9
+ # hooks:
10
+ # - id: diffract-validate
11
+
12
+ - id: diffract-validate
13
+ name: diffract (validate commit message type)
14
+ description: >
15
+ Validates that the conventional commit type in the commit message matches
16
+ the API change diffract detects. Exits 0 on match or unrecognised type;
17
+ exits 1 on mismatch. Non-conventional commits (docs:, chore:, etc.) are
18
+ never blocked. Configure the source directory via diffract.toml or
19
+ pyproject.toml [tool.diffract] rather than hook args.
20
+ language: python
21
+ entry: diffract --validate-msg-file
22
+ pass_filenames: true
23
+ stages: [commit-msg]
@@ -0,0 +1,12 @@
1
+ # Changelog
2
+
3
+ All notable changes to `sheridan-diffract` will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## ['0.1.0'] - Initial release
9
+
10
+ ### Added
11
+
12
+ - Initial project structure
@@ -0,0 +1,146 @@
1
+ # CLAUDE.md — sheridan-diffract
2
+
3
+ ## Project overview
4
+
5
+ `sheridan-diffract` detects how a Python package's public API changed between two git commits and classifies that change as a conventional commit type. It has no AST logic of its own — it delegates API surface extraction entirely to `sheridan-iceberg`.
6
+
7
+ ## Commands
8
+
9
+ ```bash
10
+ task check # full gate: lint, format, typecheck, tests (run before every commit)
11
+ task lint # ruff check --fix
12
+ task lint:check # ruff check (read-only, used in CI)
13
+ task format # ruff format
14
+ task format:check # ruff format --check (read-only, used in CI)
15
+ task typecheck # mypy --strict src/
16
+ task test # pytest --cov (coverage ≥ 90% required)
17
+ task install # uv sync --all-extras --dev
18
+ ```
19
+
20
+ ## Architecture
21
+
22
+ ```
23
+ src/sheridan/diffract/
24
+ ├── __init__.py # Public re-exports; __version__ from importlib.metadata
25
+ ├── enums.py # CommitType, ChangeKind (StrEnum)
26
+ ├── exceptions.py # DiffractError → GitError / SurfaceError
27
+ ├── models.py # NameChange, ApiDiff, DiffractResult (frozen dataclasses)
28
+ ├── differ.py # diff_surfaces() — set-diff two iceberg API surfaces
29
+ ├── classifier.py # classify() — maps ApiDiff → CommitType + summary
30
+ ├── git_utils.py # get_repo(), get_api_at_ref(), has_python_changes(), get_api_at_index(), has_python_changes_index()
31
+ ├── checker.py # check() — orchestrates git → iceberg → diff → classify; check_staged() — HEAD vs index
32
+ ├── config.py # load_config() — DiffractConfig from diffract.toml / pyproject.toml
33
+ └── cli.py # argparse CLI: `diffract [BASE] [HEAD] [--src] [--json] [--exit-code] [--validate-msg-file]`
34
+ ```
35
+
36
+ ### Key dependency: sheridan-iceberg
37
+
38
+ ```python
39
+ from sheridan.iceberg import get_public_api
40
+ # get_public_api(path: Path | str) -> dict[str, list[str]]
41
+ # Returns: {"module.qualified.name": ["PublicName1", "ClassFoo", ...], ...}
42
+ ```
43
+
44
+ `diffract` calls `get_public_api` twice (once per ref), then diffs the two dicts.
45
+
46
+ ### Classification rules (in priority order)
47
+
48
+ 1. Any public name **removed** → `feat!` (breaking)
49
+ 2. Any public name **added** (no removals) → `feat`
50
+ 3. No name changes, but `.py` files changed → `refactor`
51
+ 4. No changes detected → `fix`
52
+
53
+ ## Code conventions
54
+
55
+ - **Python ≥ 3.14** — use modern syntax freely (`X | Y`, `match`, etc.)
56
+ - **`from __future__ import annotations`** at top of every file
57
+ - **`__all__`** declared in every module
58
+ - **Frozen dataclasses** for all result/model types
59
+ - **`StrEnum`** for enumerations
60
+ - **Google-style docstrings** on all public functions, classes, and methods
61
+ - **Line length**: 120 characters (ruff enforced)
62
+ - **Import order**: stdlib → third-party → local (isort via ruff)
63
+
64
+ ## Testing conventions
65
+
66
+ - **pytest** only — never `unittest.TestCase`
67
+ - **pytest-mock** for patching: use the `mocker` fixture, not `unittest.mock.patch`
68
+ - **`monkeypatch`** for environment/`sys.argv` modification
69
+ - **No `MagicMock` as fixture arguments** — construct them inline with `MagicMock()`
70
+ - Shared fixtures live in `tests/conftest.py`
71
+ - One test file per source module: `test_<module>.py`
72
+ - Coverage threshold: **≥ 90%** (enforced by pytest-cov)
73
+
74
+ ## Dependencies
75
+
76
+ - **Runtime**: `sheridan-iceberg`, `gitpython`
77
+ - **Dev**: `ruff`, `mypy`, `pytest`, `pytest-cov`, `pytest-mock`, `commitizen`, `pre-commit`, `bandit`, `zensical`, `mkdocstrings`
78
+
79
+ ### Adding dependencies
80
+
81
+ Always run the dependency auditor before adding a new runtime dependency. Context matters: the automated auditor rejected `gitpython` on security grounds, but human review approved it (see ADR-002) because the CVEs are patched, inputs are controlled, and it handles git hook environment variables correctly.
82
+
83
+ ## Commit format (Conventional Commits via commitizen)
84
+
85
+ ```
86
+ feat: add new public API
87
+ feat!: remove or rename a public API (breaking)
88
+ fix: correct a bug with no API change
89
+ refactor: internal restructuring, no API change
90
+ docs: documentation only
91
+ test: add or update tests
92
+ chore: maintenance (deps, config, tooling)
93
+ ```
94
+
95
+ ## GitHub Actions workflows
96
+
97
+ Workflow files live in `.github/workflows/` and use the **`.yaml` extension** (not `.yml`).
98
+
99
+ ## Architecture Decision Records
100
+
101
+ Significant decisions are documented in `docs/decisions/`. Use the `adr` skill to add new ones.
102
+
103
+ ## Configuration
104
+
105
+ diffract reads `src` from the first config file found in the repo root:
106
+
107
+ | File | Key |
108
+ |---|---|
109
+ | `diffract.toml` | `src = "python/src"` |
110
+ | `pyproject.toml` | `[tool.diffract]` → `src = "python/src"` |
111
+
112
+ If neither file is present, `--src` CLI flag is used, falling back to `src/`.
113
+
114
+ Priority: `--src` flag > `diffract.toml` > `pyproject.toml` > default (`src/`)
115
+
116
+ ## CLI flags
117
+
118
+ ```
119
+ diffract [BASE] [HEAD] # defaults: HEAD~1, HEAD
120
+ --src PATH # source directory (default: src); use --src . for flat layouts
121
+ --json # output JSON instead of human-readable text
122
+ --exit-code # exit 1 (breaking), 2 (any API change), 0 (no change)
123
+ --validate-msg-file MSGFILE # validate conventional commit type against detected API change
124
+ ```
125
+
126
+ When `--validate-msg-file` is used **without** explicit `BASE`/`HEAD` refs (the standard commit-msg hook invocation), diffract automatically compares `HEAD` against the **git staging area** rather than `HEAD~1 → HEAD`. This ensures the hook validates what is actually being committed, not what was committed last. Pass explicit refs to override (e.g. for CI use with `diffract base.sha head.sha --validate-msg-file …`).
127
+
128
+ ## pre-commit hooks
129
+
130
+ One hook is exported from `.pre-commit-hooks.yaml`:
131
+
132
+ | Hook id | Stage | Purpose |
133
+ |---|---|---|
134
+ | `diffract-validate` | commit-msg | Fails if the commit message type doesn't match the detected API change |
135
+
136
+ The hook reads `src` from `diffract.toml` or `pyproject.toml` automatically:
137
+
138
+ ```yaml
139
+ - id: diffract-validate
140
+ ```
141
+
142
+ ## What diffract does NOT do
143
+
144
+ - No AST logic — all API surface extraction is delegated to `sheridan-iceberg`
145
+ - No signature diffing — only name-level additions/removals are detected
146
+ - No automatic commit rewriting — diffract only reports and optionally fails CI
@@ -0,0 +1,63 @@
1
+ # Contributing to sheridan-diffract
2
+
3
+ Thank you for considering a contribution!
4
+
5
+ ## Setup
6
+
7
+ ```bash
8
+ task install
9
+ pre-commit install
10
+ ```
11
+
12
+ ## Workflow
13
+
14
+ 1. Create a feature branch from `main`
15
+ 2. Write your code and tests (coverage must remain ≥ 90%)
16
+ 3. Run `task check` — all gates must pass
17
+ 4. Commit using [Conventional Commits](https://www.conventionalcommits.org/) format
18
+ 5. Open a pull request
19
+
20
+ ## Running checks
21
+
22
+ ```bash
23
+ task check # all gates: lint, format, typecheck, test
24
+ task lint # ruff check + autofix
25
+ task format # ruff format
26
+ task typecheck # mypy --strict
27
+ task test # pytest with coverage
28
+ ```
29
+
30
+ ## Commit format
31
+
32
+ This project uses [Commitizen](https://commitizen-tools.github.io/commitizen/) with Conventional Commits:
33
+
34
+ ```
35
+ feat: add new feature
36
+ fix: correct a bug
37
+ docs: update documentation
38
+ chore: maintenance tasks
39
+ refactor: code restructuring without behavior change
40
+ test: add or update tests
41
+ ```
42
+
43
+ ## Adding dependencies
44
+
45
+ - Propose new runtime dependencies in a PR with a clear justification
46
+ - Keep the runtime dependency list minimal
47
+ - Dev dependencies go under `[project.optional-dependencies] dev`
48
+
49
+ ## Architecture Decision Records
50
+
51
+ Significant decisions are recorded as ADRs in `docs/decisions/`.
52
+ Use the `docs/decisions/` directory to document any non-obvious choices.
53
+
54
+ ## CI pipeline
55
+
56
+ The full CI pipeline runs via Dagger:
57
+
58
+ ```bash
59
+ task ci-init # first time only
60
+ task ci # run all gates in containers
61
+ ```
62
+
63
+ Individual gates: `task lint:check`, `task format:check`, `task typecheck`, `task test`.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 sheridan
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.