weld-cli 0.6.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 (115) hide show
  1. weld_cli-0.6.0/.claude/docs/DOCS_BEST_PRACTICES.md +209 -0
  2. weld_cli-0.6.0/.claude/docs/PYTHON_BEST_PRACTICES.md +215 -0
  3. weld_cli-0.6.0/.claude/docs/RELEASE_WORKFLOW.md +272 -0
  4. weld_cli-0.6.0/.claude/docs/SPEC.md +1228 -0
  5. weld_cli-0.6.0/.github/weld-logo.png +0 -0
  6. weld_cli-0.6.0/.github/workflows/ci.yml +59 -0
  7. weld_cli-0.6.0/.github/workflows/docs.yml +68 -0
  8. weld_cli-0.6.0/.github/workflows/release.yml +91 -0
  9. weld_cli-0.6.0/.gitignore +57 -0
  10. weld_cli-0.6.0/.pre-commit-config.yaml +29 -0
  11. weld_cli-0.6.0/.python-version +1 -0
  12. weld_cli-0.6.0/.secrets.baseline +127 -0
  13. weld_cli-0.6.0/CHANGELOG.md +312 -0
  14. weld_cli-0.6.0/CLAUDE.md +221 -0
  15. weld_cli-0.6.0/LICENSE +190 -0
  16. weld_cli-0.6.0/Makefile +351 -0
  17. weld_cli-0.6.0/PKG-INFO +84 -0
  18. weld_cli-0.6.0/README.md +52 -0
  19. weld_cli-0.6.0/RELEASE_NOTES.md +19 -0
  20. weld_cli-0.6.0/docs/assets/weld-logo.png +0 -0
  21. weld_cli-0.6.0/docs/commands/commit.md +88 -0
  22. weld_cli-0.6.0/docs/commands/discover.md +69 -0
  23. weld_cli-0.6.0/docs/commands/doctor.md +53 -0
  24. weld_cli-0.6.0/docs/commands/implement.md +231 -0
  25. weld_cli-0.6.0/docs/commands/index.md +57 -0
  26. weld_cli-0.6.0/docs/commands/init.md +44 -0
  27. weld_cli-0.6.0/docs/commands/interview.md +57 -0
  28. weld_cli-0.6.0/docs/commands/plan.md +61 -0
  29. weld_cli-0.6.0/docs/commands/research.md +66 -0
  30. weld_cli-0.6.0/docs/commands/review.md +91 -0
  31. weld_cli-0.6.0/docs/configuration.md +111 -0
  32. weld_cli-0.6.0/docs/development/architecture.md +127 -0
  33. weld_cli-0.6.0/docs/development/contributing.md +92 -0
  34. weld_cli-0.6.0/docs/development/index.md +90 -0
  35. weld_cli-0.6.0/docs/index.md +52 -0
  36. weld_cli-0.6.0/docs/installation.md +84 -0
  37. weld_cli-0.6.0/docs/overrides/.gitkeep +3 -0
  38. weld_cli-0.6.0/docs/quickstart.md +74 -0
  39. weld_cli-0.6.0/docs/reference/exit-codes.md +70 -0
  40. weld_cli-0.6.0/docs/reference/plan-format.md +122 -0
  41. weld_cli-0.6.0/docs/stylesheets/extra.css +51 -0
  42. weld_cli-0.6.0/docs/troubleshooting.md +140 -0
  43. weld_cli-0.6.0/docs/workflow.md +125 -0
  44. weld_cli-0.6.0/mkdocs.yml +167 -0
  45. weld_cli-0.6.0/pyproject.toml +112 -0
  46. weld_cli-0.6.0/scripts/assert_unreleased_empty.py +20 -0
  47. weld_cli-0.6.0/scripts/extract_release_notes.py +27 -0
  48. weld_cli-0.6.0/src/weld/__init__.py +3 -0
  49. weld_cli-0.6.0/src/weld/__main__.py +6 -0
  50. weld_cli-0.6.0/src/weld/cli.py +135 -0
  51. weld_cli-0.6.0/src/weld/commands/__init__.py +17 -0
  52. weld_cli-0.6.0/src/weld/commands/commit.py +821 -0
  53. weld_cli-0.6.0/src/weld/commands/discover.py +147 -0
  54. weld_cli-0.6.0/src/weld/commands/doc_review.py +439 -0
  55. weld_cli-0.6.0/src/weld/commands/doctor.py +88 -0
  56. weld_cli-0.6.0/src/weld/commands/implement.py +869 -0
  57. weld_cli-0.6.0/src/weld/commands/init.py +127 -0
  58. weld_cli-0.6.0/src/weld/commands/interview.py +68 -0
  59. weld_cli-0.6.0/src/weld/commands/plan.py +293 -0
  60. weld_cli-0.6.0/src/weld/commands/research.py +182 -0
  61. weld_cli-0.6.0/src/weld/config.py +322 -0
  62. weld_cli-0.6.0/src/weld/constants.py +34 -0
  63. weld_cli-0.6.0/src/weld/core/__init__.py +60 -0
  64. weld_cli-0.6.0/src/weld/core/discover_engine.py +196 -0
  65. weld_cli-0.6.0/src/weld/core/doc_review_engine.py +502 -0
  66. weld_cli-0.6.0/src/weld/core/history.py +112 -0
  67. weld_cli-0.6.0/src/weld/core/interview_engine.py +106 -0
  68. weld_cli-0.6.0/src/weld/core/plan_parser.py +342 -0
  69. weld_cli-0.6.0/src/weld/core/weld_dir.py +36 -0
  70. weld_cli-0.6.0/src/weld/logging.py +116 -0
  71. weld_cli-0.6.0/src/weld/models/__init__.py +24 -0
  72. weld_cli-0.6.0/src/weld/models/discover.py +30 -0
  73. weld_cli-0.6.0/src/weld/models/issues.py +54 -0
  74. weld_cli-0.6.0/src/weld/models/session.py +55 -0
  75. weld_cli-0.6.0/src/weld/output.py +94 -0
  76. weld_cli-0.6.0/src/weld/py.typed +2 -0
  77. weld_cli-0.6.0/src/weld/services/__init__.py +125 -0
  78. weld_cli-0.6.0/src/weld/services/claude.py +346 -0
  79. weld_cli-0.6.0/src/weld/services/filesystem.py +84 -0
  80. weld_cli-0.6.0/src/weld/services/gist_uploader.py +123 -0
  81. weld_cli-0.6.0/src/weld/services/git.py +222 -0
  82. weld_cli-0.6.0/src/weld/services/session_detector.py +100 -0
  83. weld_cli-0.6.0/src/weld/services/session_tracker.py +353 -0
  84. weld_cli-0.6.0/src/weld/services/transcript_renderer.py +194 -0
  85. weld_cli-0.6.0/src/weld/services/transcripts.py +91 -0
  86. weld_cli-0.6.0/src/weld/validation.py +54 -0
  87. weld_cli-0.6.0/tests/__init__.py +1 -0
  88. weld_cli-0.6.0/tests/conftest.py +98 -0
  89. weld_cli-0.6.0/tests/e2e_test.sh +312 -0
  90. weld_cli-0.6.0/tests/test_claude.py +514 -0
  91. weld_cli-0.6.0/tests/test_cli.py +1074 -0
  92. weld_cli-0.6.0/tests/test_commit.py +812 -0
  93. weld_cli-0.6.0/tests/test_commit_e2e.py +180 -0
  94. weld_cli-0.6.0/tests/test_config.py +478 -0
  95. weld_cli-0.6.0/tests/test_discover.py +147 -0
  96. weld_cli-0.6.0/tests/test_doc_review.py +779 -0
  97. weld_cli-0.6.0/tests/test_filesystem.py +139 -0
  98. weld_cli-0.6.0/tests/test_gist_uploader.py +341 -0
  99. weld_cli-0.6.0/tests/test_git.py +356 -0
  100. weld_cli-0.6.0/tests/test_history.py +370 -0
  101. weld_cli-0.6.0/tests/test_implement.py +750 -0
  102. weld_cli-0.6.0/tests/test_interview.py +138 -0
  103. weld_cli-0.6.0/tests/test_logging.py +131 -0
  104. weld_cli-0.6.0/tests/test_output.py +265 -0
  105. weld_cli-0.6.0/tests/test_plan.py +176 -0
  106. weld_cli-0.6.0/tests/test_plan_parser.py +442 -0
  107. weld_cli-0.6.0/tests/test_research.py +107 -0
  108. weld_cli-0.6.0/tests/test_session_detector.py +195 -0
  109. weld_cli-0.6.0/tests/test_session_models.py +196 -0
  110. weld_cli-0.6.0/tests/test_session_tracker.py +772 -0
  111. weld_cli-0.6.0/tests/test_transcript_renderer.py +431 -0
  112. weld_cli-0.6.0/tests/test_transcripts.py +146 -0
  113. weld_cli-0.6.0/tests/test_validation.py +71 -0
  114. weld_cli-0.6.0/tests/test_weld_dir.py +82 -0
  115. weld_cli-0.6.0/uv.lock +1427 -0
