patchcraft 0.5.3__tar.gz → 0.5.4__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 (90) hide show
  1. {patchcraft-0.5.3 → patchcraft-0.5.4}/AGENTS.md +24 -1
  2. {patchcraft-0.5.3 → patchcraft-0.5.4}/CHANGELOG.md +126 -1
  3. {patchcraft-0.5.3 → patchcraft-0.5.4}/CITATION.cff +2 -2
  4. {patchcraft-0.5.3 → patchcraft-0.5.4}/CONTRIBUTING.md +7 -2
  5. {patchcraft-0.5.3 → patchcraft-0.5.4}/MAP.md +3 -3
  6. {patchcraft-0.5.3/src/patchcraft.egg-info → patchcraft-0.5.4}/PKG-INFO +2 -2
  7. {patchcraft-0.5.3 → patchcraft-0.5.4}/README.md +1 -1
  8. {patchcraft-0.5.3 → patchcraft-0.5.4}/README.pt-BR.md +1 -1
  9. {patchcraft-0.5.3 → patchcraft-0.5.4}/README.pypi.md +1 -1
  10. {patchcraft-0.5.3 → patchcraft-0.5.4}/SECURITY.md +13 -6
  11. patchcraft-0.5.4/STATUS.md +97 -0
  12. patchcraft-0.5.4/docs/ADR/0003-reversibility-classes.md +139 -0
  13. {patchcraft-0.5.3 → patchcraft-0.5.4}/docs/ADR/0004-precision-and-effort-parameters.md +61 -78
  14. {patchcraft-0.5.3 → patchcraft-0.5.4}/docs/FOCO-1.0.md +1 -1
  15. {patchcraft-0.5.3 → patchcraft-0.5.4}/docs/GUIDE.md +4 -4
  16. patchcraft-0.5.4/docs/STUDIES/2026-09-03-precision-measurements.md +98 -0
  17. {patchcraft-0.5.3 → patchcraft-0.5.4}/docs/THEORY.md +3 -2
  18. {patchcraft-0.5.3 → patchcraft-0.5.4}/src/patchcraft/_version.py +3 -3
  19. {patchcraft-0.5.3 → patchcraft-0.5.4}/src/patchcraft/cache.py +5 -1
  20. {patchcraft-0.5.3 → patchcraft-0.5.4}/src/patchcraft/geometry.py +3 -1
  21. {patchcraft-0.5.3 → patchcraft-0.5.4}/src/patchcraft/reconstruct.py +5 -2
  22. {patchcraft-0.5.3 → patchcraft-0.5.4}/src/patchcraft/resize.py +33 -3
  23. {patchcraft-0.5.3 → patchcraft-0.5.4/src/patchcraft.egg-info}/PKG-INFO +2 -2
  24. {patchcraft-0.5.3 → patchcraft-0.5.4}/src/patchcraft.egg-info/SOURCES.txt +1 -0
  25. {patchcraft-0.5.3 → patchcraft-0.5.4}/src/patchcraft.egg-info/scm_file_list.json +1 -0
  26. patchcraft-0.5.4/src/patchcraft.egg-info/scm_version.json +8 -0
  27. {patchcraft-0.5.3 → patchcraft-0.5.4}/tests/test_cache.py +29 -0
  28. {patchcraft-0.5.3 → patchcraft-0.5.4}/tests/test_resize.py +73 -0
  29. patchcraft-0.5.3/STATUS.md +0 -85
  30. patchcraft-0.5.3/docs/ADR/0003-reversibility-classes.md +0 -108
  31. patchcraft-0.5.3/src/patchcraft.egg-info/scm_version.json +0 -8
  32. {patchcraft-0.5.3 → patchcraft-0.5.4}/.gitignore +0 -0
  33. {patchcraft-0.5.3 → patchcraft-0.5.4}/.pre-commit-config.yaml +0 -0
  34. {patchcraft-0.5.3 → patchcraft-0.5.4}/.python-version +0 -0
  35. {patchcraft-0.5.3 → patchcraft-0.5.4}/CODE_OF_CONDUCT.md +0 -0
  36. {patchcraft-0.5.3 → patchcraft-0.5.4}/LICENSE +0 -0
  37. {patchcraft-0.5.3 → patchcraft-0.5.4}/MANIFEST.in +0 -0
  38. {patchcraft-0.5.3 → patchcraft-0.5.4}/accel/Cargo.lock +0 -0
  39. {patchcraft-0.5.3 → patchcraft-0.5.4}/accel/Cargo.toml +0 -0
  40. {patchcraft-0.5.3 → patchcraft-0.5.4}/accel/README.md +0 -0
  41. {patchcraft-0.5.3 → patchcraft-0.5.4}/accel/src/kernel.rs +0 -0
  42. {patchcraft-0.5.3 → patchcraft-0.5.4}/accel/src/lib.rs +0 -0
  43. {patchcraft-0.5.3 → patchcraft-0.5.4}/docs/ADR/0001-patch-extraction-api.md +0 -0
  44. {patchcraft-0.5.3 → patchcraft-0.5.4}/docs/ADR/0002-patchify-transform.md +0 -0
  45. {patchcraft-0.5.3 → patchcraft-0.5.4}/docs/AUXILIARY.md +0 -0
  46. {patchcraft-0.5.3 → patchcraft-0.5.4}/docs/PERFORMANCE.md +0 -0
  47. {patchcraft-0.5.3 → patchcraft-0.5.4}/docs/ROADMAP.md +0 -0
  48. {patchcraft-0.5.3 → patchcraft-0.5.4}/docs/SCOPE.md +0 -0
  49. {patchcraft-0.5.3 → patchcraft-0.5.4}/docs/STUDIES/2026-08-04-patch-techniques.md +0 -0
  50. {patchcraft-0.5.3 → patchcraft-0.5.4}/docs/STUDIES/2026-09-03-strata-conformity.md +0 -0
  51. {patchcraft-0.5.3 → patchcraft-0.5.4}/docs/USAGE.md +0 -0
  52. {patchcraft-0.5.3 → patchcraft-0.5.4}/docs/design/2026-09-01-fase1-bugs-performance.md +0 -0
  53. {patchcraft-0.5.3 → patchcraft-0.5.4}/docs/design/2026-09-01-fase2-acelerador.md +0 -0
  54. {patchcraft-0.5.3 → patchcraft-0.5.4}/docs/design/2026-09-01-fase3-predicado.md +0 -0
  55. {patchcraft-0.5.3 → patchcraft-0.5.4}/pyproject.toml +0 -0
  56. {patchcraft-0.5.3 → patchcraft-0.5.4}/setup.cfg +0 -0
  57. {patchcraft-0.5.3 → patchcraft-0.5.4}/setup.py +0 -0
  58. {patchcraft-0.5.3 → patchcraft-0.5.4}/src/patchcraft/__init__.py +0 -0
  59. {patchcraft-0.5.3 → patchcraft-0.5.4}/src/patchcraft/_accel.py +0 -0
  60. {patchcraft-0.5.3 → patchcraft-0.5.4}/src/patchcraft/_foldgeom.py +0 -0
  61. {patchcraft-0.5.3 → patchcraft-0.5.4}/src/patchcraft/extract.py +0 -0
  62. {patchcraft-0.5.3 → patchcraft-0.5.4}/src/patchcraft/metrics.py +0 -0
  63. {patchcraft-0.5.3 → patchcraft-0.5.4}/src/patchcraft/pair.py +0 -0
  64. {patchcraft-0.5.3 → patchcraft-0.5.4}/src/patchcraft/py.typed +0 -0
  65. {patchcraft-0.5.3 → patchcraft-0.5.4}/src/patchcraft/stitch.py +0 -0
  66. {patchcraft-0.5.3 → patchcraft-0.5.4}/src/patchcraft.egg-info/dependency_links.txt +0 -0
  67. {patchcraft-0.5.3 → patchcraft-0.5.4}/src/patchcraft.egg-info/requires.txt +0 -0
  68. {patchcraft-0.5.3 → patchcraft-0.5.4}/src/patchcraft.egg-info/top_level.txt +0 -0
  69. {patchcraft-0.5.3 → patchcraft-0.5.4}/tests/__init__.py +0 -0
  70. {patchcraft-0.5.3 → patchcraft-0.5.4}/tests/_datasets.py +0 -0
  71. {patchcraft-0.5.3 → patchcraft-0.5.4}/tests/_rng.py +0 -0
  72. {patchcraft-0.5.3 → patchcraft-0.5.4}/tests/conftest.py +0 -0
  73. {patchcraft-0.5.3 → patchcraft-0.5.4}/tests/test_accel.py +0 -0
  74. {patchcraft-0.5.3 → patchcraft-0.5.4}/tests/test_citation.py +0 -0
  75. {patchcraft-0.5.3 → patchcraft-0.5.4}/tests/test_datasets_helper.py +0 -0
  76. {patchcraft-0.5.3 → patchcraft-0.5.4}/tests/test_docs_usage.py +0 -0
  77. {patchcraft-0.5.3 → patchcraft-0.5.4}/tests/test_exactness.py +0 -0
  78. {patchcraft-0.5.3 → patchcraft-0.5.4}/tests/test_extract.py +0 -0
  79. {patchcraft-0.5.3 → patchcraft-0.5.4}/tests/test_geometry.py +0 -0
  80. {patchcraft-0.5.3 → patchcraft-0.5.4}/tests/test_import.py +0 -0
  81. {patchcraft-0.5.3 → patchcraft-0.5.4}/tests/test_metrics.py +0 -0
  82. {patchcraft-0.5.3 → patchcraft-0.5.4}/tests/test_pair.py +0 -0
  83. {patchcraft-0.5.3 → patchcraft-0.5.4}/tests/test_public_api.py +0 -0
  84. {patchcraft-0.5.3 → patchcraft-0.5.4}/tests/test_reconstruct.py +0 -0
  85. {patchcraft-0.5.3 → patchcraft-0.5.4}/tests/test_reference.py +0 -0
  86. {patchcraft-0.5.3 → patchcraft-0.5.4}/tests/test_rng.py +0 -0
  87. {patchcraft-0.5.3 → patchcraft-0.5.4}/tests/test_stitch.py +0 -0
  88. {patchcraft-0.5.3 → patchcraft-0.5.4}/tools/benchmark.py +0 -0
  89. {patchcraft-0.5.3 → patchcraft-0.5.4}/tools/check_dist.py +0 -0
  90. {patchcraft-0.5.3 → patchcraft-0.5.4}/uv.lock +0 -0
