patchcraft 0.5.0__tar.gz → 0.5.2__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 (83) hide show
  1. {patchcraft-0.5.0 → patchcraft-0.5.2}/.gitignore +4 -0
  2. {patchcraft-0.5.0 → patchcraft-0.5.2}/CHANGELOG.md +201 -3
  3. patchcraft-0.5.2/CONTRIBUTING.md +326 -0
  4. patchcraft-0.5.2/MANIFEST.in +39 -0
  5. {patchcraft-0.5.0 → patchcraft-0.5.2}/PKG-INFO +31 -30
  6. {patchcraft-0.5.0 → patchcraft-0.5.2}/README.md +14 -13
  7. {patchcraft-0.5.0 → patchcraft-0.5.2}/README.pt-BR.md +14 -13
  8. {patchcraft-0.5.0 → patchcraft-0.5.2}/README.pypi.md +15 -13
  9. patchcraft-0.5.2/accel/Cargo.lock +232 -0
  10. patchcraft-0.5.2/accel/Cargo.toml +32 -0
  11. patchcraft-0.5.2/accel/README.md +48 -0
  12. patchcraft-0.5.2/accel/src/kernel.rs +252 -0
  13. patchcraft-0.5.2/accel/src/lib.rs +88 -0
  14. {patchcraft-0.5.0 → patchcraft-0.5.2}/docs/ADR/0003-reversibility-classes.md +1 -0
  15. patchcraft-0.5.2/docs/ADR/0004-precision-and-effort-parameters.md +227 -0
  16. {patchcraft-0.5.0 → patchcraft-0.5.2}/docs/AUXILIARY.md +11 -6
  17. {patchcraft-0.5.0 → patchcraft-0.5.2}/docs/GUIDE.md +34 -23
  18. patchcraft-0.5.2/docs/PERFORMANCE.md +116 -0
  19. {patchcraft-0.5.0 → patchcraft-0.5.2}/docs/ROADMAP.md +2 -1
  20. {patchcraft-0.5.0 → patchcraft-0.5.2}/docs/SCOPE.md +9 -6
  21. {patchcraft-0.5.0 → patchcraft-0.5.2}/docs/THEORY.md +5 -4
  22. patchcraft-0.5.2/pyproject.toml +130 -0
  23. patchcraft-0.5.2/setup.cfg +4 -0
  24. patchcraft-0.5.2/setup.py +81 -0
  25. {patchcraft-0.5.0 → patchcraft-0.5.2}/src/patchcraft/__init__.py +15 -1
  26. {patchcraft-0.5.0 → patchcraft-0.5.2}/src/patchcraft/_accel.py +12 -11
  27. patchcraft-0.5.2/src/patchcraft/_version.py +24 -0
  28. {patchcraft-0.5.0 → patchcraft-0.5.2}/src/patchcraft/reconstruct.py +9 -2
  29. {patchcraft-0.5.0 → patchcraft-0.5.2}/src/patchcraft/resize.py +12 -3
  30. {patchcraft-0.5.0 → patchcraft-0.5.2}/src/patchcraft/stitch.py +10 -4
  31. patchcraft-0.5.2/src/patchcraft.egg-info/PKG-INFO +188 -0
  32. patchcraft-0.5.2/src/patchcraft.egg-info/SOURCES.txt +74 -0
  33. patchcraft-0.5.2/src/patchcraft.egg-info/dependency_links.txt +1 -0
  34. patchcraft-0.5.2/src/patchcraft.egg-info/requires.txt +13 -0
  35. patchcraft-0.5.2/src/patchcraft.egg-info/scm_file_list.json +83 -0
  36. patchcraft-0.5.2/src/patchcraft.egg-info/scm_version.json +8 -0
  37. patchcraft-0.5.2/src/patchcraft.egg-info/top_level.txt +1 -0
  38. {patchcraft-0.5.0 → patchcraft-0.5.2}/tests/test_accel.py +10 -10
  39. {patchcraft-0.5.0 → patchcraft-0.5.2}/tests/test_exactness.py +1 -1
  40. {patchcraft-0.5.0 → patchcraft-0.5.2}/tests/test_import.py +1 -1
  41. {patchcraft-0.5.0 → patchcraft-0.5.2}/tests/test_reconstruct.py +21 -0
  42. {patchcraft-0.5.0 → patchcraft-0.5.2}/tests/test_resize.py +35 -0
  43. {patchcraft-0.5.0 → patchcraft-0.5.2}/tests/test_stitch.py +35 -0
  44. patchcraft-0.5.2/tools/benchmark.py +165 -0
  45. patchcraft-0.5.2/tools/check_dist.py +209 -0
  46. {patchcraft-0.5.0 → patchcraft-0.5.2}/uv.lock +26 -10
  47. patchcraft-0.5.0/.github/workflows/release.yml +0 -203
  48. patchcraft-0.5.0/.github/workflows/test.yml +0 -93
  49. patchcraft-0.5.0/CONTRIBUTING.md +0 -161
  50. patchcraft-0.5.0/docs/superpowers/plans/2026-09-01-fase1-bugs-performance.md +0 -1179
  51. patchcraft-0.5.0/docs/superpowers/plans/2026-09-01-fase2-acel.md +0 -1643
  52. patchcraft-0.5.0/docs/superpowers/plans/2026-09-01-fase3-g1-predicado.md +0 -1705
  53. patchcraft-0.5.0/pyproject.toml +0 -112
  54. {patchcraft-0.5.0 → patchcraft-0.5.2}/.python-version +0 -0
  55. {patchcraft-0.5.0 → patchcraft-0.5.2}/LICENSE +0 -0
  56. {patchcraft-0.5.0 → patchcraft-0.5.2}/docs/ADR/0001-patch-extraction-api.md +0 -0
  57. {patchcraft-0.5.0 → patchcraft-0.5.2}/docs/ADR/0002-patchify-transform.md +0 -0
  58. {patchcraft-0.5.0 → patchcraft-0.5.2}/docs/FOCO-1.0.md +0 -0
  59. {patchcraft-0.5.0 → patchcraft-0.5.2}/docs/STUDIES/2026-08-04-patch-techniques.md +0 -0
  60. {patchcraft-0.5.0 → patchcraft-0.5.2}/docs/USAGE.md +0 -0
  61. /patchcraft-0.5.0/docs/superpowers/specs/2026-09-01-fase1-bugs-performance-design.md → /patchcraft-0.5.2/docs/design/2026-09-01-fase1-bugs-performance.md +0 -0
  62. /patchcraft-0.5.0/docs/superpowers/specs/2026-09-01-fase2-acel-design.md → /patchcraft-0.5.2/docs/design/2026-09-01-fase2-acelerador.md +0 -0
  63. /patchcraft-0.5.0/docs/superpowers/specs/2026-09-01-fase3-g1-predicado-design.md → /patchcraft-0.5.2/docs/design/2026-09-01-fase3-predicado.md +0 -0
  64. {patchcraft-0.5.0 → patchcraft-0.5.2}/src/patchcraft/_foldgeom.py +0 -0
  65. {patchcraft-0.5.0 → patchcraft-0.5.2}/src/patchcraft/cache.py +0 -0
  66. {patchcraft-0.5.0 → patchcraft-0.5.2}/src/patchcraft/extract.py +0 -0
  67. {patchcraft-0.5.0 → patchcraft-0.5.2}/src/patchcraft/geometry.py +0 -0
  68. {patchcraft-0.5.0 → patchcraft-0.5.2}/src/patchcraft/metrics.py +0 -0
  69. {patchcraft-0.5.0 → patchcraft-0.5.2}/src/patchcraft/pair.py +0 -0
  70. {patchcraft-0.5.0 → patchcraft-0.5.2}/src/patchcraft/py.typed +0 -0
  71. {patchcraft-0.5.0 → patchcraft-0.5.2}/tests/__init__.py +0 -0
  72. {patchcraft-0.5.0 → patchcraft-0.5.2}/tests/_datasets.py +0 -0
  73. {patchcraft-0.5.0 → patchcraft-0.5.2}/tests/_rng.py +0 -0
  74. {patchcraft-0.5.0 → patchcraft-0.5.2}/tests/conftest.py +0 -0
  75. {patchcraft-0.5.0 → patchcraft-0.5.2}/tests/test_cache.py +0 -0
  76. {patchcraft-0.5.0 → patchcraft-0.5.2}/tests/test_datasets_helper.py +0 -0
  77. {patchcraft-0.5.0 → patchcraft-0.5.2}/tests/test_extract.py +0 -0
  78. {patchcraft-0.5.0 → patchcraft-0.5.2}/tests/test_geometry.py +0 -0
  79. {patchcraft-0.5.0 → patchcraft-0.5.2}/tests/test_metrics.py +0 -0
  80. {patchcraft-0.5.0 → patchcraft-0.5.2}/tests/test_pair.py +0 -0
  81. {patchcraft-0.5.0 → patchcraft-0.5.2}/tests/test_public_api.py +0 -0
  82. {patchcraft-0.5.0 → patchcraft-0.5.2}/tests/test_reference.py +0 -0
  83. {patchcraft-0.5.0 → patchcraft-0.5.2}/tests/test_rng.py +0 -0
