commitcraft-cli 0.1.0__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 (102) hide show
  1. commitcraft_cli-0.1.0/.github/workflows/ci.yml +31 -0
  2. commitcraft_cli-0.1.0/.gitignore +14 -0
  3. commitcraft_cli-0.1.0/.superpowers/sdd/.gitignore +1 -0
  4. commitcraft_cli-0.1.0/.superpowers/sdd/progress.md +39 -0
  5. commitcraft_cli-0.1.0/.superpowers/sdd/review-1d0a3e5..59992b8.diff +141 -0
  6. commitcraft_cli-0.1.0/.superpowers/sdd/review-2ecf6ef..ba368ce.diff +211 -0
  7. commitcraft_cli-0.1.0/.superpowers/sdd/review-305ed0e..e0ac712.diff +8 -0
  8. commitcraft_cli-0.1.0/.superpowers/sdd/review-30a7e6e..f4f700d.diff +172 -0
  9. commitcraft_cli-0.1.0/.superpowers/sdd/review-4b825dc..4c64c74.diff +5338 -0
  10. commitcraft_cli-0.1.0/.superpowers/sdd/review-4b825dc..e0ac712.diff +148 -0
  11. commitcraft_cli-0.1.0/.superpowers/sdd/review-4c64c74..c18dbf7.diff +159 -0
  12. commitcraft_cli-0.1.0/.superpowers/sdd/review-59992b8..ec92295.diff +217 -0
  13. commitcraft_cli-0.1.0/.superpowers/sdd/review-5ebae75..791fe7b.diff +187 -0
  14. commitcraft_cli-0.1.0/.superpowers/sdd/review-791fe7b..02d0776.diff +184 -0
  15. commitcraft_cli-0.1.0/.superpowers/sdd/review-791fe7b..2ecf6ef.diff +185 -0
  16. commitcraft_cli-0.1.0/.superpowers/sdd/review-7c6089c..fa0a2f9.diff +76 -0
  17. commitcraft_cli-0.1.0/.superpowers/sdd/review-805556e..7c6089c.diff +69 -0
  18. commitcraft_cli-0.1.0/.superpowers/sdd/review-98fd4b8..4c64c74.diff +4134 -0
  19. commitcraft_cli-0.1.0/.superpowers/sdd/review-ba368ce..1d0a3e5.diff +74 -0
  20. commitcraft_cli-0.1.0/.superpowers/sdd/review-e0ac712..5ebae75.diff +301 -0
  21. commitcraft_cli-0.1.0/.superpowers/sdd/review-ec92295..6b23b58.diff +231 -0
  22. commitcraft_cli-0.1.0/.superpowers/sdd/review-f4f700d..1d8ef6a.diff +292 -0
  23. commitcraft_cli-0.1.0/.superpowers/sdd/review-f4f700d..805556e.diff +343 -0
  24. commitcraft_cli-0.1.0/.superpowers/sdd/review-fa0a2f9..98fd4b8.diff +107 -0
  25. commitcraft_cli-0.1.0/.superpowers/sdd/task-1-brief.md +153 -0
  26. commitcraft_cli-0.1.0/.superpowers/sdd/task-1-report.md +91 -0
  27. commitcraft_cli-0.1.0/.superpowers/sdd/task-10-brief.md +170 -0
  28. commitcraft_cli-0.1.0/.superpowers/sdd/task-10-report.md +68 -0
  29. commitcraft_cli-0.1.0/.superpowers/sdd/task-11-brief.md +270 -0
  30. commitcraft_cli-0.1.0/.superpowers/sdd/task-11-report.md +58 -0
  31. commitcraft_cli-0.1.0/.superpowers/sdd/task-12-brief.md +57 -0
  32. commitcraft_cli-0.1.0/.superpowers/sdd/task-12-report.md +33 -0
  33. commitcraft_cli-0.1.0/.superpowers/sdd/task-13-brief.md +66 -0
  34. commitcraft_cli-0.1.0/.superpowers/sdd/task-14-brief.md +88 -0
  35. commitcraft_cli-0.1.0/.superpowers/sdd/task-14-report.md +46 -0
  36. commitcraft_cli-0.1.0/.superpowers/sdd/task-15-brief.md +378 -0
  37. commitcraft_cli-0.1.0/.superpowers/sdd/task-15-report.md +41 -0
  38. commitcraft_cli-0.1.0/.superpowers/sdd/task-2-brief.md +314 -0
  39. commitcraft_cli-0.1.0/.superpowers/sdd/task-2-report.md +82 -0
  40. commitcraft_cli-0.1.0/.superpowers/sdd/task-3-brief.md +203 -0
  41. commitcraft_cli-0.1.0/.superpowers/sdd/task-3-report.md +75 -0
  42. commitcraft_cli-0.1.0/.superpowers/sdd/task-4-brief.md +211 -0
  43. commitcraft_cli-0.1.0/.superpowers/sdd/task-4-report.md +72 -0
  44. commitcraft_cli-0.1.0/.superpowers/sdd/task-5-brief.md +246 -0
  45. commitcraft_cli-0.1.0/.superpowers/sdd/task-5-report.md +88 -0
  46. commitcraft_cli-0.1.0/.superpowers/sdd/task-6-brief.md +96 -0
  47. commitcraft_cli-0.1.0/.superpowers/sdd/task-6-report.md +66 -0
  48. commitcraft_cli-0.1.0/.superpowers/sdd/task-7-brief.md +165 -0
  49. commitcraft_cli-0.1.0/.superpowers/sdd/task-7-report.md +62 -0
  50. commitcraft_cli-0.1.0/.superpowers/sdd/task-8-brief.md +208 -0
  51. commitcraft_cli-0.1.0/.superpowers/sdd/task-8-report.md +116 -0
  52. commitcraft_cli-0.1.0/.superpowers/sdd/task-9-brief.md +220 -0
  53. commitcraft_cli-0.1.0/.superpowers/sdd/task-9-report.md +23 -0
  54. commitcraft_cli-0.1.0/CONTRIBUTING.md +40 -0
  55. commitcraft_cli-0.1.0/LICENSE +21 -0
  56. commitcraft_cli-0.1.0/PKG-INFO +113 -0
  57. commitcraft_cli-0.1.0/README.md +92 -0
  58. commitcraft_cli-0.1.0/commitcraft/__init__.py +1 -0
  59. commitcraft_cli-0.1.0/commitcraft/analysis/__init__.py +0 -0
  60. commitcraft_cli-0.1.0/commitcraft/analysis/classifier.py +57 -0
  61. commitcraft_cli-0.1.0/commitcraft/analysis/filters.py +67 -0
  62. commitcraft_cli-0.1.0/commitcraft/analysis/rule_engine.py +101 -0
  63. commitcraft_cli-0.1.0/commitcraft/cli.py +172 -0
  64. commitcraft_cli-0.1.0/commitcraft/config/__init__.py +0 -0
  65. commitcraft_cli-0.1.0/commitcraft/config/models.py +25 -0
  66. commitcraft_cli-0.1.0/commitcraft/config/store.py +48 -0
  67. commitcraft_cli-0.1.0/commitcraft/config/wizard.py +44 -0
  68. commitcraft_cli-0.1.0/commitcraft/context/__init__.py +0 -0
  69. commitcraft_cli-0.1.0/commitcraft/context/builder.py +59 -0
  70. commitcraft_cli-0.1.0/commitcraft/generators/__init__.py +0 -0
  71. commitcraft_cli-0.1.0/commitcraft/generators/commit.py +33 -0
  72. commitcraft_cli-0.1.0/commitcraft/generators/pr.py +12 -0
  73. commitcraft_cli-0.1.0/commitcraft/generators/release_notes.py +26 -0
  74. commitcraft_cli-0.1.0/commitcraft/git/__init__.py +0 -0
  75. commitcraft_cli-0.1.0/commitcraft/git/diff_parser.py +132 -0
  76. commitcraft_cli-0.1.0/commitcraft/git/history.py +73 -0
  77. commitcraft_cli-0.1.0/commitcraft/providers/__init__.py +0 -0
  78. commitcraft_cli-0.1.0/commitcraft/providers/anthropic_provider.py +44 -0
  79. commitcraft_cli-0.1.0/commitcraft/providers/base.py +19 -0
  80. commitcraft_cli-0.1.0/commitcraft/providers/gemini_provider.py +40 -0
  81. commitcraft_cli-0.1.0/commitcraft/providers/ollama_provider.py +61 -0
  82. commitcraft_cli-0.1.0/commitcraft/providers/openai_provider.py +43 -0
  83. commitcraft_cli-0.1.0/commitcraft/utils/__init__.py +0 -0
  84. commitcraft_cli-0.1.0/commitcraft/utils/token_estimator.py +3 -0
  85. commitcraft_cli-0.1.0/docs/adding_a_provider.md +83 -0
  86. commitcraft_cli-0.1.0/docs/superpowers/plans/2026-06-30-commitcraft.md +2934 -0
  87. commitcraft_cli-0.1.0/pyproject.toml +44 -0
  88. commitcraft_cli-0.1.0/tests/__init__.py +0 -0
  89. commitcraft_cli-0.1.0/tests/fixtures/complex_multi_file.diff +29 -0
  90. commitcraft_cli-0.1.0/tests/fixtures/lockfile_only.diff +12 -0
  91. commitcraft_cli-0.1.0/tests/fixtures/simple_readme.diff +10 -0
  92. commitcraft_cli-0.1.0/tests/test_classifier.py +104 -0
  93. commitcraft_cli-0.1.0/tests/test_commit_generator.py +87 -0
  94. commitcraft_cli-0.1.0/tests/test_config_store.py +56 -0
  95. commitcraft_cli-0.1.0/tests/test_context_builder.py +60 -0
  96. commitcraft_cli-0.1.0/tests/test_diff_parser.py +63 -0
  97. commitcraft_cli-0.1.0/tests/test_filters.py +86 -0
  98. commitcraft_cli-0.1.0/tests/test_generators.py +81 -0
  99. commitcraft_cli-0.1.0/tests/test_history.py +103 -0
  100. commitcraft_cli-0.1.0/tests/test_providers.py +107 -0
  101. commitcraft_cli-0.1.0/tests/test_providers_extra.py +149 -0
  102. commitcraft_cli-0.1.0/tests/test_rule_engine.py +98 -0