@@ -89,7 +89,30 @@ literal directory of that name on Linux.
89
89
  `tools/check_dist.py` holds the matching allowlist. Adding a top-level
90
90
  directory means updating both. The gate will tell you; do not silence it.
91
91
 
92
- ### I10. Do not invent what already exists
92
+ ### I10. An ADR carries the choice, not the research behind it
93
+
94
+ Before writing one, split what is being said into what is **determined** and
95
+ what is **chosen**, and put only the second in the ADR.
96
+
97
+ A name is determined: which identifier a parameter carries is a fact about the
98
+ ecosystem the caller already imports, and picking one by taste is a guess made
99
+ permanent by a frozen surface. A measurement is determined. A benchmark table is
100
+ determined. Those belong in `docs/STUDIES/`, dated, with the ADR linking to
101
+ them.
102
+
103
+ What is chosen is whether to do the thing at all, when, and what it costs the
104
+ surface. If that part comes out empty, it was never an ADR: it was a finding,
105
+ and a study is where it goes.
106
+
107
+ Both ADRs written in 2026-09 failed this on the first draft, in the same way
108
+ and for the same reason, which is that writing down everything you learned feels
109
+ like thoroughness and reads as a lab notebook. ADR 0003 mixed a rule with a
110
+ vocabulary for six transforms that never shipped; ADR 0004 mixed the names,
111
+ which precedent decides, with whether to spend a frozen surface on them, which
112
+ only the owner decides. Both came out clearer once separated, and both were
113
+ caught by the owner asking what the document was for.
114
+
115
+ ### I11. Do not invent what already exists
93
116
 