@@ -3,6 +3,10 @@ __pycache__/
3
3
  *.py[cod]
4
4
  *$py.class
5
5
  *.so
6
+ # Built in place by an editable install (setuptools-rust).
7
+ src/patchcraft/_accel_native*
8
+ # Generated at build time by setuptools-scm.
9
+ src/patchcraft/_version.py
6
10
 
7
11
  # Distribution / packaging
8
12
  build/
@@ -6,6 +6,200 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.5.2] - 2026-09-03
10
+
11
+ ### Fixed
12
+
13
+ - **`stitch(weight="hann")` corrupted the corner band of every patch of 99 or
14
+ more.** The denominator was floored by an absolute `clamp(min=1e-6)` whose
15
+ comment called it a defensive no-op. It was not: the 2-D hann corner weight
16
+ is `(pi/(n+1))**4`, which crosses below `1e-6` at patch 99, so the corner
17
+ band was divided by the floor rather than by the real weight. Measured at
18
+ 640x640, patch 256, stride 128: maximum error 0.94 on data in [0, 1] and 960
19
+ wrong pixels, identical in float64 because the constant and not the precision
20
+ was the cause. The floor is now `finfo(dtype).tiny`, the model already used
21
+ in `metrics.py`, which never reaches a legitimate weight: the smallest 2-D
22
+ hann corner even at patch 1024 is 8.8e-11. THEORY 9.9 claimed no covered
23
+ pixel is zeroed by the window, which was true of the window and false of the
24
+ result.
25
+ - **`reconstruct` returned a view aliasing the caller's patches on a
26
+ single-patch grid.** Writing to the patches afterwards changed an image the
27
+ function had already returned, measured as 0.744 becoming 99.0. `extract`
28
+ guards the mirror image of this case and `reconstruct` did not.
29
+ - **`resize(..., backend="pil")` raised a raw `RuntimeError` on `int8`.** The
30
+ inbound `clamp(0, 255)` was applied in the input dtype, where the bound 255
31
+ is unrepresentable, so torch raised `value cannot be converted to type
32
+ int8_t without overflow` before clamping anything. Every integer dtype now
33
+ works on both backends, and THEORY 9.4 gained the row for integer tensors on
34
+ the PIL backend, which it had never specified.
35
+
36
+
37
+ ### Removed
38
+
39
+ - The agentic workflow's leftovers. `.superpowers/` at the repository root
40
+ held 62 files of review diffs and task-progress notes, hidden from
41
+ `git status` by a nested `.gitignore` containing `*`, so it sat in the tree
42
+ unseen and reached the 0.5.0 sdist. The three implementation plans under
43
+ `docs/superpowers/plans/` were 4527 lines of checkbox task lists addressed
44
+ "For agentic workers", and they shipped to PyPI inside every sdist.
45
+
46
+ ### Added
47
+
48
+ - `docs/ADR/0004-precision-and-effort-parameters.md`, in `Proposed` status: names the
49
+ precision policy the library already applies in five places, and designs an `effort=`
50
+ preset under the constraint that it may change speed and memory but never a returned
51
+ value. Nothing is implemented; the ADR records the design, the effort per step and the
52
+ order. It amends ADR 0003 on one measured point, that the exactness predicate is a
53
+ property of the geometry and the accumulator rather than of the geometry alone.
54
+ - `outreach/`, material for presenting the project publicly, in the shape the
55
+ sibling projects use: a dated news source at the root and one subfolder per
56
+ channel, with the rule that no channel text changes before the source does.
57
+ It publishes no new measurement, and it is pruned from the sdist.
58
+
59
+ ### Changed
60
+
61
+ - The three design specs that were under `docs/superpowers/specs/` moved to
62
+ `docs/design/`. They are the record of what was measured before each phase
63
+ was built, including the spike table that rejected `conv_transpose2d` and
64
+ `index_add_` before the Rust kernel was written, and Amendment A, which
65
+ `tests/test_exactness.py` cites for the per-pixel error bound.
66
+ - The sdist is 264 KiB where it was 316 KiB.
67
+ - Nothing ephemeral is kept in the project folder any more. `accel/target`
68
+ had reached 269 MiB of Rust build output, and `dist/`, `.pytest_cache` and
69
+ `src/patchcraft.egg-info` were accumulating alongside it. The caches are
70
+ now redirected by user-level environment variables, joining the ones this
71
+ machine already had for uv, ruff, mypy, pip and `__pycache__`, and
72
+ `CONTRIBUTING.md` records the convention and why it is not committed here.
73
+ - `.vscode/settings.json` stays tracked and is pruned from the sdist instead.
74
+ - **The versioning rule was self-contradictory and is corrected.** One row said
75
+ an output value change is a `0.Y.0`, the next said a fix is a `0.y.Z`, and a
76
+ bug fix satisfies both, so the table made every fix a minor and left patch
77
+ releases unable to fix anything. It also contradicted the project's own
78
+ history: `0.2.1` changed `stitch(weight="hann")` output for every geometry
79
+ and shipped as a patch. The rule now turns on whether the contract moved or
80
+ the implementation stopped violating it, which sorts `0.2.1`, `0.3.0`,
81
+ `0.5.0` and `0.5.2` correctly. `CONTRIBUTING.md` carries the rule with those
82
+ four worked, and GUIDE section 8 item 6 no longer cites a patch release as
83
+ an example of what only a minor may do.
84
+ Its `${workspaceFolder}/.venv` path is not broken: `.venv` is a directory
85
+ junction to the real environment outside the tree, so the setting resolves
86
+ and is portable to any checkout following the same convention.
87
+
88
+ ## [0.5.1] - 2026-09-02
89
+
90
+ > Numbered `0.6.0` while it was being built. Corrected before release: the
91
+ > middle digit is the compatibility boundary, and this release changes no
92
+ > signature, no name and no output value. See the versioning policy and its
93
+ > errata in [`CONTRIBUTING.md`](CONTRIBUTING.md).
94
+
95
+ ### Changed
96
+
97
+ - **The native accelerator ships inside this wheel.** It was going to be a
98
+ second PyPI project, `patchcraft-accel`, reached through
99
+ `pip install patchcraft[accel]`. That project was never published and now
100
+ never will be. A release produces one sdist and six wheels for the single
101
+ `patchcraft` project: five tagged `cp312-abi3-<platform>` with the Rust
102
+ kernel compiled in as `patchcraft._accel_native`, and one `py3-none-any`
103
+ for every other platform. Installers prefer the most specific compatible
104
+ tag, so `pip install patchcraft` picks up the accelerator where one exists
105
+ and the pure wheel everywhere else, with no extra to remember. The pattern
106
+ and its universal fallback follow `coverage`, which publishes the same two
107
+ shapes for one version.
108
+ - The `accel` extra is removed. Nothing depended on it, because it never
109
+ reached the index.
110
+ - The accelerator is now on by default where a wheel carries it, rather than
111
+ opt-in. The two paths are bit-identical by test, and `PATCHCRAFT_ACCEL=0`
112
+ still forces the pure one at runtime.
113
+ - Build backend moves from hatchling to setuptools with setuptools-rust, which
114
+ is what makes one project able to emit both shapes of wheel. `setup.py`
115
+ holds the single decision, driven by `PATCHCRAFT_PURE_PYTHON` and
116
+ `PATCHCRAFT_REQUIRE_EXTENSION`; everything else stays declarative in
117
+ `pyproject.toml`. Installing from the sdist compiles the extension when a
118
+ Rust toolchain is present and degrades to a pure install when it is not.
119
+ - License metadata follows PEP 639: the SPDX expression `MIT` with
120
+ `license-files`, and the superseded `License :: OSI Approved` classifier is
121
+ gone. setuptools rejects carrying both.
122
+
123
+ ### Added
124
+
125
+ - **The version comes from the git tag.** `setuptools-scm` derives it at build
126
+ time, so no file in the source states a version, the release procedure loses
127
+ its bump step, and a tag and a release can no longer disagree.
128
+ `patchcraft.__version__` still works: the build writes a generated
129
+ `_version.py` that `__init__.py` imports, which costs nothing at import time,
130
+ with `importlib.metadata` as a fallback for a tree that was never built.
131
+ Measured before choosing: `importlib.metadata` alone was the wrong default,
132
+ because it took 56 ms on first call and reported a stale version in an
133
+ editable install.
134
+ - `docs/PERFORMANCE.md`, which is where the accelerator's measurements live.
135
+ It carries the test machine, the versions, the date, the reproduction
136
+ command and what the numbers do not say, and its table is pasted from
137
+ `tools/benchmark.py --markdown` rather than retyped.
138
+ - The Rust kernel is now always built in release mode, including in an
139
+ editable install. It was following the build command, so a development
140
+ checkout compiled a debug kernel: measured here, that turned a 14x speedup
141
+ into 2.1x, which made local benchmarks meaningless and made the accelerated
142
+ CI job exercise code no user runs.
143
+ - `patchcraft.__version__` is re-exported explicitly, so `mypy --strict` in a
144
+ consuming project still accepts it. Moving it out of a literal and into an
145
+ import had quietly made it an implicit re-export, which strict mode rejects.
146
+ - `tools/benchmark.py` measures the accelerated fold against the pure one and
147
+ refuses to report a timing unless the two results are bit-identical. The
148
+ numbers in the documentation come from it, and it is the reproduction
149
+ command they cite.
150
+ - Every build checkout uses `fetch-depth: 0`. The default shallow clone hides
151
+ the tags, and setuptools-scm's fallback for that is a silent
152
+ `0.1.dev1+g<sha>`, a version that walks backwards rather than an error.
153
+ - `tools/check_dist.py` grew `--expect-version` and `--require-universal`, and
154
+ it now rejects any artifact carrying a local version segment. PyPI refuses
155
+ those on upload, so this turns a late and confusing server-side rejection
156
+ into an early local one. It also fixes a bug in the job that checks the
157
+ native wheels, which demanded a universal wheel that job never has.
158
+ - Linux aarch64 wheels, which the previous four-target matrix did not cover.
159
+ - `tools/check_dist.py`, run before every upload. It fails the release if a
160
+ platform wheel lost its extension, which `optional=True` would otherwise
161
+ let through as a silently slow wheel, or if the universal wheel gained one.
162
+ - The versioning policy now names the resolver rule it follows, which is that
163
+ `0.y` is the compatibility boundary in Cargo, npm and PEP 440 alike, and it
164
+ carries an errata for the two releases numbered before the rule was written.
165
+ - Version numbers are gone from the documentation surface. The three cover
166
+ pages carry none at all, since the PyPI badge already shows the current
167
+ release, and the manual keeps only the one the citation entry needs. The
168
+ release archaeology those pages used to recite lives here, and the rest is
169
+ in the git history.
170
+ - `MANIFEST.in`, which puts `accel/` in the sdist so the source path can build
171
+ the extension, and keeps `lab/`, `.superpowers/` and cargo's `target/` out.
172
+
173
+ - **Corrected: the hand-rolled and library tile-and-blend results are not
174
+ bit-identical.** The three cover pages and GUIDE section 1 claimed they were,
175
+ and the GUIDE block asserted it. Re-measured: they agree to 1.17e-05 on a
176
+ value in [0, 1], differing on 24500 of 49152 elements. The cause is the 0.3.0
177
+ separable denominator, where `stitch` sums two 1-D window folds while the
178
+ hand-rolled twin folds a replicated 2-D kernel. `stitch(weight="uniform")`
179
+ is still bit-identical to `reconstruct`. Nothing about the round-trip
180
+ contract changed; the documentation was simply asserting more than the code
181
+ delivered.
182
+ - Documentation corrections found by auditing every page against the code:
183
+ the changelog's reference links did not define the two most recent versions;
184
+ `docs/AUXILIARY.md` listed wheel contents that predate three releases;
185
+ `docs/SCOPE.md` still promised "pure-Python single-threaded", which the
186
+ rayon-parallel kernel contradicts; `docs/ROADMAP.md` still named hatchling;
187
+ GUIDE said the library "is not faster", that the no-zstandard branch is
188
+ untested, and that CI is six cells; the cover pages pointed at `USAGE.md` as
189
+ a walkthrough of 20 symbols when it covers 18 and says so. `CONTRIBUTING.md`
190
+ told a contributor to run `pytest` after a `uv sync` that does not install it.
191
+
192
+ ### Notes
193
+
194
+ - No signature, no name and no output value changed. The public surface is the
195
+ same 20 names, still pinned by `tests/test_public_api.py`.
196
+ - Measured on torch 2.14, CPU, mean over the dev machine: `reconstruct` at
197
+ 3x512x512 p=32 s=16 goes 11.4 ms to 2.5 ms (4.5x), at 3x2048x2048 p=64 s=32
198
+ 477.9 ms to 31.8 ms (15.0x); `stitch[hann]` 21.6 ms to 7.0 ms (3.1x) and
199
+ 488.7 ms to 38.8 ms (12.6x). The gain is algorithmic rather than a thread
200
+ count: the pure `F.fold` measured between 365 and 465 ms on that largest
201
+ case at 4, 8, 16 and 36 torch threads.
202
+
9
203
  ## [0.5.0] - 2026-09-02