@@ -0,0 +1,209 @@
1
+ ## Best practices for user docs for a Python CLI tool (OSS-grade)
2
+
3
+ ### 1) Define your doc set (minimum viable, then scale)
4
+
5
+ **Minimum**
6
+
7
+ * **README.md**: fast onboarding + core commands.
8
+ * **`mycli --help`**: authoritative CLI reference for flags/args.
9
+ * **Command reference** (docs site or `/docs`): one page per command + examples.
10
+
11
+ **Common additions**
12
+
13
+ * **Installation**: pipx / uv tool install / pip / brew (if you ship it).
14
+ * **Configuration**: file format, env vars, precedence, locations.
15
+ * **Tutorials**: 2–3 end-to-end workflows users actually do.
16
+ * **How-to**: targeted tasks (“use dry-run”, “debug logging”, “CI integration”).
17
+ * **Troubleshooting**: known errors, exit codes, common gotchas.
18
+ * **FAQ**: concise answers, links to deeper docs.
19
+ * **Changelog** + **Upgrade notes**: breaking changes and migration steps.
20
+
21
+ ---
22
+
23
+ ### 2) Optimize for “first success in 2 minutes”
24
+
25
+ Your README should contain, in this order:
26
+
27
+ 1. **One-sentence value** (what it does, who for).
28
+ 2. **Install** (one preferred method, others collapsed).
29
+ 3. **Quick start**: 3–5 commands that produce a visible result.
30
+ 4. **Common workflows**: 2–4 examples.
31
+ 5. **Configuration**: where config lives + minimal example.
32
+ 6. **Support**: how to file issues, debug info to include.
33
+
34
+ Pattern for quick start:
35
+
36
+ ```bash
37
+ uv tool install mycli
38
+ mycli init
39
+ mycli run plan.md
40
+ mycli status
41
+ ```
42
+
43
+ ---
44
+
45
+ ### 3) Make the CLI self-documenting
46
+
47
+ **`--help` quality is part of docs quality.**
48
+
49
+ Best practices:
50
+
51
+ * Every command has:
52
+
53
+ * short summary
54
+ * longer help text
55
+ * examples (even 1 is enough)
56
+ * Flags:
57
+
58
+ * consistent naming (`--dry-run`, `--debug`, `--json`, `--config`, `--version`)
59
+ * clear defaults shown in help output
60
+ * Subcommands are discoverable:
61
+
62
+ * `mycli help`
63
+ * `mycli <cmd> --help`
64
+
65
+ If using Typer/Click, invest in:
66
+
67
+ * good parameter names
68
+ * `help=` strings everywhere
69
+ * sane defaults
70
+
71
+ ---
72
+
73
+ ### 4) Document output contracts (users build automation)
74
+
75
+ If people will script your tool, document:
76
+
77
+ * **Exit codes** (table)
78
+ * **stdout vs stderr** behavior
79
+ * **JSON output schemas** (`--json`), stability guarantees:
80
+
81
+ * “stable fields” vs “best-effort fields”
82
+ * **Determinism** expectations (ordering, timestamps, concurrency)
83
+
84
+ Example exit code contract:
85
+
86
+ * `0` success
87
+ * `1` runtime error
88
+ * `2` usage error (bad args/config)
89
+ * `3` partial success / some checks failed (if relevant)
90
+
91
+ ---
92
+
93
+ ### 5) Configuration docs: be explicit and testable
94
+
95
+ Users fail most often on config.
96
+
97
+ Include:
98
+
99
+ * **Search path** (e.g. current dir → XDG → home)
100
+ * **Precedence**: flags > env > config file > defaults
101
+ * **Full config reference** (generated if possible)
102
+ * A “minimal config” and a “full config” example
103
+ * Validation behavior: what happens on unknown keys / wrong types
104
+
105
+ If you support env vars, document them in a dedicated table:
106
+
107
+ * name, type, default, example, related flags.
108
+
109
+ ---
110
+
111
+ ### 6) Examples that match real user intent
112
+
113
+ Prefer “task-based” pages over exhaustive prose.
114
+
115
+ * “Run in dry-run mode to preview changes”
116
+ * “Enable debug logs and collect diagnostics for an issue”
117
+ * “Integrate with CI (GitHub Actions)”
118
+ * “Use JSON output with jq”
119
+ * “Handle non-zero exit codes in bash”
120
+
121
+ Each example should show:
122
+
123
+ * command
124
+ * expected output snippet (short)
125
+ * what to do next
126
+
127
+ ---
128
+
129
+ ### 7) Keep docs versioned and tied to releases
130
+
131
+ Best practice for OSS:
132
+
133
+ * Host docs per version (or at least “latest” + “stable”).
134
+ * At minimum: docs in repo; release tags preserve exact docs state.
135
+ * For breaking changes, add a dedicated **Upgrade Guide** section and link it from release notes.
136
+
137
+ ---
138
+
139
+ ### 8) Choose a docs toolchain that fits CLI repos
140
+
141
+ Recommended:
142
+
143
+ * **MkDocs + mkdocs-material**: simple, fast, great UX.
144
+ * Keep docs in `docs/`, build on CI, deploy to GitHub Pages.
145
+
146
+ Doc site structure that scales:
147
+
148
+ ```
149
+ docs/
150
+ index.md
151
+ install.md
152
+ quickstart.md
153
+ commands/
154
+ init.md
155
+ run.md
156
+ status.md
157
+ checks.md
158
+ config.md
159
+ tutorials/
160
+ workflow-basic.md
161
+ ci-github-actions.md
162
+ troubleshooting.md
163
+ faq.md
164
+ ```
165
+
166
+ ---
167
+
168
+ ### 9) Troubleshooting: include a diagnostics checklist
169
+
170
+ Have a “copy/paste for issues” section:
171
+
172
+ * `mycli --version`
173
+ * `python --version`
174
+ * OS info
175
+ * config file used
176
+ * command invoked
177
+ * `--debug` logs (redaction guidance)
178
+ * relevant output files/artifacts paths
179
+
180
+ Also document:
181
+
182
+ * common permission errors
183
+ * path issues
184
+ * lockfile/state issues (if your tool uses locks)
185
+ * proxy/network behavior (if applicable)
186
+
187
+ ---
188
+
189
+ ### 10) Doc quality gates (enterprise habit)
190
+
191
+ Add CI that fails if docs break:
192
+
193
+ * Build docs on PRs (MkDocs build)
194
+ * Check links (optional)
195
+ * Spellcheck (optional, but useful at scale)
196
+ * Ensure `--help` output is updated if you snapshot it
197
+
198
+ ---
199
+
200
+ ## Recommended “minimum bar” for your CLI (given your features like `--dry-run`, `--debug`, checks)
201
+
202
+ * README with install + quick start + “how it works” paragraph
203
+ * Command docs: `init`, `plan`, `implement`, `review`, `checks`, `status`, `commit`
204
+ * Config docs including new checks categories and defaults
205
+ * “CI integration” how-to
206
+ * Troubleshooting page covering locks, stale locks, debug logs, non-zero exit codes
207
+ * Output contract page (`--json`, exit codes)
208
+
209
+ If you share your command list (or `mycli --help` output), I can propose an exact docs IA (page tree) and draft the README + 2–3 core docs pages in your style.
@@ -0,0 +1,215 @@
1
+ ## Enterprise-grade best practices for a `uv`-managed Python CLI tool
2
+
3
+ ### Project layout (CLI-first, testable, packageable)
4
+
5
+ Use `src/` layout and keep CLI thin.
6
+
7
+ ```
8
+ mycli/
9
+ pyproject.toml
10
+ uv.lock
11
+ src/mycli/
12
+ __init__.py
13
+ __main__.py
14
+ cli.py
15
+ commands/
16
+ __init__.py
17
+ foo.py
18
+ core/
19
+ __init__.py
20
+ logic.py
21
+ services/
22
+ __init__.py
23
+ fs.py
24
+ net.py
25
+ tests/
26
+ test_cli.py
27
+ test_logic.py
28
+ README.md
29
+ ```
30
+
31
+ Principles
32
+
33
+ * **`cli.py`**: argument parsing + command dispatch only.
34
+ * **`core/`**: pure logic, easy unit tests.
35
+ * **`services/`**: side effects (filesystem/network/subprocess), injectable/mocked.
36
+
37
+ ---
38
+
39
+ ## `uv` workflow standard (local + CI)
40
+
41
+ ### Commands developers run
42
+
43
+ * Install/sync: `uv sync`
44
+ * Run CLI: `uv run mycli ...`
45
+ * Run tests: `uv run pytest`
46
+ * Lint: `uv run ruff check .`
47
+ * Format: `uv run ruff format .` (or `uv run black .`)
48
+ * Typecheck: `uv run pyright`
49
+
50
+ ### CI should run (no autofix)
51
+
52
+ * `uv sync --frozen`
53
+ * `uv run ruff format --check .`
54
+ * `uv run ruff check .`
55
+ * `uv run pyright`
56
+ * `uv run pytest -q --maxfail=1`
57
+ * `uv run pip-audit` (or `uv run pip-audit -r <exported requirements>` if needed)
58
+
59
+ ---
60
+
61
+ ## `pyproject.toml` baseline (uv + CLI + quality gates)
62
+
63
+ ```toml
64
+ [project]
65
+ name = "mycli"
66
+ version = "0.1.0"
67
+ description = "My CLI tool"
68
+ requires-python = ">=3.11"
69
+ dependencies = [
70
+ "typer>=0.12",
71
+ "rich>=13.7",
72
+ ]
73
+
74
+ [project.scripts]
75
+ mycli = "mycli.cli:app"
76
+
77
+ [build-system]
78
+ requires = ["hatchling"]
79
+ build-backend = "hatchling.build"
80
+
81
+ [tool.ruff]
82
+ target-version = "py311"
83
+ line-length = 100
84
+ src = ["src"]
85
+
86
+ [tool.ruff.lint]
87
+ select = ["E","F","I","UP","B","SIM","C4","RUF"]
88
+ ignore = []
89
+
90
+ [tool.ruff.format]
91
+ quote-style = "double"
92
+ indent-style = "space"
93
+
94
+ [tool.pyright]
95
+ typeCheckingMode = "standard"
96
+ pythonVersion = "3.11"
97
+ include = ["src"]
98
+ ```
99
+
100
+ Notes
101
+
102
+ * `project.scripts` makes a proper entry point (`mycli`).
103
+ * `hatchling` is a clean default build backend for CLIs.
104
+ * Ruff rule-set chosen for CLI repos: correctness + modernization + import hygiene.
105
+
106
+ ---
107
+
108
+ ## CLI implementation best practices (enterprise-grade)
109
+
110
+ ### Use a real CLI framework
111
+
112
+ * Prefer **Typer** (excellent UX, type-hints map well to args/options).
113
+ * Alternate: Click (mature), argparse (stdlib, minimal deps).
114
+
115
+ ### Make failures predictable
116
+
117
+ * Exit codes: `0` success, `1` generic error, `2` usage error, etc.
118
+ * Print user-facing errors to **stderr**, and keep them concise.
119
+ * Provide `--json` for machine-readable output if automation matters.
120
+
121
+ ### Logging strategy
122
+
123
+ * Default: quiet user output; add `--verbose/-v` for logs.
124
+ * For CI / automation: `--log-level` and `--no-color`.
125
+ * If you emit logs, separate them from primary output (stderr vs stdout).
126
+
127
+ ### Configuration
128
+
129
+ * Support precedence:
130
+
131
+ 1. CLI flags
132
+ 2. environment variables
133
+ 3. config file (e.g., `~/.config/mycli/config.toml`)
134
+ 4. defaults
135
+ * Use `platformdirs` to locate config directories.
136
+
137
+ ---
138
+
139
+ ## Testing strategy for a CLI tool
140
+
141
+ ### Unit tests
142
+
143
+ * Pure logic in `core/` with straightforward tests.
144
+
145
+ ### CLI tests
146
+
147
+ * For Typer: `typer.testing.CliRunner()`
148
+ * Assert:
149
+
150
+ * exit code
151
+ * stdout/stderr
152
+ * behavior under invalid args
153
+ * behavior with env vars / config
154
+
155
+ ### Integration tests (optional but common)
156
+
157
+ * Use `tmp_path` for filesystem scenarios.
158
+ * Mock network/subprocess calls unless you explicitly test them behind a marker.
159
+
160
+ ---
161
+
162
+ ## Security + supply chain for CLIs
163
+
164
+ * Dependency scanning: `pip-audit` in CI.
165
+ * Secret scanning: `detect-secrets` or `gitleaks` pre-commit + CI.
166
+ * If invoking subprocesses:
167
+
168
+ * avoid `shell=True`
169
+ * sanitize args
170
+ * timeouts everywhere
171
+ * If handling files:
172
+
173
+ * avoid unsafe path joins (validate inputs; use `pathlib`)
174
+
175
+ ---
176
+
177
+ ## Pre-commit (works perfectly with `uv run`)
178
+
179
+ `.pre-commit-config.yaml` recommended hooks:
180
+
181
+ * ruff (lint)
182
+ * ruff (format)
183
+ * pyright (optional; can be slow but doable)
184
+ * detect-secrets
185
+
186
+ Run hooks via:
187
+
188
+ * `uv run pre-commit install`
189
+ * `uv run pre-commit run -a`
190
+
191
+ ---
192
+
193
+ ## Release and packaging (CLI distribution)
194
+
195
+ * Build: `uv run python -m build`
196
+ * Publish: `uv run twine upload dist/*`
197
+ * Add `--version` and `mycli version` command (handy for support).
198
+ * Consider shipping a single-file binary (optional):
199
+
200
+ * `pyinstaller` / `shiv` / `pex` depending on target environment.
201
+
202
+ ---
203
+
204
+ ## CI (GitHub Actions skeleton for `uv`)
205
+
206
+ Core pattern:
207
+
208
+ * checkout
209
+ * install uv
210
+ * `uv sync --frozen`
211
+ * run quality gates listed above
212
+
213
+ ---
214
+
215
+ If you want, I can output a complete ready-to-drop-in repo scaffold: `pyproject.toml`, `ruff/pyright` settings, `pre-commit` config, and a GitHub Actions workflow using `uv sync --frozen`, plus a minimal Typer-based CLI with one command and tests.
@@ -0,0 +1,272 @@
1
+ ## Best practices spec: OSS `uv` package releases via GitHub tag → CI → GitHub Release (+ PyPI)
2
+
3
+ ### Goals
4
+
5
+ * Release is **triggered by pushing a git tag** `vX.Y.Z`.
6
+ * CI **creates a GitHub Release** and **publishes to PyPI**.
7
+ * GitHub Release notes are **pulled automatically from `CHANGELOG.md`**.
8
+ * Release is **reproducible** and **fails early** on versioning/changelog mistakes.
9
+ * Prefer **PyPI Trusted Publishing (OIDC)** over long-lived API tokens.
10
+
11
+ ---
12
+
13
+ ## Repository requirements
14
+
15
+ ### 1) Version source of truth
16
+
17
+ * `pyproject.toml` contains `project.version = "X.Y.Z"`.
18
+ * The git tag is `vX.Y.Z`.
19
+ * CI must assert: `tag_version == pyproject_version`.
20
+
21
+ ### 2) Changelog format (Keep a Changelog compatible)
22
+
23
+ `CHANGELOG.md` must have:
24
+
25
+ * An `## [Unreleased]` section at the top.
26
+ * Released sections like: `## [0.1.0] - 2026-01-04`
27
+ * Content under headings (`###`, `####`, etc.) is allowed.
28
+
29
+ Example:
30
+
31
+ ```md
32
+ ## [Unreleased]
33
+ ### Added
34
+ - ...
35
+
36
+ ## [0.1.0] - 2026-01-04
37
+ Initial release...
38
+ ```
39
+
40
+ ---
41
+
42
+ ## Release process (human + automation)
43
+
44
+ ### Human steps (per release)
45
+
46
+ 1. Move entries from `## [Unreleased]` into a new version section `## [X.Y.Z] - YYYY-MM-DD`.
47
+ 2. Ensure `pyproject.toml` version is set to `X.Y.Z`.
48
+ 3. Commit to `main`.
49
+ 4. Create annotated tag: `git tag -a vX.Y.Z -m "vX.Y.Z"`.
50
+ 5. Push tag: `git push origin vX.Y.Z`.
51
+
52
+ ### CI responsibilities (on tag push)
53
+
54
+ 1. Checkout with full history.
55
+ 2. Install `uv`, sync dependencies with lockfile (`--frozen`).
56
+ 3. Run quality gates (format, lint, typecheck, tests).
57
+ 4. Extract version from tag.
58
+ 5. Verify `pyproject.toml` version matches tag.
59
+ 6. Verify `CHANGELOG.md`:
60
+
61
+ * `[X.Y.Z]` section exists.
62
+ * `## [Unreleased]` is **empty** (recommended gate).
63
+ 7. Extract release notes from the `[X.Y.Z]` section and write `RELEASE_NOTES.md`.
64
+ 8. Build sdist + wheel once.
65
+ 9. Publish to PyPI (Trusted Publishing preferred).
66
+ 10. Create GitHub Release using extracted notes and attach artifacts.
67
+
68
+ ---
69
+
70
+ ## Changelog extraction spec (tailored to your structure)
71
+
72
+ ### A) Extract release notes for version `X.Y.Z`
73
+
74
+ Rules:
75
+
76
+ * Find section header matching `^## \[X.Y.Z\]` (date suffix optional).
77
+ * Capture everything until the next `^## \[` section header or EOF.
78
+ * Exclude the header line itself.
79
+ * Preserve all markdown below (including nested headings).
80
+
81
+ Reference implementation (`scripts/extract_release_notes.py`):
82
+
83
+ ```python
84
+ import re, sys, pathlib
85
+
86
+ version = sys.argv[1]
87
+ text = pathlib.Path("CHANGELOG.md").read_text(encoding="utf-8")
88
+
89
+ pattern = rf"""
90
+ ^##\s+\[{re.escape(version)}\][^\n]*\n
91
+ (.*?)
92
+ (?=^##\s+\[|\Z)
93
+ """
94
+
95
+ m = re.search(pattern, text, re.S | re.M | re.X)
96
+ if not m:
97
+ raise SystemExit(f"CHANGELOG.md missing section for [{version}]")
98
+
99
+ notes = m.group(1).strip()
100
+ pathlib.Path("RELEASE_NOTES.md").write_text(notes + "\n", encoding="utf-8")
101
+ ```
102
+
103
+ ### B) Enforce “Unreleased is empty” on tag builds (recommended)
104
+
105
+ Rule:
106
+
107
+ * Extract the body of `## [Unreleased]`.
108
+ * If it contains non-whitespace content, fail the release.
109
+
110
+ Reference implementation (`scripts/assert_unreleased_empty.py`):
111
+
112
+ ```python
113
+ import re, pathlib
114
+
115
+ text = pathlib.Path("CHANGELOG.md").read_text(encoding="utf-8")
116
+ m = re.search(r"^##\s+\[Unreleased\]\n(.*?)(?=^##\s+\[|\Z)", text, re.S | re.M)
117
+
118
+ if m and m.group(1).strip():
119
+ raise SystemExit("Unreleased section is not empty — move entries into the release section before tagging.")
120
+ ```
121
+
122
+ Policy note:
123
+
124
+ * This is a “hard gate” that prevents accidental partial releases.
125
+ * If you want a softer policy, change it to warn-only (not recommended for clean OSS releases).
126
+
127
+ ---
128
+
129
+ ## GitHub Actions workflow (tag push trigger)
130
+
131
+ Create: `.github/workflows/release.yml`
132
+
133
+ ```yaml
134
+ name: release
135
+
136
+ on:
137
+ push:
138
+ tags:
139
+ - "v*"
140
+
141
+ permissions:
142
+ contents: write
143
+ id-token: write
144
+
145
+ jobs:
146
+ release:
147
+ runs-on: ubuntu-latest
148
+ steps:
149
+ - uses: actions/checkout@v4
150
+ with:
151
+ fetch-depth: 0
152
+
153
+ - uses: actions/setup-python@v5
154
+ with:
155
+ python-version: "3.11"
156
+
157
+ - name: Install uv
158
+ uses: astral-sh/setup-uv@v3
159
+
160
+ - name: Sync deps (locked)
161
+ run: uv sync --frozen
162
+
163
+ - name: Quality gates
164
+ run: |
165
+ uv run ruff format --check .
166
+ uv run ruff check .
167
+ uv run pyright
168
+ uv run pytest -q
169
+
170
+ - name: Extract version from tag
171
+ id: tag
172
+ run: echo "version=${GITHUB_REF_NAME#v}" >> $GITHUB_OUTPUT
173
+
174
+ - name: Verify pyproject version matches tag
175
+ run: |
176
+ uv run python - <<'PY'
177
+ import tomllib
178
+ v=tomllib.load(open("pyproject.toml","rb"))["project"]["version"]
179
+ tv="${{ steps.tag.outputs.version }}"
180
+ assert v==tv, f"pyproject version {v} != tag {tv}"
181
+ PY
182
+
183
+ - name: Ensure Unreleased is empty
184
+ run: uv run python scripts/assert_unreleased_empty.py
185
+
186
+ - name: Extract release notes from CHANGELOG.md
187
+ run: uv run python scripts/extract_release_notes.py "${{ steps.tag.outputs.version }}"
188
+
189
+ - name: Build (sdist + wheel)
190
+ run: uv run python -m build
191
+
192
+ - name: Publish to PyPI (Trusted Publishing)
193
+ uses: pypa/gh-action-pypi-publish@release/v1
194
+ with:
195
+ packages-dir: dist/
196
+
197
+ - name: Create GitHub Release + upload artifacts
198
+ env:
199
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
200
+ run: |
201
+ gh release create "${GITHUB_REF_NAME}" \
202
+ --title "${GITHUB_REF_NAME}" \
203
+ --notes-file RELEASE_NOTES.md \
204
+ dist/*
205
+ ```
206
+
207
+ ---
208
+
209
+ ## PyPI publishing (recommended: Trusted Publishing)
210
+
211
+ ### Setup (one-time)
212
+
213
+ 1. Create the project on PyPI (or publish once manually).
214
+ 2. In PyPI project settings, add a **Trusted Publisher**:
215
+
216
+ * Provider: GitHub
217
+ * Repo: your org/repo
218
+ * Workflow: `.github/workflows/release.yml`
219
+ 3. Ensure GitHub Actions has:
220
+
221
+ * `permissions: id-token: write`
222
+
223
+ ### Fallback (token-based)
224
+
225
+ If Trusted Publishing is not possible:
226
+
227
+ * Store `PYPI_API_TOKEN` as a GitHub secret.
228
+ * Replace publish step:
229
+
230
+ ```yaml
231
+ - uses: pypa/gh-action-pypi-publish@release/v1
232
+ with:
233
+ password: ${{ secrets.PYPI_API_TOKEN }}
234
+ ```
235
+
236
+ ---
237
+
238
+ ## Hardening checklist (recommended for serious OSS)
239
+
240
+ ### Reproducibility
241
+
242
+ * Commit `uv.lock`.
243
+ * Use `uv sync --frozen` in CI.
244
+ * Build artifacts only after tests pass.
245
+
246
+ ### Security
247
+
248
+ * Add dependency audit on tag builds (or every PR):
249
+
250
+ * `uv run pip-audit`
251
+ * Add secret scanning:
252
+
253
+ * `gitleaks` or `detect-secrets` in pre-commit + CI
254
+
255
+ ### Release integrity
256
+
257
+ * Fail if:
258
+
259
+ * tag version != pyproject version
260
+ * changelog section missing
261
+ * Unreleased is non-empty
262
+ * tests/lint/typecheck fail
263
+ * build fails
264
+
265
+ ---
266
+
267
+ ## Expected conventions
268
+
269
+ * Tags are `vX.Y.Z`.
270
+ * Changelog headers are `## [X.Y.Z] - YYYY-MM-DD`.
271
+ * `CHANGELOG.md` is authoritative release notes source.
272
+ * CI is authoritative publisher and release creator.