94
117
  Check for an official or established solution before writing one. The
95
118
  accelerator's packaging, the versioning rule, the citation format and the
@@ -6,6 +6,130 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.5.4] - 2026-09-04
10
+
11
+ ### Fixed
12
+
13
+ - **13 of the 25 resample-mode x integer-dtype combinations raised a raw torch
14
+ error** instead of resizing. THEORY §9.4 accepts any integer tensor on the
15
+ `torch` backend and promises interpolation in float with round + clamp on the
16
+ way back, unconditionally. `_resize_torch` guarded the promotion with
17
+ `mode in {"bilinear", "bicubic"}`, so an integer tensor reached
18
+ `F.interpolate` unpromoted under `nearest`, `area` and `nearest-exact` and
19
+ surfaced `NotImplementedError: "adaptive_avg_pool2d" not implemented for
20
+ 'Int'`, naming an internal op the caller never called. The outbound half of
21
+ the same function already did the right thing for every integer dtype with no
22
+ condition on the mode, so only the inbound guard was ever wrong.
23
+
24
+ The suite could not catch it because the two parametrizations that would have
25
+ never crossed: `test_torch_accepts_all_resamples` sweeps all five modes on
26
+ `torch.rand`, and every integer test used the default mode. The regression is
27
+ that crossing, 50 cases, plus the upsampling direction because `area` is
28
+ `adaptive_avg_pool2d` and behaves differently going up.
29
+
30
+ Fixing it by promoting everything through `float32` would have traded the
31
+ loud error for the silent truncation this library exists to prevent, so the
32
+ promotion width is now derived from the dtype's range: the narrowest float
33
+ that represents it exactly, `float32` for `uint8`, `int8` and `int16` and
34
+ `float64` for `int32`. That mirrors `patchcraft.metrics`, which already
35
+ promotes to `float64` for the same reason. **This also fixes a silent defect
36
+ that predates the one above**: on the bilinear path an `int32` tensor was
37
+ already being promoted through `float32`, so `2**24 + 1` came back as
38
+ `2**24`. Measured on 2026-09-04, on every mode. `int64` remains the one dtype
39
+ no float covers, and §9.4 now says so rather than implying otherwise.
40
+
41
+ - **`Cache("~/cache")` created a directory literally named `~`** in the working
42
+ directory instead of using the home directory. `Path(root)` was used without
43
+ `expanduser`. Every established library expands and then stops: verified that
44
+ `torch.hub.set_dir` calls `expanduser` and validates nothing, that pytest's
45
+ cacheprovider does not validate, and that pip passes `--cache-dir` through.
46
+ `Cache` now does the same one thing they do.
47
+
48
+ The question that found this was whether to reject a `namespace` containing
49
+ `..`. The answer is no, and the reason is the owner's: it is not this
50
+ library's job to invent path security, and no library in the ecosystem does
51
+ it. What it was missing was the one thing they all do. `SECURITY.md` and
52
+ THEORY §9.5 now say that the path is the caller's and that only `~` is
53
+ expanded.
54
+
55
+
56
+ ### Added
57
+
58
+ - The rule that produced the two rescopings is recorded as invariant I10 in
59
+ `AGENTS.md`: an ADR carries the choice, not the research behind it. Before
60
+ writing one, split what is determined from what is chosen and keep only the
61
+ second; if the chosen part comes out empty it was a finding, and a dated
62
+ study is where it goes. Both ADRs written this month failed it on the first
63
+ draft, in the same way, and both were caught by the owner asking what the
64
+ document was for. `docs/ADR/0001` and `0002` were checked and do not have the
65
+ problem: they are context, decision, consequences and alternatives, at a
66
+ hundred lines each.
67
+ - **ADR 0004 separates what is determined from what is chosen, and what is left
68
+ is nothing.** The owner's point settled the first half: naming is a fact about
69
+ the ecosystem the caller already imports, not a preference of this project,
70
+ and choosing by taste would be a guess made permanent by a frozen surface. So
71
+ the three names are stated as determined by precedent, verified on 2026-09-03
72
+ against the installed libraries: `numpy.sum` and `torch.sum` take `dtype=`,
73
+ `torch.div` takes `rounding_mode=` and documents exactly `None`, `"trunc"` and
74
+ `"floor"`, both `interpolate` and torchvision's `resize` take `antialias=`,
75
+ and `numpy.ufunc.accumulate` exists, which is the collision that rules out
76
+ `accumulate=`.
77
+
78
+ That narrowing left one apparent question, whether to add the knobs before the
79
+ 1.0 freeze or after, on the stated ground that afterwards each costs a minor
80
+ release. Applying I10 to it dissolved it, because the premise is false: every
81
+ one of these is a keyword-only argument with a default, so adding one is
82
+ additive and can never be a major, and `reconstruct` has no positional-only
83
+ parameters to make the order matter. Nothing about adding one later is harder
84
+ than adding one now. With the premise gone, YAGNI, the absence of any external
85
+ consumer and Strata §9's default verdict all point the same way, and the ADR's
86
+ own table had already marked every knob "after 1.0" before the question was
87
+ written on top of it. The document now records a design that is not built, and
88
+ stays `Proposed` because none of it is implemented rather than because
89
+ anything is pending.
90
+ - `docs/STUDIES/2026-09-03-precision-measurements.md`. ADR 0004 was carrying
91
+ its own evidence, which made a 227-line document that read as a lab notebook
92
+ where a decision was wanted. The measurements moved here, the ADR kept the
93
+ decision, the names and the order, and it is 176 lines. Each measurement now
94
+ says what it settled, so a number can be checked without reading the argument
95
+ that used it.
96
+
97
+ ### Changed
98
+
99
+ - **ADR 0004 step 0 is done**, which the ADR names as the only item that must
100
+ land before 1.0: the `reconstruct` docstring's "widening the dtype does not
101
+ help" now says *input* dtype, and that widening the accumulator does help,
102
+ with the measurement. It was the one live site; earlier counts of seven
103
+ included historical and study files that are not edited.
104
+ - **`WeightKind` is declared an open set** in the GUIDE symbol table and in
105
+ CONTRIBUTING's versioning policy. The choice was made and pinned by test long
106
+ ago; what did not exist was the sentence a consumer reads before writing an
107
+ exhaustive `match`.
108
+ - Four one-line corrections found by the 2026-09-04 audit: FOCO §1 counted
109
+ "four things" over a list of five; MAP and CONTRIBUTING said ten invariants
110
+ after I10 made it eleven; AGENTS.md claimed both rescoped ADRs "shrank by a
111
+ third" when ADR 0003 grew; and `tilings`' docstring promised an ordering by
112
+ `(patch_size, stride)` that the function does not produce. The docstring now
113
+ states the measured order (patch size ascending, exact tile first within each)
114
+ rather than the function changing, because nothing depends on the order and
115
+ a change would be behaviour.
116
+ - **ADR 0003 is accepted, after being rescoped to what it actually governs.**
117
+ Its first draft decided two things at once: a rule about how an exactness
118
+ claim is written, and a three-letter vocabulary plus naming policy for six
119
+ transforms that were under consideration. A month later the rule was
120
+ governing the code and the vocabulary had been used nowhere: `R1`, `R2` and
121
+ `R3` appear in no docstring, no contract section and no page a reader opens,
122
+ and none of rotation, flips, padding, DCT, quantization or dithering shipped.
123
+ What is now accepted is the rule, that an exactness claim is declared per
124
+ regime with a condition the caller can evaluate before calling, with the four
125
+ measured regime axes in its body including the accumulator amendment from
126
+ ADR 0004. The vocabulary is deferred until the first transform that needs it.
127
+ The document also states plainly what its title had obscured: inexact
128
+ operations are not restricted, since `resize` and windowed `stitch` are
129
+ exactly what users come for, and what is forbidden is a docstring implying an
130
+ exactness the operation does not have.
131
+
132
+
9
133
  ## [0.5.3] - 2026-09-03