10
204
 
11
205
  ### Added
@@ -472,9 +666,13 @@ First public release. Public API stable; signatures will only change in 1.x.
472
666
  - [`README.md`](README.md) covers installation, the car-vs-track metaphor,
473
667
  validation lab.
474
668
 
475
- [Unreleased]: https://github.com/LeoPR/PatchCraft/compare/v0.4.0...HEAD
476
- [0.4.0]: https://github.com/LeoPR/PatchCraft/releases/tag/v0.4.0
477
- [0.3.0]: https://github.com/LeoPR/PatchCraft/releases/tag/v0.3.0
669
+ [Unreleased]: https://github.com/LeoPR/PatchCraft/compare/v0.5.2...HEAD
670
+ [0.5.2]: https://github.com/LeoPR/PatchCraft/releases/tag/v0.5.2
671
+ [0.5.1]: https://github.com/LeoPR/PatchCraft/releases/tag/v0.5.1
672
+ [0.5.0]: https://github.com/LeoPR/PatchCraft/releases/tag/v0.5.0
673
+
674
+ <!-- 0.4.0 and 0.3.0 are deliberately unlinked: they are source milestones that
675
+ were never tagged and never released, as their own entries say. -->
478
676
  [0.2.2]: https://github.com/LeoPR/PatchCraft/releases/tag/v0.2.2