@@ -0,0 +1,31 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ jobs:
10
+ test:
11
+ runs-on: ubuntu-latest
12
+ strategy:
13
+ matrix:
14
+ python-version: ["3.11", "3.12"]
15
+
16
+ steps:
17
+ - uses: actions/checkout@v4
18
+
19
+ - name: Set up Python ${{ matrix.python-version }}
20
+ uses: actions/setup-python@v5
21
+ with:
22
+ python-version: ${{ matrix.python-version }}
23
+
24
+ - name: Install dependencies
25
+ run: pip install -e ".[dev]"
26
+
27
+ - name: Lint
28
+ run: ruff check commitcraft/ tests/
29
+
30
+ - name: Test
31
+ run: pytest tests/ -v --cov=commitcraft --cov-report=term-missing
@@ -0,0 +1,14 @@
1
+ __pycache__/
2
+ *.pyc
3
+ *.pyo
4
+ .pytest_cache/
5
+ .ruff_cache/
6
+ *.egg-info/
7
+ dist/
8
+ build/
9
+ .venv/
10
+ venv/
11
+ .env
12
+ *.log
13
+ .coverage
14
+ htmlcov/
@@ -0,0 +1,39 @@
1
+ # commitcraft — SDD Progress Ledger
2
+
3
+ Plan: docs/superpowers/plans/2026-06-30-commitcraft.md
4
+
5
+ ## Tasks
6
+
7
+ - [x] Task 1: Project Scaffolding
8
+ - [x] Task 2: Git Diff Parser
9
+ - [x] Task 3: File Filters
10
+ - [x] Task 4: Complexity Classifier
11
+ - [x] Task 5: Rule Engine
12
+ - [x] Task 6: Provider ABC + Config Models
13
+ - [x] Task 7: Ollama Provider
14
+ - [x] Task 8: Anthropic, OpenAI, Gemini Providers
15
+ - [x] Task 9: Config Store + Provider Factory + Setup Wizard
16
+ - [x] Task 10: Context Builder
17
+ - [x] Task 11: Commit Generator + Full `commitcraft commit` Command
18
+ - [x] Task 12: PR Description Generator
19
+ - [x] Task 13: Release Notes Generator
20
+ - [x] Task 14: History Analyzer
21
+ - [x] Task 15: README, CONTRIBUTING, Provider Docs, CI
22
+
23
+ ## Completed Tasks
24
+
25
+ Task 1: complete (commits 4b825dc..e0ac712, review clean)
26
+ Task 2: complete (commits e0ac712..5ebae75, review clean; minor: inline regex comments, import inside fn body)
27
+ Task 3: complete (commits 5ebae75..791fe7b, review clean)
28
+ Task 4: complete (commits 791fe7b..2ecf6ef, fix round: added type annotations to helpers)
29
+ Task 5: complete (commits 2ecf6ef..ba368ce, review clean; Phase 1 done: 39/39 tests)
30
+ Task 6: complete (commits ba368ce..1d0a3e5, review clean)
31
+ Task 7: complete (commits 1d0a3e5..59992b8, review clean)
32
+ Task 8: complete (commits 59992b8..ec92295, review clean)
33
+ Task 9: complete (commits ec92295..30a7e6e, fix: removed unused Confirm import; 54/54 tests)
34
+ Task 10: complete (commits 30a7e6e..f4f700d, review clean)
35
+ Task 11: complete (commits f4f700d..805556e, fix round: removed dead classify block + pycache; 65/65)
36
+ Task 12: complete (commits 805556e..7c6089c, review clean)
37
+ Task 13: complete (commits 7c6089c..fa0a2f9, review clean)
38
+ Task 14: complete (commits fa0a2f9..98fd4b8, review clean)
39
+ Task 15: complete (commits 98fd4b8..4c64c74, ruff fixed 55 pre-existing issues; review clean)
@@ -0,0 +1,141 @@
1
+ # Review package: 1d0a3e5..HEAD
2
+
3
+ ## Commits
4
+ 59992b8 feat: implement Ollama provider with commit/PR/release-notes generation
5
+
6
+ ## Files changed
7
+ commitcraft/providers/ollama_provider.py | 60 ++++++++++++++++++++++++++++++++
8
+ tests/test_providers.py | 58 ++++++++++++++++++++++++++++++
9
+ 2 files changed, 118 insertions(+)
10
+
11
+ ## Diff
12
+ diff --git a/commitcraft/providers/ollama_provider.py b/commitcraft/providers/ollama_provider.py
13
+ new file mode 100644
14
+ index 0000000..3e69a91
15
+ --- /dev/null
16
+ +++ b/commitcraft/providers/ollama_provider.py
17
+ @@ -0,0 +1,60 @@
18
+ +import httpx
19
+ +from commitcraft.config.models import CommitcraftConfig
20
+ +from commitcraft.providers.base import Provider
21
+ +
22
+ +_COMMIT_SYSTEM = (
23
+ + "You are an expert at writing conventional commit messages. "
24
+ + "Given a summary of code changes, write a single conventional commit message. "
25
+ + "Format: <type>(<optional scope>): <description>. "
26
+ + "Types: feat, fix, docs, style, refactor, test, chore, perf, ci, build. "
27
+ + "Output ONLY the commit message — no explanation, no markdown, no quotes."
28
+ +)
29
+ +
30
+ +_PR_SYSTEM = (
31
+ + "You are an expert at writing GitHub pull request descriptions. "
32
+ + "Given a summary of code changes, write a clear PR description in markdown with: "
33
+ + "a '## Summary' section (3-5 bullet points) and a '## Changes' section. "
34
+ + "Be concise and factual."
35
+ +)
36
+ +
37
+ +_RELEASE_SYSTEM = (
38
+ + "You are an expert at writing software release notes. "
39
+ + "Given a list of commits or change summaries, produce structured release notes "
40
+ + "grouped by: Features, Bug Fixes, Documentation, Other. Use markdown."
41
+ +)
42
+ +
43
+ +
44
+ +class OllamaProvider(Provider):
45
+ + def __init__(self, config: CommitcraftConfig) -> None:
46
+ + self._config = config
47
+ + self._base_url = config.ollama_base_url
48
+ + self._model = config.ollama_model
49
+ +
50
+ + @property
51
+ + def name(self) -> str:
52
+ + return "ollama"
53
+ +
54
+ + def _generate(self, system: str, prompt: str) -> str:
55
+ + response = httpx.post(
56
+ + f"{self._base_url}/api/generate",
57
+ + json={"model": self._model, "system": system, "prompt": prompt, "stream": False},
58
+ + timeout=60.0,
59
+ + )
60
+ + response.raise_for_status()
61
+ + return response.json()["response"].strip()
62
+ +
63
+ + def generate_commit_message(self, context: str) -> str:
64
+ + return self._generate(_COMMIT_SYSTEM, context)
65
+ +
66
+ + def generate_pr_description(self, context: str) -> str:
67
+ + return self._generate(_PR_SYSTEM, context)
68
+ +
69
+ + def generate_release_notes(self, context: str) -> str:
70
+ + return self._generate(_RELEASE_SYSTEM, context)
71
+ +
72
+ + def health_check(self) -> bool:
73
+ + try:
74
+ + httpx.get(f"{self._base_url}/api/tags", timeout=5.0).raise_for_status()
75
+ + return True
76
+ + except Exception:
77
+ + return False
78
+ diff --git a/tests/test_providers.py b/tests/test_providers.py
79
+ new file mode 100644
80
+ index 0000000..05045e6
81
+ --- /dev/null
82
+ +++ b/tests/test_providers.py
83
+ @@ -0,0 +1,58 @@
84
+ +import pytest
85
+ +from unittest.mock import patch, MagicMock
86
+ +from commitcraft.config.models import CommitcraftConfig, ProviderName
87
+ +from commitcraft.providers.ollama_provider import OllamaProvider
88
+ +
89
+ +
90
+ +@pytest.fixture
91
+ +def config():
92
+ + return CommitcraftConfig(
93
+ + provider=ProviderName.OLLAMA,
94
+ + ollama_model="llama3.2",
95
+ + ollama_base_url="http://localhost:11434",
96
+ + )
97
+ +
98
+ +
99
+ +def test_ollama_provider_name(config):
100
+ + p = OllamaProvider(config)
101
+ + assert p.name == "ollama"
102
+ +
103
+ +
104
+ +def test_ollama_generate_commit_message(config):
105
+ + mock_response = MagicMock()
106
+ + mock_response.json.return_value = {"response": "feat: add user authentication"}
107
+ + mock_response.raise_for_status = MagicMock()
108
+ +
109
+ + with patch("httpx.post", return_value=mock_response) as mock_post:
110
+ + p = OllamaProvider(config)
111
+ + result = p.generate_commit_message("context: added login function")
112
+ + assert result == "feat: add user authentication"
113
+ + mock_post.assert_called_once()
114
+ + call_args = mock_post.call_args
115
+ + assert "llama3.2" in str(call_args)
116
+ +
117
+ +
118
+ +def test_ollama_generate_pr_description(config):
119
+ + mock_response = MagicMock()
120
+ + mock_response.json.return_value = {"response": "## Summary\n- Added auth"}
121
+ + mock_response.raise_for_status = MagicMock()
122
+ +
123
+ + with patch("httpx.post", return_value=mock_response):
124
+ + p = OllamaProvider(config)
125
+ + result = p.generate_pr_description("branch diff context")
126
+ + assert "Summary" in result
127
+ +
128
+ +
129
+ +def test_ollama_health_check_success(config):
130
+ + mock_response = MagicMock()
131
+ + mock_response.raise_for_status = MagicMock()
132
+ +
133
+ + with patch("httpx.get", return_value=mock_response):
134
+ + p = OllamaProvider(config)
135
+ + assert p.health_check() is True
136
+ +
137
+ +
138
+ +def test_ollama_health_check_failure(config):
139
+ + with patch("httpx.get", side_effect=Exception("Connection refused")):
140
+ + p = OllamaProvider(config)
141
+ + assert p.health_check() is False
@@ -0,0 +1,211 @@
1
+ # Review package: 2ecf6ef..HEAD
2
+
3
+ ## Commits
4
+ ba368ce feat: implement rule engine for zero-AI commit message generation
5
+
6
+ ## Files changed
7
+ commitcraft/analysis/rule_engine.py | 91 ++++++++++++++++++++++++++++++++++
8
+ tests/test_rule_engine.py | 97 +++++++++++++++++++++++++++++++++++++
9
+ 2 files changed, 188 insertions(+)
10
+
11
+ ## Diff
12
+ diff --git a/commitcraft/analysis/rule_engine.py b/commitcraft/analysis/rule_engine.py
13
+ new file mode 100644
14
+ index 0000000..a521dfc
15
+ --- /dev/null
16
+ +++ b/commitcraft/analysis/rule_engine.py
17
+ @@ -0,0 +1,91 @@
18
+ +from dataclasses import dataclass
19
+ +from pathlib import Path
20
+ +from commitcraft.analysis.filters import FilteredDiff
21
+ +
22
+ +LOCKFILE_NAMES = {
23
+ + "package-lock.json", "yarn.lock", "poetry.lock",
24
+ + "Pipfile.lock", "composer.lock", "Gemfile.lock", "pnpm-lock.yaml",
25
+ +}
26
+ +
27
+ +DEPENDENCY_MANIFESTS = {
28
+ + "requirements.txt", "requirements-dev.txt", "setup.cfg",
29
+ + "package.json", "Pipfile", "pyproject.toml",
30
+ +}
31
+ +
32
+ +CONFIG_PATTERNS = {".yml", ".yaml", ".json", ".toml", ".ini", ".cfg", ".env"}
33
+ +CONFIG_DIRS = {".github", ".circleci", ".gitlab", "config", ".husky"}
34
+ +
35
+ +
36
+ +@dataclass
37
+ +class RuleResult:
38
+ + matched: bool
39
+ + message: str | None
40
+ + rule_name: str | None
41
+ +
42
+ +
43
+ +def _all_paths(filtered: FilteredDiff) -> list[str]:
44
+ + return [f.path for f in filtered.files]
45
+ +
46
+ +
47
+ +def _readme_only(paths: list[str]) -> RuleResult | None:
48
+ + if len(paths) == 1 and Path(paths[0]).name.upper().startswith("README"):
49
+ + return RuleResult(matched=True, message="docs: update README", rule_name="readme_only")
50
+ + return None
51
+ +
52
+ +
53
+ +def _lockfile_only(paths: list[str]) -> RuleResult | None:
54
+ + if all(Path(p).name in LOCKFILE_NAMES for p in paths):
55
+ + return RuleResult(matched=True, message="chore: update lockfile", rule_name="lockfile_only")
56
+ + return None
57
+ +
58
+ +
59
+ +def _dependency_bump(paths: list[str]) -> RuleResult | None:
60
+ + if all(Path(p).name in DEPENDENCY_MANIFESTS for p in paths):
61
+ + return RuleResult(matched=True, message="chore: bump dependencies", rule_name="dependency_bump")
62
+ + return None
63
+ +
64
+ +
65
+ +def _docs_only(paths: list[str]) -> RuleResult | None:
66
+ + doc_exts = {".md", ".rst", ".txt"}
67
+ + doc_dirs = {"docs", "doc", "documentation"}
68
+ + if all(
69
+ + Path(p).suffix.lower() in doc_exts or Path(p).parts[0].lower() in doc_dirs
70
+ + for p in paths
71
+ + ):
72
+ + return RuleResult(matched=True, message="docs: update documentation", rule_name="docs_only")
73
+ + return None
74
+ +
75
+ +
76
+ +def _single_test_file(paths: list[str]) -> RuleResult | None:
77
+ + if len(paths) == 1 and "test" in paths[0].lower():
78
+ + name = Path(paths[0]).stem
79
+ + return RuleResult(matched=True, message=f"test: add tests for {name}", rule_name="single_test_file")
80
+ + return None
81
+ +
82
+ +
83
+ +def _config_only(paths: list[str]) -> RuleResult | None:
84
+ + def is_config(p: str) -> bool:
85
+ + parts = Path(p).parts
86
+ + return (
87
+ + Path(p).suffix.lower() in CONFIG_PATTERNS
88
+ + and (parts[0] in CONFIG_DIRS or not any("src" in part or "lib" in part for part in parts))
89
+ + )
90
+ + if paths and all(is_config(p) for p in paths):
91
+ + return RuleResult(matched=True, message="chore: update config", rule_name="config_only")
92
+ + return None
93
+ +
94
+ +
95
+ +_RULES = [_readme_only, _lockfile_only, _dependency_bump, _docs_only, _single_test_file, _config_only]
96
+ +
97
+ +
98
+ +def apply_rules(filtered: FilteredDiff) -> RuleResult:
99
+ + if not filtered.files:
100
+ + return RuleResult(matched=False, message=None, rule_name=None)
101
+ +
102
+ + paths = _all_paths(filtered)
103
+ + for rule in _RULES:
104
+ + result = rule(paths)
105
+ + if result is not None:
106
+ + return result
107
+ +
108
+ + return RuleResult(matched=False, message=None, rule_name=None)
109
+ diff --git a/tests/test_rule_engine.py b/tests/test_rule_engine.py
110
+ new file mode 100644
111
+ index 0000000..f8ec227
112
+ --- /dev/null
113
+ +++ b/tests/test_rule_engine.py
114
+ @@ -0,0 +1,97 @@
115
+ +import pytest
116
+ +from commitcraft.git.diff_parser import DiffFile
117
+ +from commitcraft.analysis.filters import FilteredDiff
118
+ +from commitcraft.analysis.rule_engine import apply_rules, RuleResult
119
+ +
120
+ +
121
+ +def _file(path: str, added: int = 3, removed: int = 1) -> DiffFile:
122
+ + return DiffFile(
123
+ + path=path, file_type="text", is_new=False, is_deleted=False,
124
+ + added_lines=added, removed_lines=removed, raw_hunks="",
125
+ + has_function_changes=False, has_class_changes=False,
126
+ + )
127
+ +
128
+ +
129
+ +def _diff(*files: DiffFile) -> FilteredDiff:
130
+ + return FilteredDiff(
131
+ + files=list(files),
132
+ + total_added=sum(f.added_lines for f in files),
133
+ + total_removed=sum(f.removed_lines for f in files),
134
+ + )
135
+ +
136
+ +
137
+ +def test_readme_only_matches():
138
+ + result = apply_rules(_diff(_file("README.md")))
139
+ + assert result.matched is True
140
+ + assert result.message == "docs: update README"
141
+ + assert result.rule_name == "readme_only"
142
+ +
143
+ +
144
+ +def test_lockfile_package_lock_matches():
145
+ + result = apply_rules(_diff(_file("package-lock.json")))
146
+ + assert result.matched is True
147
+ + assert "chore" in result.message
148
+ + assert result.rule_name == "lockfile_only"
149
+ +
150
+ +
151
+ +def test_lockfile_poetry_lock_matches():
152
+ + result = apply_rules(_diff(_file("poetry.lock")))
153
+ + assert result.matched is True
154
+ + assert result.rule_name == "lockfile_only"
155
+ +
156
+ +
157
+ +def test_lockfile_yarn_lock_matches():
158
+ + result = apply_rules(_diff(_file("yarn.lock")))
159
+ + assert result.matched is True
160
+ + assert result.rule_name == "lockfile_only"
161
+ +
162
+ +
163
+ +def test_requirements_txt_matches_dependency_bump():
164
+ + result = apply_rules(_diff(_file("requirements.txt")))
165
+ + assert result.matched is True
166
+ + assert result.message == "chore: bump dependencies"
167
+ + assert result.rule_name == "dependency_bump"
168
+ +
169
+ +
170
+ +def test_package_json_without_lock_matches_dependency_bump():
171
+ + result = apply_rules(_diff(_file("package.json")))
172
+ + assert result.matched is True
173
+ + assert result.rule_name == "dependency_bump"
174
+ +
175
+ +
176
+ +def test_docs_directory_only():
177
+ + result = apply_rules(_diff(_file("docs/guide.md"), _file("docs/api.md")))
178
+ + assert result.matched is True
179
+ + assert result.message == "docs: update documentation"
180
+ + assert result.rule_name == "docs_only"
181
+ +
182
+ +
183
+ +def test_single_test_file_only():
184
+ + f = _file("tests/test_utils.py")
185
+ + result = apply_rules(_diff(f))
186
+ + assert result.matched is True
187
+ + assert "test" in result.message.lower()
188
+ + assert result.rule_name == "single_test_file"
189
+ +
190
+ +
191
+ +def test_config_file_only():
192
+ + result = apply_rules(_diff(_file(".github/workflows/ci.yml")))
193
+ + assert result.matched is True
194
+ + assert result.rule_name == "config_only"
195
+ +
196
+ +
197
+ +def test_complex_change_no_match():
198
+ + result = apply_rules(_diff(_file("src/auth.py"), _file("src/db.py"), _file("tests/test_auth.py")))
199
+ + assert result.matched is False
200
+ + assert result.message is None
201
+ + assert result.rule_name is None
202
+ +
203
+ +
204
+ +def test_mixed_readme_and_source_no_match():
205
+ + result = apply_rules(_diff(_file("README.md"), _file("src/main.py")))
206
+ + assert result.matched is False
207
+ +
208
+ +
209
+ +def test_result_is_rulesresult_type():
210
+ + result = apply_rules(_diff(_file("README.md")))
211
+ + assert isinstance(result, RuleResult)
@@ -0,0 +1,8 @@
1
+ # Review package: 305ed0e..HEAD
2
+
3
+ ## Commits
4
+ e0ac712 chore: scaffold commitcraft project with stubbed CLI commands
5
+
6
+ ## Files changed
7
+
8
+ ## Diff
@@ -0,0 +1,172 @@
1
+ # Review package: 30a7e6e..HEAD
2
+
3
+ ## Commits
4
+ f4f700d feat: context builder produces condensed LLM prompts from filtered diff
5
+
6
+ ## Files changed
7
+ commitcraft/context/__init__.py | 0
8
+ .../context/__pycache__/__init__.cpython-312.pyc | Bin 0 -> 152 bytes
9
+ .../context/__pycache__/builder.cpython-312.pyc | Bin 0 -> 3321 bytes
10
+ commitcraft/context/builder.py | 52 ++++++++++++++++++
11
+ commitcraft/utils/__init__.py | 0
12
+ .../utils/__pycache__/__init__.cpython-312.pyc | Bin 0 -> 150 bytes
13
+ .../__pycache__/token_estimator.cpython-312.pyc | Bin 0 -> 396 bytes
14
+ commitcraft/utils/token_estimator.py | 3 ++
15
+ tests/test_context_builder.py | 59 +++++++++++++++++++++
16
+ 9 files changed, 114 insertions(+)
17
+
18
+ ## Diff
19
+ diff --git a/commitcraft/context/__init__.py b/commitcraft/context/__init__.py
20
+ new file mode 100644
21
+ index 0000000..e69de29
22
+ diff --git a/commitcraft/context/__pycache__/__init__.cpython-312.pyc b/commitcraft/context/__pycache__/__init__.cpython-312.pyc
23
+ new file mode 100644
24
+ index 0000000..fd5bf45
25
+ Binary files /dev/null and b/commitcraft/context/__pycache__/__init__.cpython-312.pyc differ
26
+ diff --git a/commitcraft/context/__pycache__/builder.cpython-312.pyc b/commitcraft/context/__pycache__/builder.cpython-312.pyc
27
+ new file mode 100644
28
+ index 0000000..8f9985d
29
+ Binary files /dev/null and b/commitcraft/context/__pycache__/builder.cpython-312.pyc differ
30
+ diff --git a/commitcraft/context/builder.py b/commitcraft/context/builder.py
31
+ new file mode 100644
32
+ index 0000000..4a61ff6
33
+ --- /dev/null
34
+ +++ b/commitcraft/context/builder.py
35
+ @@ -0,0 +1,52 @@
36
+ +from commitcraft.analysis.filters import FilteredDiff
37
+ +
38
+ +
39
+ +def build_commit_context(filtered: FilteredDiff, recent_commits: list[str]) -> str:
40
+ + lines: list[str] = []
41
+ +
42
+ + lines.append("=== Changed Files ===")
43
+ + for f in filtered.files:
44
+ + markers = []
45
+ + if f.is_new:
46
+ + markers.append("NEW")
47
+ + if f.is_deleted:
48
+ + markers.append("DELETED")
49
+ + if f.has_function_changes:
50
+ + markers.append("has function/def changes")
51
+ + if f.has_class_changes:
52
+ + markers.append("has class changes")
53
+ + marker_str = f" [{', '.join(markers)}]" if markers else ""
54
+ + lines.append(f" {f.path} (+{f.added_lines}/-{f.removed_lines}){marker_str}")
55
+ +
56
+ + lines.append(f"\nTotal: +{filtered.total_added}/-{filtered.total_removed} lines across {len(filtered.files)} file(s)")
57
+ +
58
+ + if recent_commits:
59
+ + lines.append("\n=== Recent Commit Style (follow this format) ===")
60
+ + for c in recent_commits[:5]:
61
+ + lines.append(f" {c}")
62
+ +
63
+ + lines.append("\n=== Task ===")
64
+ + lines.append("Write a single conventional commit message for the changes above.")
65
+ + lines.append("Output ONLY the commit message. No explanation.")
66
+ +
67
+ + return "\n".join(lines)
68
+ +
69
+ +
70
+ +def build_pr_context(branch_diff: str, branch_name: str, recent_commits: list[str]) -> str:
71
+ + lines: list[str] = []
72
+ + lines.append(f"Branch: {branch_name}")
73
+ + lines.append(f"\nRecent commits on this branch:")
74
+ + for c in recent_commits[:10]:
75
+ + lines.append(f" {c}")
76
+ + lines.append("\n=== Task ===")
77
+ + lines.append("Write a GitHub PR description for this branch. Use markdown with ## Summary and ## Changes sections.")
78
+ + return "\n".join(lines)
79
+ +
80
+ +
81
+ +def build_release_context(tag_diff: str, tag_range: str) -> str:
82
+ + lines: list[str] = [
83
+ + f"Release range: {tag_range}",
84
+ + "\n=== Task ===",
85
+ + "Write structured release notes grouped by: Features, Bug Fixes, Documentation, Other. Use markdown.",
86
+ + ]
87
+ + return "\n".join(lines)
88
+ diff --git a/commitcraft/utils/__init__.py b/commitcraft/utils/__init__.py
89
+ new file mode 100644
90
+ index 0000000..e69de29
91
+ diff --git a/commitcraft/utils/__pycache__/__init__.cpython-312.pyc b/commitcraft/utils/__pycache__/__init__.cpython-312.pyc
92
+ new file mode 100644
93
+ index 0000000..3c18b7b
94
+ Binary files /dev/null and b/commitcraft/utils/__pycache__/__init__.cpython-312.pyc differ
95
+ diff --git a/commitcraft/utils/__pycache__/token_estimator.cpython-312.pyc b/commitcraft/utils/__pycache__/token_estimator.cpython-312.pyc
96
+ new file mode 100644
97
+ index 0000000..444a5ab
98
+ Binary files /dev/null and b/commitcraft/utils/__pycache__/token_estimator.cpython-312.pyc differ
99
+ diff --git a/commitcraft/utils/token_estimator.py b/commitcraft/utils/token_estimator.py
100
+ new file mode 100644
101
+ index 0000000..edafb04
102
+ --- /dev/null
103
+ +++ b/commitcraft/utils/token_estimator.py
104
+ @@ -0,0 +1,3 @@
105
+ +def estimate_tokens(text: str) -> int:
106
+ + # Rough approximation: 1 token ≈ 4 characters (widely used heuristic)
107
+ + return max(1, len(text) // 4)
108
+ diff --git a/tests/test_context_builder.py b/tests/test_context_builder.py
109
+ new file mode 100644
110
+ index 0000000..f30177a
111
+ --- /dev/null
112
+ +++ b/tests/test_context_builder.py
113
+ @@ -0,0 +1,59 @@
114
+ +import pytest
115
+ +from commitcraft.git.diff_parser import DiffFile
116
+ +from commitcraft.analysis.filters import FilteredDiff
117
+ +from commitcraft.context.builder import build_commit_context, build_pr_context
118
+ +from commitcraft.utils.token_estimator import estimate_tokens
119
+ +
120
+ +
121
+ +def _file(path: str, added: int = 10, removed: int = 3, has_func: bool = False) -> DiffFile:
122
+ + return DiffFile(
123
+ + path=path, file_type="python", is_new=False, is_deleted=False,
124
+ + added_lines=added, removed_lines=removed, raw_hunks=f"@@ hunk for {path} @@\n+new line",
125
+ + has_function_changes=has_func, has_class_changes=False,
126
+ + )
127
+ +
128
+ +
129
+ +def test_commit_context_contains_file_list():
130
+ + diff = FilteredDiff(
131
+ + files=[_file("src/auth.py"), _file("tests/test_auth.py")],
132
+ + total_added=20, total_removed=6,
133
+ + )
134
+ + ctx = build_commit_context(diff, recent_commits=["feat: add login", "fix: fix typo"])
135
+ + assert "src/auth.py" in ctx
136
+ + assert "tests/test_auth.py" in ctx
137
+ +
138
+ +
139
+ +def test_commit_context_contains_line_counts():
140
+ + diff = FilteredDiff(files=[_file("src/a.py", added=15, removed=3)], total_added=15, total_removed=3)
141
+ + ctx = build_commit_context(diff, recent_commits=[])
142
+ + assert "15" in ctx or "+15" in ctx
143
+ +
144
+ +
145
+ +def test_commit_context_mentions_function_changes():
146
+ + diff = FilteredDiff(files=[_file("src/a.py", has_func=True)], total_added=10, total_removed=0)
147
+ + ctx = build_commit_context(diff, recent_commits=[])
148
+ + assert "function" in ctx.lower() or "def" in ctx.lower()
149
+ +
150
+ +
151
+ +def test_commit_context_includes_recent_commit_style():
152
+ + recent = ["feat: add authentication", "fix: resolve login bug"]
153
+ + diff = FilteredDiff(files=[_file("src/a.py")], total_added=5, total_removed=0)
154
+ + ctx = build_commit_context(diff, recent_commits=recent)
155
+ + assert "feat: add authentication" in ctx
156
+ +
157
+ +
158
+ +def test_commit_context_is_short():
159
+ + diff = FilteredDiff(files=[_file("src/a.py", added=500)], total_added=500, total_removed=0)
160
+ + ctx = build_commit_context(diff, recent_commits=[])
161
+ + assert estimate_tokens(ctx) < 800
162
+ +
163
+ +
164
+ +def test_pr_context_contains_branch_name():
165
+ + ctx = build_pr_context("diff content", "feature/add-auth", ["feat: add auth"])
166
+ + assert "feature/add-auth" in ctx or "add-auth" in ctx
167
+ +
168
+ +
169
+ +def test_estimate_tokens_rough():
170
+ + text = "hello world " * 100 # 200 words ~ 300 tokens
171
+ + tokens = estimate_tokens(text)
172
+ + assert 150 < tokens < 500