10
134
 
11
135
  ### Added
@@ -771,7 +895,8 @@ First public release. Public API stable; signatures will only change in 1.x.
771
895
  - [`README.md`](README.md) covers installation, the car-vs-track metaphor,
772
896
  validation lab.
773
897
 
774
- [Unreleased]: https://github.com/LeoPR/PatchCraft/compare/v0.5.3...HEAD
898
+ [Unreleased]: https://github.com/LeoPR/PatchCraft/compare/v0.5.4...HEAD
899
+ [0.5.4]: https://github.com/LeoPR/PatchCraft/releases/tag/v0.5.4
775
900
  [0.5.3]: https://github.com/LeoPR/PatchCraft/releases/tag/v0.5.3
776
901
  [0.5.2]: https://github.com/LeoPR/PatchCraft/releases/tag/v0.5.2
777
902
  [0.5.1]: https://github.com/LeoPR/PatchCraft/releases/tag/v0.5.1
@@ -6,8 +6,8 @@ authors:
6
6
  - given-names: Leonardo
7
7
  family-names: Marques de Souza
8
8
  email: leonardo.marques.souza@gmail.com
9
- version: "0.5.3"
10
- date-released: "2026-09-03"
9
+ version: "0.5.4"
10
+ date-released: "2026-09-04"
11
11
  repository-code: "https://github.com/LeoPR/PatchCraft"
12
12
  license: MIT
13
13
  abstract: >
@@ -136,7 +136,7 @@ PatchCraft/
136
136
  ├── README.pypi.md the PyPI page (long_description), links absolute
137
137
  ├── CONTRIBUTING.md this file
138
138
  ├── MAP.md one-page wayfinding: the tree, and a want-to/go-to table
139
- ├── AGENTS.md the canonical rules; ten invariants, each earned
139
+ ├── AGENTS.md the canonical rules; eleven invariants, each earned
140
140
  ├── CLAUDE.md thin pointer to AGENTS.md plus tool-specific notes
141
141
  ├── STATUS.md the snapshot; what is in flight and what waits on whom
142
142
  ├── CHANGELOG.md Keep-a-Changelog format
@@ -200,7 +200,7 @@ PatchCraft/
200
200
  │ ├── THEORY.md distilled design + §9 condition contract; §0 binding scope
201
201
  │ ├── ROADMAP.md milestone plan
202
202
  │ ├── FOCO-1.0.md what 1.0 freezes and the blockers in the way
203
- │ ├── ADR/ one file per decision that shaped the API
203
+ │ ├── ADR/ one file per decision; the choice only, evidence in STUDIES/
204
204
  │ ├── STUDIES/ background reading behind those decisions
205
205
  │ └── design/ one spec per work phase: the alternatives measured and the decision
206
206
  ├── outreach/ material for presenting the project publicly, not shipped
@@ -255,6 +255,11 @@ promise that nothing does.
255
255
 
256
256
  That fixes what each digit is for here:
257
257
 
258
+ One consequence worth stating on its own, because a consumer cannot infer it
259
+ from a `Literal`: **`WeightKind` is an open set.** Adding a member is additive
260
+ and lands in a `0.y` before 1.0 and a minor after it; removing or renaming one
261
+ is a break. Code that matches on it exhaustively should keep a fallback arm.
262
+
258
263
  The question that decides it is not "did a returned value change". A bug fix
259
264
  changes returned values by definition, and if that forced a `0.Y.0` then a
260
265
  patch release could never fix anything. The question is **whether the contract
@@ -15,7 +15,7 @@ PatchCraft/
15
15
  │ README.pt-BR.md ........... the same page in Portuguese
16
16
  │ README.pypi.md ............ the PyPI page; links absolute, because relative ones break there
17
17
  ├── MAP.md .................... this file