479
677
  [0.2.1]: https://github.com/LeoPR/PatchCraft/releases/tag/v0.2.1
480
678
  [0.2.0]: https://github.com/LeoPR/PatchCraft/releases/tag/v0.2.0
@@ -0,0 +1,326 @@
1
+ # Contributing to PatchCraft
2
+
3
+ If you've cloned this repo and want to run the test suite, understand how the project is laid out, or follow the validation conventions, this is the page. For installation and usage, see the [README](README.md).
4
+
5
+ ---
6
+
7
+ ## Run tests
8
+
9
+ The test runner and the linters live in the `dev` extra, so a plain `uv sync`
10
+ does not install them:
11
+
12
+ ```
13
+ uv sync --locked --extra cache --extra dev
14
+ ```
15
+
16
+ ```
17
+ pytest
18
+ pytest -m "not gpu" # skip GPU-requiring tests
19
+ ```
20
+
21
+ CI runs the full suite on every push and PR. See [`.github/workflows/test.yml`](.github/workflows/test.yml). Matrix is `{ubuntu-latest, windows-latest} × {python 3.12, 3.13, 3.14}` for the pure path, plus a second job on both operating systems for the accelerated one. To run the same checks locally before pushing:
22
+
23
+ ```
24
+ ruff check src tests
25
+ mypy --strict src
26
+ pytest -m "not gpu"
27
+ ```
28
+
29
+ ---
30
+
31
+ ## The two paths
32
+
33
+ The overlapping fold in `reconstruct` and `stitch` has a Rust implementation
34
+ in [`accel/`](accel/), compiled into the wheel on the platforms CI builds for.
35
+ Everything else runs the same operation in torch. The two are bit-identical by
36
+ test, so a contribution has to keep both green rather than pick one.
37
+
38
+ Which one your checkout runs depends on whether the extension got built:
39
+
40
+ ```
41
+ python -c "import patchcraft; print(patchcraft.accel_available())"
42
+ ```
43
+
44
+ `uv sync --extra cache --extra dev` builds it when a Rust toolchain is on
45
+ PATH, always in release mode. Two environment variables override that, and CI
46
+ sets both explicitly rather than relying on what happens to be installed:
47
+
48
+ | Variable | Effect |
49
+ |---|---|
50
+ | `PATCHCRAFT_PURE_PYTHON=1` | Skip the extension. How the universal wheel is built. |
51
+ | `PATCHCRAFT_REQUIRE_EXTENSION=1` | Make a Rust build failure fatal instead of degrading to the pure path. |
52
+ | `PATCHCRAFT_ACCEL=0` | Runtime only: ignore an extension that is already built. |
53
+
54
+ The Rust kernel has its own tests, which need no Python at all:
55
+
56
+ ```
57
+ cargo test --manifest-path accel/Cargo.toml
58
+ ```
59
+
60
+ One local wrinkle worth knowing. If your checkout sits under a path with
61
+ non-ASCII characters, `setuptools-rust` mis-decodes the artifact path that
62
+ cargo reports and the copy step fails. Point the build at an ASCII directory
63
+ and it works:
64
+
65
+ ```
66
+ CARGO_TARGET_DIR=/tmp/pc-target uv sync --extra cache --extra dev
67
+ ```
68
+
69
+ ---
70
+
71
+ ## Nothing ephemeral lives in the project folder
72
+
73
+ Caches, build output and virtual environments are kept off the tree, because
74
+ they are large, regenerable and machine-specific. They are redirected by
75
+ user-level environment variables rather than by anything committed here: a
76
+ machine path in `pyproject.toml` would be wrong for every other machine and
77
+ for CI, which is why one was removed in 0.3.0.
78
+
79
+ | Variable | Sends |
80
+ |---|---|
81
+ | `CARGO_TARGET_DIR` | the Rust build output, which reached 269 MiB inside `accel/target` before it was redirected |
82
+ | `PYTEST_ADDOPTS=-o cache_dir=...` | `.pytest_cache` |
83
+ | `RUFF_CACHE_DIR`, `MYPY_CACHE_DIR` | the linter caches |
84
+ | `UV_CACHE_DIR`, `PIP_CACHE_DIR` | downloaded wheels |
85
+ | `PYTHONPYCACHEPREFIX` | `__pycache__` |
86
+
87
+ The virtual environment lives outside the tree too; see
88
+ [`docs/AUXILIARY.md`](docs/AUXILIARY.md).
89
+
90
+ Two directories still appear during a build and are gitignored: `build/` and
91
+ `src/patchcraft.egg-info/`, both setuptools working state. They exist only
92
+ while you build, and redirecting them would need a machine path in a tracked
93
+ file, which is the thing this section is avoiding.
94
+
95
+ Three files are written into `src/patchcraft/` by an editable install and
96
+ are gitignored: the compiled `_accel_native`, the generated `_version.py`, and
97
+ `__pycache__`. Those have to be there for the package to import.
98
+
99
+ ---
100
+
101
+ ## Layout
102
+
103
+ ```
104
+ PatchCraft/
105
+ ├── pyproject.toml package metadata, build backend, cibuildwheel targets
106
+ ├── setup.py the one build decision: with or without the Rust extension
107
+ ├── tools/check_dist.py gate: extension present, versions agree, tag respected
108
+ ├── tools/benchmark.py accelerated vs pure, and proves they agree first
109
+ ├── MANIFEST.in what the sdist carries
110
+ ├── README.md the call page, canonical, English
111
+ ├── README.pt-BR.md the same call page in Portuguese
112
+ ├── README.pypi.md the PyPI page (long_description), links absolute
113
+ ├── CONTRIBUTING.md this file
114
+ ├── CHANGELOG.md Keep-a-Changelog format
115
+ ├── LICENSE MIT
116
+ ├── .python-version 3.13
117
+ ├── .gitignore ignores archive/, venvs, caches, outputs
118
+ ├── .github/workflows/
119
+ │ ├── test.yml matrix CI on PRs/main, plus the accelerated job
120
+ │ └── release.yml publishes to PyPI on vX.Y.Z tag push (Trusted Publishing)
121
+ ├── src/patchcraft/ library core, one-image-at-a-time primitives
122
+ │ ├── __init__.py re-exports the full public API and the generated version
123
+ │ ├── extract.py patches via strided view or F.unfold; Patchify (ADR 0002)
124
+ │ ├── reconstruct.py inverse via F.fold + count map, closed-form fast path
125
+ │ ├── stitch.py weighted reassembly for modified patches
126
+ │ ├── geometry.py pre-flight: num_patches, tilings, scale_factor, paired_tilings
127
+ │ ├── metrics.py patch_metrics, per_patch_mse, per_patch_psnr
128
+ │ ├── pair.py LR↔HR pairing; PatchPair, PatchMeta
129
+ │ ├── resize.py resize with PIL or torch backends
130
+ │ ├── cache.py content-addressed disk cache
131
+ │ ├── _accel.py bridge to the optional native accelerator, falls back silently
132
+ │ └── _foldgeom.py shared fold-geometry validation for reconstruct and stitch
133
+ ├── accel/ the Rust crate compiled into this wheel (pyo3, setuptools-rust)
134
+ │ ├── Cargo.toml crate metadata; the version there is internal
135
+ │ ├── src/kernel.rs the gather-fold kernel, pure Rust, cargo-tested
136
+ │ ├── src/lib.rs pyo3 glue, exports patchcraft._accel_native
137
+ │ └── README.md how to build it, and how to check it is active
138
+ ├── tests/ pytest suite (contract tests for src/)
139
+ │ ├── test_extract.py extract + Patchify
140
+ │ ├── test_reconstruct.py
141
+ │ ├── test_stitch.py
142
+ │ ├── test_geometry.py num_patches + tilings + scale_factor + paired_tilings
143
+ │ ├── test_metrics.py
144
+ │ ├── test_pair.py
145
+ │ ├── test_resize.py
146
+ │ ├── test_cache.py
147
+ │ ├── test_exactness.py falsifies the count-map predicate over the legal space
148
+ │ ├── test_reference.py naive loop-based reference for the fast paths
149
+ │ ├── test_public_api.py freezes the 20 names, their signatures and carrier fields
150
+ │ ├── test_accel.py native path, skipped unless the extension was built
151
+ │ ├── test_datasets_helper.py label_subset
152
+ │ ├── test_import.py
153
+ │ ├── _rng.py audited round-trip helpers (data generation, bit equality)
154
+ │ ├── test_rng.py tests for those helpers
155
+ │ └── _datasets.py dev-only fixtures (MNIST, etc), NOT public API
156
+ ├── lab/ ephemeral experiments; see lab/README.md
157
+ │ ├── README.md bench rules (tracked; the scripts themselves are not)
158
+ │ └── .gitignore ignores everything else (tracked)
159
+ ├── docs/
160
+ │ ├── GUIDE.md the manual, every README claim measured with its output
161
+ │ ├── PERFORMANCE.md what the accelerator is worth, and how to re-measure
162
+ │ ├── USAGE.md REPL walkthrough of every public symbol (behind, see B4)
163
+ │ ├── SCOPE.md responsibilities matrix + parallelization analysis
164
+ │ ├── AUXILIARY.md tests/_datasets, lab/, Z:\ conventions (NOT part of the wheel)
165
+ │ ├── THEORY.md distilled design + §9 condition contract; §0 binding scope
166
+ │ ├── ROADMAP.md milestone plan
167
+ │ ├── FOCO-1.0.md what 1.0 freezes and the blockers in the way
168
+ │ ├── ADR/ one file per decision that shaped the API
169
+ │ ├── STUDIES/ background reading behind those decisions
170
+ │ └── design/ one spec per work phase: the alternatives measured and the decision
171
+ ├── outreach/ material for presenting the project publicly, not shipped
172
+ │ ├── README.md how it is organized and what to check before publishing
173
+ │ ├── 2026-09-03-lancamento.md the dated news source every channel text derives from
174
+ │ └── linkedin/ post (short) and artigo (long), each in PT and EN
175
+ └── archive/ reference-only; gitignored (pruned 2026-05-17, only HISTORY.md kept)
176
+ ```
177
+
178
+ ---
179
+
180
+ ## Validation lab
181
+
182
+ The library is "one image in, one tensor out" by design, but you only know it works once you run it end-to-end on real images. That happens in two places, neither of which is part of the shipped package:
183
+
184
+ - [`tests/`](tests/) holds the formal pytest suite that defines the contract from [`docs/THEORY.md`](docs/THEORY.md) §9.
185
+ - [`lab/`](lab/) holds ephemeral scripts and notebooks for fast hypothesis-checking. See [`lab/README.md`](lab/README.md) for the bench rules; outputs go to `Z:\outputs\patchcraft\` (off-tree).
186
+
187
+ Datasets used by tests/lab are downloaded lazily into `Z:\caches\datasets\<name>\` on first use; they do not ship with the package and are never bundled into the wheel.
188
+
189
+ The dev framework that makes this all work (fixtures, dataset helpers, `Z:\` conventions) is documented in [`docs/AUXILIARY.md`](docs/AUXILIARY.md).
190
+
191
+ ---
192
+
193
+ ## Versioning policy
194
+
195
+ The scheme is [Semantic Versioning 2.0.0](https://semver.org/spec/v2.0.0.html),
196
+ expressed in the [PEP 440](https://peps.python.org/pep-0440/) form that PyPI
197
+ accepts. Two rules matter more than the arithmetic, because both were broken
198
+ once and both cost work to undo.
199
+
200
+ **The number names a published artifact, not a unit of work.** Finishing a
201
+ phase, merging a branch and landing a refactor are not releases, and none of
202
+ them touches `__version__`. Everything between releases accumulates under
203
+ `[Unreleased]` in [`CHANGELOG.md`](CHANGELOG.md), and the number is assigned
204
+ in the release commit itself. Versions 0.3.0 and 0.4.0 came from the opposite
205
+ habit: each internal work phase minted a minor, so three numbers existed
206
+ before any of them reached PyPI.
207
+
208
+ **A published version is frozen.** PyPI refuses a filename it has already
209
+ seen, so a version that shipped can only be superseded, never edited. That is
210
+ also why a failed pipeline never costs a number: `skip-existing` makes the
211
+ upload a no-op and the same tag can be re-run.
212
+
213
+ **While the project is pre-1.0, the middle digit is the compatibility
214
+ boundary.** SemVer itself leaves `0.y.z` open, saying only that "anything MAY
215
+ change at any time", so the meaning comes from the resolvers instead, and they
216
+ agree: Cargo expands `^0.2.3` to `>=0.2.3, <0.3.0`, npm does the same, and
217
+ PEP 440's `~=0.5.0` means `>=0.5.0, <0.6.0`. In every one of them a `0.y` bump
218
+ is the announcement that something may break, and a `0.y.z` bump is the
219
+ promise that nothing does.
220
+
221
+ That fixes what each digit is for here:
222
+
223
+ The question that decides it is not "did a returned value change". A bug fix
224
+ changes returned values by definition, and if that forced a `0.Y.0` then a
225
+ patch release could never fix anything. The question is **whether the contract
226
+ moved, or whether the implementation stopped violating it**:
227
+
228
+ | Change | Bump | Why |
229
+ |---|---|---|
230
+ | The documented contract moves: a signature, a name, or behaviour the documentation described as correct | `0.Y.0` | Code that was right against the old documentation can now be wrong |
231
+ | The implementation stops violating the contract, however visibly | `0.y.Z` | The documentation always promised this; only the code was wrong |
232
+ | New functionality that breaks nothing | `0.y.Z` | Anyone pinned to the y-series should get it |
233
+ | Documentation, tests, CI, packaging or tooling only | `0.y.Z` | Still a release if it is published, still no break |
234
+ | Nothing published | nothing | Accumulate under `[Unreleased]` |
235
+
236
+ The project's own history sorts cleanly under that rule, and does not sort
237
+ under any rule keyed on whether values changed:
238
+
239
+ - `0.2.1` rewrote the hann window, changing `stitch(weight="hann")` output for
240
+ **every** geometry. THEORY §9.9 had promised no covered pixel is zeroed by
241
+ the window; the old window zeroed 108 of 144 pixels in one measured case. A
242
+ violation, so a **z**, and it shipped as one.
243
+ - `0.5.2` floored the `stitch` denominator by the dtype's tiny instead of an
244
+ absolute `1e-6`, changing hann output at patch 99 and above. The same
245
+ promise, the same kind of violation, so also a **z**.
246
+ - `0.3.0` moved `stitch` output by ULPs through a different summation order.
247
+ Nothing was wrong before, so behaviour the documentation endorsed changed.
248
+ A **y**.
249
+ - `0.5.0` made `tilings` return 73 specs where it returned 100. The
250
+ enumeration's output set is the contract. A **y**.
251
+
252
+ After 1.0 the ordinary reading applies: breaking is major, additive is minor,
253
+ fixed is patch. What 1.0 freezes is written in
254
+ [`docs/FOCO-1.0.md`](docs/FOCO-1.0.md).
255
+
256
+ ### Errata
257
+
258
+ The rule above was written after the fact, and two releases predate it.
259
+
260
+ `0.4.0` should have been `0.3.1`. It added `accel_available()` and nothing
261
+ else: no signature changed, and the accelerated path was bit-exact against the
262
+ pure one. Additive and non-breaking is a z-bump under the boundary rule, and
263
+ minting a y announced a break that never happened.
264
+
265
+ `0.5.1` was briefly numbered `0.6.0` before release. It changes only how the
266
+ package is built and shipped, so it is a z-bump.
267
+
268
+ Two others were right and are worth recording as the shape to copy. `0.3.0`
269
+ moved `stitch` output by ULPs through a different summation order, which is an
270
+ output change and belongs at a y boundary. `0.5.0` made `tilings` return 73
271
+ specs where it returned 100, which is squarely a break.
272
+
273
+ The accelerator in [`accel/`](accel/) has no version of its own to manage. It
274
+ is compiled into this wheel, so it ships when `patchcraft` ships. The version
275
+ in `accel/Cargo.toml` is internal and nothing reads it; what the Python side
276
+ actually checks is `_ABI_VERSION` in `accel/src/lib.rs`, and a mismatch there
277
+ means a silent fall back to the pure path rather than a crash.
278
+
279
+ ---
280
+
281
+ ## Where the version comes from
282
+
283
+ The git tag, and nowhere else. `setuptools-scm` derives it at build time, so
284
+ no file in the source states a version and there is no literal to forget.
285
+ `v0.5.1` produces `0.5.1`; the leading `v` is stripped for you.
286
+
287
+ At build time setuptools-scm writes `src/patchcraft/_version.py`, which
288
+ `__init__.py` imports and re-exports as `patchcraft.__version__`. That file is
289
+ generated and gitignored, so a checkout that was never built falls back to
290
+ `importlib.metadata` and then to `0+unknown`. The fallback exists so an
291
+ un-built tree still imports; it is not a supported way to learn the version.
292
+
293
+ Anything other than a clean tree sitting exactly on a tag produces a version
294
+ with a local segment, like `0.5.2.dev0+gb4edfd81.d20260902` from a dirty tree
295
+ or `0.1.dev1+gb4edfd81` from a checkout whose tags were never fetched. PyPI
296
+ refuses local segments outright, so such a build can never be published by
297
+ accident, and `tools/check_dist.py` rejects it before the upload rather than
298
+ letting the upload fail.
299
+
300
+ That last point is why every `actions/checkout` that builds uses
301
+ `fetch-depth: 0`. The default shallow clone hides the tags, and the failure is
302
+ silent: a version that walks backwards rather than an error.
303
+
304
+ ---
305
+
306
+ ## Releasing (maintainer only)
307
+
308
+ 1. Close the `[Unreleased]` section in [`CHANGELOG.md`](CHANGELOG.md) as `[X.Y.Z] YYYY-MM-DD`.
309
+ 2. Update the `version` field of the BibTeX entry in [`docs/GUIDE.md`](docs/GUIDE.md) section 9. It is the one version string left in the documentation, because a citation needs a concrete one.
310
+ 3. Update [`docs/ROADMAP.md`](docs/ROADMAP.md) milestone checkboxes.
311
+ 4. Commit: `release: vX.Y.Z`.
312
+ 5. Tag + push: `git tag -a vX.Y.Z -m "..."` then `git push origin vX.Y.Z`.
313
+ 6. `release.yml` fires automatically: validates, builds, publishes to PyPI via Trusted Publishing, then creates the GitHub Release with every artifact attached.
314
+
315
+ There is no version to bump in step 4, and no way for a tag and a release to
316
+ disagree. The `validate` job still cross-checks that setuptools-scm derived
317
+ exactly what the tag says, which catches a dirty or shallow checkout before
318
+ anything is built.
319
+
320
+ One release produces one sdist and six wheels: five `cp312-abi3-<platform>`
321
+ wheels with the Rust extension inside, and one `py3-none-any` wheel for
322
+ everywhere else. `tools/check_dist.py` runs before the upload and fails the
323
+ release if a platform wheel lost its extension, if the universal wheel gained
324
+ one, if the artifacts disagree about the version, or if any of them is not the
325
+ version the tag names. Publishing is a single project, so there is nothing to
326
+ register on PyPI beyond the publisher that already exists.
@@ -0,0 +1,39 @@
1
+ # The sdist is the reproducible source snapshot: the product (`src/`, `accel/`)
2
+ # plus the track that proves it works (`tests/`, `docs/`). `accel/` has to be
3
+ # here, because an sdist build compiles it when a Rust toolchain is present.
4
+ #
5
+ # The `prune` lines below are load-bearing rather than decorative, and the
6
+ # reason changed with setuptools-scm. Its file finder offers every git-tracked
7
+ # file to the sdist, so this file stopped being an allowlist and became a
8
+ # blocklist over everything git knows about. Measured: with no prune for it,
9
+ # `.vscode/settings.json` reached the sdist.
10
+ #
11
+ # Note that `lab/` is only *mostly* ignored: `lab/README.md` and
12
+ # `lab/.gitignore` are tracked, so the finder offers them and the prune is what
13
+ # stops them. The rest of `lab/` is scratch work that no commit can reproduce,
14
+ # and patchcraft-0.2.0.tar.gz shipped three such files to PyPI.
15
+ #
16
+ # Adding a tracked directory to this repository ships it to PyPI by default.
17
+ # `tools/check_dist.py` holds the matching allowlist and will fail the release
18
+ # for anything unexpected, which is the reminder to come back here.
19
+
20
+ include LICENSE
21
+ include CHANGELOG.md CONTRIBUTING.md
22
+ include README.md README.pt-BR.md README.pypi.md
23
+ include pyproject.toml setup.py uv.lock .python-version
24
+
25
+ recursive-include src *.py
26
+ include src/patchcraft/py.typed
27
+ recursive-include tests *.py
28
+ recursive-include docs *.md
29
+ recursive-include accel *.rs *.toml *.lock *.md
30
+ prune accel/target
31
+ recursive-include tools *.py
32
+
33
+ prune lab
34
+ prune archive
35
+ prune .github
36
+ prune .vscode
37
+ # Presentation material, not source. It is tracked, so the file finder offers
38
+ # it and only this line keeps it out.
39
+ prune outreach