18
- ├── AGENTS.md ................. the rules, agent-agnostic and canonical; ten invariants
18
+ ├── AGENTS.md ................. the rules, agent-agnostic and canonical; eleven invariants
19
19
  ├── CLAUDE.md ................. thin; points at AGENTS.md and holds tool-specific notes
20
20
  ├── STATUS.md ................. the snapshot: where things are now, and who each item waits on
21
21
  ├── CHANGELOG.md .............. every release, each change with the measurement behind it
@@ -56,7 +56,7 @@ PatchCraft/
56
56
  │ ├── AUXILIARY.md .......... test fixtures, lab/, the off-tree conventions
57
57
  │ ├── ROADMAP.md ............ milestones, historical
58
58
  │ ├── FOCO-1.0.md ........... what 1.0 freezes, and the blockers still in the way
59
- │ ├── ADR/ .................. one file per decision that shaped the API
59
+ │ ├── ADR/ .................. one file per decision; the choice, not the research
60
60
  │ ├── design/ ............... one spec per work phase: alternatives measured, then chosen
61
61
  │ └── STUDIES/ .............. background reading, and the Strata conformity review
62
62
  │
@@ -84,7 +84,7 @@ PatchCraft/
84
84
  | Know **whether this library is the right tool** | [docs/SCOPE.md](docs/SCOPE.md) |
85
85
  | Know **what the native accelerator is worth**, and re-measure it | [docs/PERFORMANCE.md](docs/PERFORMANCE.md), then `python tools/benchmark.py` |
86
86
  | Know **what the project refuses to claim** | [docs/GUIDE.md §8](docs/GUIDE.md#8-what-this-project-does-not-claim) |
87
- | Understand **why the API looks like this** | [docs/ADR/](docs/ADR/) |
87
+ | Understand **why the API looks like this** | [docs/ADR/](docs/ADR/), and [docs/STUDIES/](docs/STUDIES/) for the measurements behind them |
88
88
  | Understand **why a phase was built the way it was** | [docs/design/](docs/design/) |
89
89
  | **Contribute**: run the gates, understand the two code paths | [CONTRIBUTING.md](CONTRIBUTING.md) |
90
90
  | **Release** it | [CONTRIBUTING.md](CONTRIBUTING.md), the "Releasing" section |
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: patchcraft
3
- Version: 0.5.3
3
+ Version: 0.5.4
4
4
  Summary: Image patch extraction, reconstruction, pairing and seam-aware stitching for super-resolution and dataset pipelines.
5
5
  Author: Leonardo Marques de Souza
6
6
  License-Expression: MIT
@@ -170,7 +170,7 @@ The everyday shorthand is that `stride == patch_size` and `stride == patch_size
170
170
 
171
171
  ## Status
172
172
 
173
- This is pre-1.0, so both the output values and the API shape can still move. While the leading digit is zero the middle one is the compatibility boundary, which makes a new `0.y.z` safe to take and a new `0.y` the place where a change is allowed to land, and the [changelog](https://github.com/LeoPR/PatchCraft/blob/main/CHANGELOG.md) records each one with the measurement behind it. The suite collects 1571 tests and passes on Python 3.12, 3.13 and 3.14, on Ubuntu and on Windows alike.
173
+ This is pre-1.0, so both the output values and the API shape can still move. While the leading digit is zero the middle one is the compatibility boundary, which makes a new `0.y.z` safe to take and a new `0.y` the place where a change is allowed to land, and the [changelog](https://github.com/LeoPR/PatchCraft/blob/main/CHANGELOG.md) records each one with the measurement behind it. The suite collects 1656 tests and passes on Python 3.12, 3.13 and 3.14, on Ubuntu and on Windows alike.
174
174
 
175
175
  Two limits are worth knowing before you depend on it. Every figure on this page was measured on CPU, and no CUDA path has ever executed in the test matrix, so the pipeline does preserve the device you hand it while the exactness numbers stay unverified on GPU. The other limit is that no external project has consumed the published API in real use yet, and that consumption is this project's own stated gate for calling the shape settled.
176
176
 
@@ -146,7 +146,7 @@ The round trip is exact when every value in the count map is a power of two, whi
146
146
 
147
147
  **Pre-1.0**, and the badge above is the current release. While the leading digit is zero the middle one is the compatibility boundary, so `0.y.z` is safe to take and a new `0.y` is where an output value or a signature is allowed to move. [CHANGELOG.md](CHANGELOG.md) records every such change with the measurement behind it.
148
148
 
149
- 1571 tests collected and CI is green on {Ubuntu, Windows} x {Python 3.12, 3.13, 3.14}, with `ruff check` and `mypy --strict` in the same run. The package is typed and ships `py.typed`.
149
+ 1656 tests collected and CI is green on {Ubuntu, Windows} x {Python 3.12, 3.13, 3.14}, with `ruff check` and `mypy --strict` in the same run. The package is typed and ships `py.typed`.
150
150
 
151
151
  No external project has consumed it yet, and no CUDA path in this library has ever executed. [The guide](docs/GUIDE.md#8-what-this-project-does-not-claim) lists what else this project declines to claim.
152
152
 
@@ -146,7 +146,7 @@ O round-trip é exato quando todo valor do mapa de cobertura é potência de doi
146
146
 
147
147
  **Pré-1.0**, e o badge acima mostra a versão atual. Enquanto o primeiro dígito for zero, quem marca a fronteira de compatibilidade é o do meio: um `0.y.z` novo é seguro de pegar, e é num `0.y` novo que um valor de saída ou uma assinatura pode mudar. O [CHANGELOG.md](CHANGELOG.md) registra cada uma dessas mudanças com a medição por trás dela.
148
148
 
149
- São 1571 testes coletados, com CI verde em {Ubuntu, Windows} x {Python 3.12, 3.13, 3.14}, e com `ruff check` e `mypy --strict` na mesma execução. O pacote é tipado e distribui o `py.typed`.
149
+ São 1656 testes coletados, com CI verde em {Ubuntu, Windows} x {Python 3.12, 3.13, 3.14}, e com `ruff check` e `mypy --strict` na mesma execução. O pacote é tipado e distribui o `py.typed`.
150
150
 
151
151
  Nenhum projeto externo consumiu a biblioteca ainda, e nenhum caminho CUDA dela jamais executou. [O manual](docs/GUIDE.md#8-what-this-project-does-not-claim) lista o que mais este projeto se recusa a afirmar.
152
152
 
@@ -128,7 +128,7 @@ The everyday shorthand is that `stride == patch_size` and `stride == patch_size
128
128
 
129
129
  ## Status
130
130
 
131
- This is pre-1.0, so both the output values and the API shape can still move. While the leading digit is zero the middle one is the compatibility boundary, which makes a new `0.y.z` safe to take and a new `0.y` the place where a change is allowed to land, and the [changelog](https://github.com/LeoPR/PatchCraft/blob/main/CHANGELOG.md) records each one with the measurement behind it. The suite collects 1571 tests and passes on Python 3.12, 3.13 and 3.14, on Ubuntu and on Windows alike.
131
+ This is pre-1.0, so both the output values and the API shape can still move. While the leading digit is zero the middle one is the compatibility boundary, which makes a new `0.y.z` safe to take and a new `0.y` the place where a change is allowed to land, and the [changelog](https://github.com/LeoPR/PatchCraft/blob/main/CHANGELOG.md) records each one with the measurement behind it. The suite collects 1656 tests and passes on Python 3.12, 3.13 and 3.14, on Ubuntu and on Windows alike.
132
132
 
133
133
  Two limits are worth knowing before you depend on it. Every figure on this page was measured on CPU, and no CUDA path has ever executed in the test matrix, so the pipeline does preserve the device you hand it while the exactness numbers stay unverified on GPU. The other limit is that no external project has consumed the published API in real use yet, and that consumption is this project's own stated gate for calling the shape settled.
134
134
 
@@ -35,12 +35,19 @@ out-of-bounds write rather than an exception, and it is the thing most worth
35
35
  reporting. `PATCHCRAFT_ACCEL=0` disables the native path entirely, and the
36
36
  universal wheel does not contain it at all.
37
37
 
38
- **`Cache` joins its `namespace` into a filesystem path.** The argument is
39
- validated only as a non-empty string, so `Cache(root, namespace="../elsewhere")`
40
- writes outside `root`. The namespace is chosen by the calling code, not read
41
- from data, so this is a trust boundary rather than a hole: **do not derive a
42
- cache namespace from untrusted input**. If you need per-user or per-request
43
- caches, hash or otherwise sanitise the value before passing it.
38
+ **`Cache` uses the path you give it, and validates nothing beyond expanding
39
+ `~`.** That is deliberate and it is what every established library does with a
40
+ caller-supplied cache directory: `torch.hub.set_dir` calls `expanduser` and
41
+ stops, pytest's cacheprovider does not validate, pip passes its `--cache-dir`
42
+ through. A cache location is an argument the calling code chooses, not data
43
+ read from an untrusted source, so `Cache(root, namespace="../elsewhere")` will
44
+ write outside `root` and that is the caller's decision.
45
+
46
+ The boundary, therefore: **do not derive a cache root or namespace from
47
+ untrusted input.** If you need per-user or per-request caches, hash or
48
+ otherwise sanitise the value in the layer that has the context to do it. This
49
+ library is not the place for path policing, and adding it here would be
50
+ inventing a security mechanism where the ecosystem has a settled convention.
44
51
 
45
52
  **Nothing in the shipped package opens a network connection.** The dataset
46
53
  helpers that download anything live in `tests/`, which is not part of the
@@ -0,0 +1,97 @@
1
+ # STATUS: 2026-09-04
2
+
3
+ > The snapshot. Where things are *right now*, and who each open item waits on.
4
+ > Where things **are** is [MAP.md](MAP.md); the rules are [AGENTS.md](AGENTS.md);
5
+ > the history is [CHANGELOG.md](CHANGELOG.md). This file does not repeat them.
6
+
7
+ ## Where the project is
8
+
9
+ **0.5.4 is on PyPI**, published through Trusted Publishing on a tag push: one
10
+ sdist, five `cp312-abi3` platform wheels carrying the Rust accelerator, and one
11
+ universal `py3-none-any` wheel. There is no extra to enable and no second
12
+ package.
13
+
14
+ **1619 tests pass** under `pytest -m "not gpu"`, with 32 skipped and 5
15
+ deselected, plus two full-sweep gates behind `PATCHCRAFT_SWEEP_FULL=1`
16
+ that enumerate all 126,736 legal geometries. CI is green on
17
+ {Ubuntu, Windows} x {3.12, 3.13, 3.14} on the pure path, plus a two-OS job that
18
+ builds the Rust kernel and runs the whole suite through it.
19
+
20
+ **The surface is 20 names**, pinned by `tests/test_public_api.py` with
21
+ `inspect.signature`.
22
+
23
+ ## What is in flight
24
+
25
+ **Nothing unreleased.** 0.5.4 closed the nine entries that had accumulated
26
+ since 0.5.3: two fixes, `Cache` not expanding a leading `~` and `resize`
27
+ raising a raw torch error on 13 of the 25 resample-mode x integer-dtype
28
+ combinations, the second of which also uncovered a silent `int32` truncation
29
+ older than itself. The other seven are documentation, and they close every
30
+ item the 2026-09-04 audit left open, including ADR 0004's step 0 and the
31
+ `WeightKind` compatibility sentence.
32
+
33
+ ## What is blocked, and on whom
34
+
35
+ **ADR 0003 is accepted**, rescoped first. It ratifies one rule, that an
36
+ exactness claim is declared per regime with a condition the caller can evaluate
37
+ before calling, and it carries the accumulator amendment in its body: the
38
+ predicate is a property of the geometry **and** the accumulator, since over 76
39
+ legal geometries a `float32` accumulator is exact on 45, precisely the
40
+ power-of-two ones, and a `float64` accumulator on all 76. The three-letter
41
+ vocabulary its first draft proposed is deferred until a transform needs it.
42
+
43
+ **ADR 0004 is `Proposed`, and asks nothing.** It records the precision and
44
+ effort parameters as a design that is not built; the status reflects that none
45
+ of it is implemented, not that a decision is pending. The one question it
46
+ appeared to hold, whether to add the knobs before the 1.0 freeze or after,
47
+ rested on the premise that adding one afterwards is expensive. It is not: each
48
+ is a keyword-only argument with a default, so adding one is additive and is a
49
+ minor release, and `reconstruct` has no positional-only parameters to make the
50
+ order matter. Step A of its plan shipped in 0.5.2. **Waits on nobody.**
51
+
52
+ **The `Cache` path question is closed, and it was not a decision.** It was
53
+ posed as whether to reject a `namespace` containing `..`. The answer is no:
54
+ inventing path security is not this library's job, and no library in the
55
+ ecosystem does it, verified against `torch.hub.set_dir`, pytest's cacheprovider
56
+ and pip. Asking the question found the real defect, which was the opposite
57
+ shape: `Cache` was missing the one thing all of them do, so `Cache("~/cache")`
58
+ created a directory literally named `~`. Fixed in the unreleased set, with no
59
+ validation added. [SECURITY.md](SECURITY.md) and THEORY §9.5 now say the path
60
+ is the caller's and that only `~` is expanded.
61
+
62
+ ## 1.0 blockers
63
+
64
+ **All six are closed.** The last, B4, closed in 0.5.3 by making `docs/USAGE.md`
65
+ executable rather than by regenerating it: every `>>>` on the page now runs in
66
+ the suite, so it cannot go stale silently again.
67
+
68
+ **And that leaves no decision before 1.0 either.** ADR 0003 is accepted and
69
+ ADR 0004 asks nothing, so what stands between here and 1.0 is the project's own
70
+ gate rather than an open question: an external consumer, which it does not have
71
+ yet.
72
+
73
+ | | |
74
+ |---|---|
75
+ | B1 predicate wrong in fifteen places | closed in 0.5.0 |
76
+ | B2 the suite could not falsify B1 | closed in 0.5.0 |
77
+ | B3 nothing pinned the public surface | closed in 0.5.0 |
78
+ | B4 the two pages a stranger reads | closed in 0.5.3 |
79
+ | B5 THEORY §9 contradicted itself on fp16 | closed in 0.5.2 |
80
+ | B6 enumeration junk and asymmetric guards | closed in 0.5.0 |
81
+
82
+ ## What the project still declines to claim
83
+
84
+ No external project has consumed the library, and that consumption is this
85
+ project's own stated gate for calling the shape settled. No CUDA path has ever
86
+ executed, in CI or outside it. Three of the five accelerated wheels, the two
87
+ macOS ones and the aarch64 one, are built and have their contents checked, and
88
+ their kernel has never run in CI.
89
+
90
+ ## Also outstanding, outside the 1.0 path
91
+
92
+ **A DOI.** The Strata conformity review
93
+ ([docs/STUDIES/2026-09-03-strata-conformity.md](docs/STUDIES/2026-09-03-strata-conformity.md))
94
+ found it to be the one item missing from the publishing pattern, and it is the
95
+ kind that cannot be applied retroactively: Zenodo mints a DOI per release from
96
+ the moment it is connected, and every release before that has none, permanently.
97
+ One authorisation. **Waits on the owner.**
@@ -0,0 +1,139 @@
1
+ # ADR 0003: declaring exactness per regime
2
+
3
+ - **Status:** Accepted 2026-09-03
4
+ - **Date:** 2026-08-04, rescoped and accepted 2026-09-03
5
+ - **Deciders:** Leonardo Marques de Souza
6
+ - **Relates to:** [ADR 0001](0001-patch-extraction-api.md), [ADR 0004](0004-precision-and-effort-parameters.md), [`THEORY.md`](../THEORY.md) §0, §9, [`STUDIES/2026-08-04-patch-techniques.md`](../STUDIES/2026-08-04-patch-techniques.md) §4, §5
7
+
8
+ > **What this rescoping did.** The original draft decided two things at once: a
9
+ > rule about *how exactness is declared*, and a vocabulary plus naming policy for
10
+ > *six transforms that were being considered*. A month later the rule is
11
+ > governing the code and the vocabulary has never been used: `R1`, `R2` and `R3`
12
+ > appear in no docstring, in no contract section and on no page a reader opens,
13
+ > and none of rotation, flips, padding, DCT, quantization or dithering shipped.
14
+ > The rule is what was accepted. The vocabulary is deferred, and §4 says until
15
+ > when. Freezing unused vocabulary into 1.0 would be inventing work where
16
+ > nothing is broken.
17
+
18
+ ## 1. Context: "reversible" means three different things
19
+
20
+ Several operations claim a reversal, and the word covers three unrelated cases.
21
+
22
+ `rot90` and flips reverse **exactly**: the same tensor comes back, any dtype.
23
+ Arbitrary-angle rotation reverses only **approximately**, because the forward and
24
+ inverse interpolations each degrade the signal, and no parameter choice makes it
25
+ exact. Quantization does not reverse at all: it is many-to-one by construction,
26
+ and the useful contract is a measured error bound rather than an inverse.
27
+
28
+ The library already ships operations of the second and third kind, and they are
29
+ not accidents. `resize` from 64 to 32 and back differs from the original by
30
+ 0.619 on data in [0, 1], because two thirds of the pixels are gone; that is what
31
+ resizing is. `stitch(weight="hann")` averages the pixels that overlap, so the
32
+ sum survives and the parts do not; that is what seam blending is.
33
+
34
+ **So the problem is not that inexact operations exist.** The problem is that
35
+ nothing in the contract distinguishes them, and a caller cannot tell from a
36
+ signature whether what comes back is the same tensor, a close one, or a
37
+ different thing entirely.
38
+
39
+ ## 2. Decision: exactness is declared per regime, not per function
40
+
41
+ Each transform declares, in its docstring and in its [`THEORY.md`](../THEORY.md)
42
+ §9 contract section, the conditions under which its result is exact, and states
43
+ the measured error where it is not.
44
+
45
+ **The condition must be evaluable by the caller before the call.** A statement
46
+ that a result is exact "in the usual case" is not a contract; a statement that
47
+ it is exact when every value of the coverage map is a power of two is one,
48
+ because the caller can compute that from the geometry alone.
49
+
50
+ **One label per function is not writable for this library**, which is the whole
51
+ reason for the rule. `stitch`'s behaviour is selected by its `weight` argument.
52
+ `reconstruct`'s depends on the geometry, on the dtype, and, since ADR 0004
53
+ measured it, on the accumulator. Any single label attached to those names would
54
+ be false over part of their accepted input.
55
+
56
+ ### 2.1 The regime axes, as measured
57
+
58
+ **Geometry.** The `extract`/`reconstruct` round trip is bit-exact when every
59
+ value of the overlap coverage map is a power of two, and not otherwise. The
60
+ reason is arithmetic rather than statistical: the fold sums `k` copies of a value
61
+ and divides by `k`, which is exact in binary floating point exactly when `k` is a
62
+ power of two. Every exact tiling qualifies, since all its counts are 1.
63
+
64
+ **Accumulator.** ADR 0004 measured the sentence above to carry an unstated
65
+ premise, which is the accumulator the library happens to use. Over 76 legal
66
+ geometries on float32 input, a float32 accumulator is exact on 45, and those 45
67
+ are precisely the power-of-two ones. A float64 accumulator is exact on all 76.
68
+ The predicate is therefore a property of the geometry **and** the accumulator,
69
+ and a regime row must say which accumulator it holds for.
70
+
71
+ **Dtype, and it runs the opposite way to intuition.** In a geometry where
72
+ float32 and float64 are inexact, `float16` and `bfloat16` are exact, because half
73
+ precision is promoted to a float32 accumulator before the fold and a short
74
+ mantissa times a small count still lands inside 24 bits. No per-function label
75
+ can express an ordering in which the lower precision is the exact one.
76
+
77
+ **Input values, and this is why the suite missed the error.** The round-trip
78
+ tests built their images from `torch.arange`, and integer-valued data comes back
79
+ exact in regimes where random data does not. Any test asserting exactness must
80
+ use random full-mantissa data in the declared dtype, or it is a false-negative
81
+ generator.
82
+
83
+ ## 3. The refutation this ADR exists to prevent from returning
84
+
85
+ An earlier revision proposed `k_max <= 4` as the predicate. It is false, and the
86
+ measurement is recorded here so that it cannot come back.
87
+
88
+ It coincides with the correct predicate on square patches, where a count of 3
89
+ cannot occur, and fails on rectangular ones, where it can. Swept over 14,969
90
+ rectangular full-coverage geometries: `k_max <= 4` mispredicts 3,936 of them,
91
+ 26 percent, while the power-of-two predicate mispredicts 8. Those 8 fail in the
92
+ safe direction: the power-of-two predicate has **zero false positives**, never
93
+ promising exactness and then failing, and its 8 misses are geometries that happen
94
+ to be exact without having been promised.
95
+
96
+ **A contract may under-promise. It may not over-promise.** That sentence is the
97
+ norm this ADR contributes, and the project has already used it once, to retract
98
+ the wrong predicate in public.
99
+
100
+ ## 4. What is deferred, and until when
101
+
102
+ The original draft also proposed a three-letter vocabulary (`R1` bit-exact, `R2`
103
+ measured-approximate, `R3` irreversible), a naming rule reserving `inverse_*`,
104
+ `un*` and `de*` for exact inverses, and a policy sending entirely-inexact
105
+ features to auxiliary packages.
106
+
107
+ None of it is wrong. All of it was written for six transforms that do not exist,
108
+ and it has been used nowhere in a month. It is **deferred until the first
109
+ transform that needs it**, which is the first operation offering an inverse that
110
+ is not exact. At that point this ADR is amended or a successor is written, with
111
+ the vocabulary applied to a real case rather than to six hypothetical ones.
112
+
113
+ Until then the naming rule is worth keeping as a one-line habit rather than a
114
+ frozen policy: **do not give an approximate inverse a name that promises an exact
115
+ one**. `rotate_back`, never `derotate`.
116
+
117
+ ## 5. Consequences
118
+
119
+ - **Nothing in the code changes.** No public name moves, no call that worked
120
+ stops working, no new exception is raised. What this ratifies is how the
121
+ contract is written.
122
+ - **Inexact operations are not restricted.** `resize` and `stitch` with a window
123
+ are exactly the operations users come for. What is forbidden is shipping one
124
+ whose docstring implies an exactness it does not have.
125
+ - **The 1.0 freeze gains a rule and not a vocabulary.** [`FOCO-1.0.md`](../FOCO-1.0.md)
126
+ makes accepting this ADR part of the freeze; what gets frozen is the
127
+ requirement that every exactness claim name its regime and be caller-evaluable.
128
+ - **Tests mirror the regime, not the function.** An exactness claim is asserted
129
+ by a sweep over the regime's own condition, on random full-mantissa data.
130
+ `tests/test_exactness.py` is that, and it enumerates the space independently of
131
+ the predicate so it can falsify rather than confirm.
132
+ - **Integers are outside this predicate.** For unmodified patches the integer sum
133
+ is exactly `k*v` and `k*v // k == v` for every `k`, so an integer regime needs
134
+ its own condition, which is that the accumulator does not overflow. ADR 0004
135
+ carries that.
136
+ - **CPU verified only.** Every measurement here was taken on CPU, because no CUDA
137
+ path in this library has ever executed. The power-of-two boundary is argued
138
+ from IEEE arithmetic rather than from an accumulation order, so it should hold
139
+ anywhere, but the caveat stays until a CUDA sweep confirms it.