bindery-cli 0.32.0__tar.gz → 0.34.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 (76) hide show
  1. {bindery_cli-0.32.0 → bindery_cli-0.34.0}/.gitignore +5 -0
  2. {bindery_cli-0.32.0 → bindery_cli-0.34.0}/CLAUDE.md +5 -1
  3. {bindery_cli-0.32.0 → bindery_cli-0.34.0}/PKG-INFO +18 -6
  4. {bindery_cli-0.32.0 → bindery_cli-0.34.0}/README.md +17 -5
  5. {bindery_cli-0.32.0 → bindery_cli-0.34.0}/patchnotes.md +133 -0
  6. {bindery_cli-0.32.0 → bindery_cli-0.34.0}/pyproject.toml +1 -1
  7. {bindery_cli-0.32.0 → bindery_cli-0.34.0}/roadmap.md +226 -20
  8. bindery_cli-0.34.0/run_tests.sh +18 -0
  9. {bindery_cli-0.32.0 → bindery_cli-0.34.0}/spec.md +36 -5
  10. {bindery_cli-0.32.0 → bindery_cli-0.34.0}/src/bindery/__init__.py +1 -1
  11. {bindery_cli-0.32.0 → bindery_cli-0.34.0}/src/bindery/audit.py +232 -205
  12. {bindery_cli-0.32.0 → bindery_cli-0.34.0}/src/bindery/cli.py +75 -11
  13. {bindery_cli-0.32.0 → bindery_cli-0.34.0}/src/bindery/epub.py +3 -1
  14. {bindery_cli-0.32.0 → bindery_cli-0.34.0}/src/bindery/library.py +18 -3
  15. {bindery_cli-0.32.0 → bindery_cli-0.34.0}/src/bindery/validate.py +177 -19
  16. {bindery_cli-0.32.0 → bindery_cli-0.34.0}/tests/test_audit.py +499 -72
  17. {bindery_cli-0.32.0 → bindery_cli-0.34.0}/tests/test_cli.py +191 -9
  18. {bindery_cli-0.32.0 → bindery_cli-0.34.0}/tests/test_epub.py +13 -0
  19. {bindery_cli-0.32.0 → bindery_cli-0.34.0}/tests/test_library.py +70 -3
  20. bindery_cli-0.34.0/tests/test_validate.py +305 -0
  21. {bindery_cli-0.32.0 → bindery_cli-0.34.0}/uv.lock +1 -1
  22. bindery_cli-0.32.0/run_tests.sh +0 -10
  23. bindery_cli-0.32.0/scripts/FastSweepExtract.java +0 -44
  24. bindery_cli-0.32.0/scripts/find_block_in_inline.py +0 -44
  25. bindery_cli-0.32.0/scripts/find_css_illegal_tags.py +0 -64
  26. bindery_cli-0.32.0/scripts/find_duplicate_playorder.py +0 -50
  27. bindery_cli-0.32.0/scripts/find_empty_body.py +0 -44
  28. bindery_cli-0.32.0/scripts/find_illegal_tags.py +0 -169
  29. bindery_cli-0.32.0/scripts/find_invalid_ids.py +0 -53
  30. bindery_cli-0.32.0/scripts/find_invalid_st_tag.py +0 -43
  31. bindery_cli-0.32.0/scripts/find_invalid_value_attr.py +0 -46
  32. bindery_cli-0.32.0/scripts/find_missing_images.py +0 -81
  33. bindery_cli-0.32.0/scripts/find_missing_title.py +0 -46
  34. bindery_cli-0.32.0/scripts/sweep.sh +0 -34
  35. bindery_cli-0.32.0/test_facility/agda.epub +0 -0
  36. bindery_cli-0.32.0/test_facility/agda_fixed.epub +0 -0
  37. bindery_cli-0.32.0/test_facility/last_man_out.epub +0 -0
  38. bindery_cli-0.32.0/test_facility/last_man_out_fixed.epub +0 -0
  39. bindery_cli-0.32.0/test_facility/leaves_of_grass.epub +0 -0
  40. bindery_cli-0.32.0/test_facility/leaves_of_grass_fixed.epub +0 -0
  41. bindery_cli-0.32.0/test_facility/leaves_of_grass_st_fixed.epub +0 -0
  42. bindery_cli-0.32.0/test_facility/leaves_of_grass_tags_fixed.epub +0 -0
  43. bindery_cli-0.32.0/test_facility/muse_of_nightmares.epub +0 -0
  44. bindery_cli-0.32.0/test_facility/muse_of_nightmares_fixed.epub +0 -0
  45. bindery_cli-0.32.0/test_facility/poor_mans_fight.epub +0 -0
  46. bindery_cli-0.32.0/test_facility/poor_mans_fight_fixed.epub +0 -0
  47. bindery_cli-0.32.0/test_facility/test_block_in_inline.py +0 -69
  48. bindery_cli-0.32.0/test_facility/test_duplicate_playorder.py +0 -63
  49. bindery_cli-0.32.0/test_facility/test_empty_body.py +0 -59
  50. bindery_cli-0.32.0/test_facility/test_illegal_tags.py +0 -64
  51. bindery_cli-0.32.0/test_facility/test_invalid_ids.py +0 -79
  52. bindery_cli-0.32.0/test_facility/test_invalid_value.py +0 -65
  53. bindery_cli-0.32.0/test_facility/test_missing_title.py +0 -80
  54. bindery_cli-0.32.0/test_facility/unworthy_gods.epub +0 -0
  55. bindery_cli-0.32.0/test_facility/unworthy_gods_fixed.epub +0 -0
  56. bindery_cli-0.32.0/tests/test_validate.py +0 -150
  57. {bindery_cli-0.32.0 → bindery_cli-0.34.0}/.github/workflows/ci.yml +0 -0
  58. {bindery_cli-0.32.0 → bindery_cli-0.34.0}/.github/workflows/publish.yml +0 -0
  59. {bindery_cli-0.32.0 → bindery_cli-0.34.0}/AGENTS.md +0 -0
  60. {bindery_cli-0.32.0 → bindery_cli-0.34.0}/LICENSE +0 -0
  61. {bindery_cli-0.32.0 → bindery_cli-0.34.0}/docs/screenshots/library-sweep.png +0 -0
  62. {bindery_cli-0.32.0 → bindery_cli-0.34.0}/logo.svg +0 -0
  63. {bindery_cli-0.32.0 → bindery_cli-0.34.0}/scripts/FastSweep.java +0 -0
  64. {bindery_cli-0.32.0 → bindery_cli-0.34.0}/scripts/fast_sweep.py +0 -0
  65. {bindery_cli-0.32.0 → bindery_cli-0.34.0}/src/bindery/__main__.py +0 -0
  66. {bindery_cli-0.32.0 → bindery_cli-0.34.0}/src/bindery/pagination.py +0 -0
  67. {bindery_cli-0.32.0 → bindery_cli-0.34.0}/src/bindery/reserialize.py +0 -0
  68. {bindery_cli-0.32.0 → bindery_cli-0.34.0}/src/bindery/transforms.py +0 -0
  69. {bindery_cli-0.32.0 → bindery_cli-0.34.0}/src/bindery/watermark.py +0 -0
  70. {bindery_cli-0.32.0 → bindery_cli-0.34.0}/testing_facility/books_to_fix/.gitkeep +0 -0
  71. {bindery_cli-0.32.0 → bindery_cli-0.34.0}/testing_facility/top500candidates/REPORT.md +0 -0
  72. {bindery_cli-0.32.0 → bindery_cli-0.34.0}/tests/test_pagination.py +0 -0
  73. {bindery_cli-0.32.0 → bindery_cli-0.34.0}/tests/test_reserialize.py +0 -0
  74. {bindery_cli-0.32.0 → bindery_cli-0.34.0}/tests/test_transforms.py +0 -0
  75. {bindery_cli-0.32.0 → bindery_cli-0.34.0}/tests/test_version.py +0 -0
  76. {bindery_cli-0.32.0 → bindery_cli-0.34.0}/tests/test_watermark.py +0 -0
@@ -12,6 +12,11 @@ testing_facility/books_to_fix/*
12
12
  !testing_facility/books_to_fix/.gitkeep
13
13
  testing_facility/top500candidates/*.epub
14
14
 
15
+ # test_facility EPUBs stopped being tracked in v0.33.0 (forward-only; 9 of
16
+ # the 10 books are commercial and one untracked leak on a public repo is
17
+ # one too many). Files stay on disk, out of git.
18
+ test_facility/*.epub
19
+
15
20
  # Compiled Java scratch (FastSweep harness)
16
21
  *.class
17
22
 
@@ -52,7 +52,11 @@ Born from the 2026 library audit (see the user memory `calibre-library-epubcheck
52
52
  decision by `run phase1` on both the read-only and apply paths.
53
53
  Do not let any NEW fix touch content without its own flag; if a candidate repair cannot
54
54
  be made deterministically safe, it does not belong here — report it for manual repair
55
- instead.
55
+ instead. A fourth, smaller group sits beside the structural repairs and lossy strips:
56
+ the safe opt-ins (`--fix-ids`, `--add-img-alt`, `--strip-bad-attrs`,
57
+ `--escape-unknown-entities`) plus `--encode-url-spaces`, which repair without
58
+ altering visible markup; the "twelve structural repairs and three lossy strips"
59
+ phrasing elsewhere in these docs was never the complete inventory.
56
60
  - **The gate is the safety contract.** Never apply a repair epubcheck has not accepted.
57
61
  Respect the two-mode logic in `validate.gate` (fatal-fixing tolerates error unmasking;
58
62
  error-cleanup does not). The lossy strips (`--strip-pagination`, `--strip-broken-tags`,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: bindery-cli
3
- Version: 0.32.0
3
+ Version: 0.34.0
4
4
  Summary: Audit EPUB body text and repair broken EPUBs with deterministic, epubcheck-gated fixes; atomic in-place replacement in a Calibre library.
5
5
  Author: vrnvctss
6
6
  License: MIT
@@ -82,6 +82,8 @@ Every repair is gated by [epubcheck](https://github.com/w3c/epubcheck). The acce
82
82
 
83
83
  Introducing a net-new fatal is always rejected. If epubcheck itself fails to run (crash, timeout, unparsable output), the book is reported as an error and never applied; only an explicit `--no-validate` skips the gate. Originals are never modified except by an explicit, atomic in-place replace (see below), and even then only after the gate accepts the result.
84
84
 
85
+ Under the hood, the oracle can run two ways: the usual `epubcheck --json -` subprocess, or a small in-process daemon that compiles `FastDaemon.java` into a temp directory at runtime and drives a warm JVM over a pipe. The daemon is a pool bounded by `--workers` (serial runs use one), its every roundtrip is bounded by the caller's timeout, and any failure tears it down and falls back to the subprocess for good — the two paths must never disagree about a book, and the subprocess oracle is the fallback by design. The daemon is also the scariest under-tested code in the repo and depends on a matched javac/java pair; its status is recorded in the roadmap's Phase 12 postscript and Phase 14 backlog.
86
+
85
87
  The lossy modes (`--strip-pagination`, `--strip-broken-tags`, and `--strip-watermarks`) are the exception to the "must improve" rule. Since they remove visible markup rather than correcting XML schema violations, epubcheck counts often remain unchanged. They are accepted when the result is **no worse** (no net-new fatals or errors), relying on strict programmatic safety nets instead.
86
88
 
87
89
  ## Install
@@ -192,10 +194,12 @@ Apply all safe and lossy repairs directly into the Calibre database natively:
192
194
  bindery library ~/docs/Calibre\ Library --only all --apply --all --install-to-calibre
193
195
  ```
194
196
 
195
- - `--only {fatals,ncx,all}` restricts the candidate set. `ncx` targets NCX-001 mismatches (detected without epubcheck); `fatals` needs `--audit`.
197
+ - `--only {fatals,ncx,all}` restricts the candidate set. `ncx` targets NCX-001 mismatches (detected without epubcheck); `fatals` needs `--audit` or `--sweep`.
196
198
  - `--id <ids>` limits the sweep to a comma-separated list of Calibre book IDs, skipping the full library walk.
197
199
  - `--audit CSV` (the `fatals,errors,warnings,path` format produced by an epubcheck sweep) skips clean books so a run is fast. Paths are resolved on both sides, and a CSV that matches nothing triggers a loud warning instead of silently selecting zero books.
198
- - `--sweep` replaces the CSV step entirely: it runs a live epubcheck sweep for candidate selection and reuses each result as that book's before-measurement, so no book is checked twice. bindery-cli first tries a persistent Java daemon (`EpubcheckDaemon`) to eliminate the per-book JVM startup; in practice the daemon cannot boot on most installs (its classpath carries only epubcheck's launcher jar, not the `lib/` dependencies epubcheck itself puts on the classpath), and the run silently falls back to the standard per-book epubcheck subprocess at the usual seconds per book. Budget accordingly. Fixing the classpath is a recorded decision (it would also change the counts the gate measures), not a shipped behavior. Combine with `--only fatals` for a self-contained "find and fix the broken books" run.
200
+ - `--limit N` processes at most N candidates and stops opening archives after them (lazy within one worker window). A value below 1 is a usage error.
201
+ - `--min-chars N` and `--thin-chars N` are the audit's EMPTY and THIN body-text thresholds (defaults 2000 and 20000); `--min-chars` above `--thin-chars` is a usage error. `--max-doc-chars N` is the audit monolithic-document flag threshold (default 300,000).
202
+ - `--sweep` replaces the CSV step entirely: it runs a live epubcheck sweep for candidate selection and reuses each result as that book's before-measurement, so no book is checked twice. bindery-cli may serve checks from a small persistent Java daemon (see the gate section above for how it is bounded and how it fails safe); wherever the daemon cannot serve, the run falls back to the standard per-book epubcheck subprocess at the usual seconds per book. Combine with `--only fatals` for a self-contained "find and fix the broken books" run.
199
203
  - `--workers N` runs the sweep's candidate pass through N concurrent epubcheck workers (default 1: serial, unchanged). The subprocess releases the GIL, so threads parallelize the oracle honestly; books are checked in windows of N consumed in order, so the candidate set matches the serial sweep and `--limit` stays lazy within one window of overshoot. The repair phase stays serial: that is where the shared workdir and the atomic-replacement contract live.
200
204
  - `--json FILE` writes a machine-readable report of the whole run (per-book status, before/after counts, applied flag, summary totals). `--manual-list FILE` writes the paths of every book that was not auto-repaired, one per line, ready for manual follow-up.
201
205
  - `--apply` is required to write; the default is a dry run. `--backup DIR` mirrors originals before replacing; `--backup-inplace` writes `.epub.bak` beside each file.
@@ -204,7 +208,8 @@ bindery library ~/docs/Calibre\ Library --only all --apply --all --install-to-ca
204
208
  - `--all` automatically turns on all opt-in non-fatal fixes and lossy strips (pagination, watermarks, bad attributes, unknown entities, image alt tags, etc.) in a single run.
205
209
  - Only the `.epub` is replaced. `metadata.opf`, `cover.jpg`, and `metadata.db` are left for Calibre's Quality Check sync to reconcile.
206
210
  - A per-book progress line goes to stderr (stdout stays a clean report); `--quiet` suppresses it. A corrupt or unreadable book is reported and skipped, never aborting the sweep.
207
- - Exit codes: 0 for a clean sweep, 1 for a usage error, 2 when any book was rejected, unreadable, or failed epubcheck (for scripts and cron).
211
+ - Exit codes: 0 for a clean sweep, 1 for a usage error, 2 when any book was rejected, unreadable, or failed epubcheck (for scripts and cron). Note that argparse-level misuse (an unknown flag or a malformed argument) exits with 2 before any validation runs; the tool's own usage validations exit 1.
212
+ - A `partial` book (improved but still unable to open) is reported for manual follow-up and fails the run with exit 2, in `library`, `run phase1`, and `run phase3` alike: a book that still needs a human is trouble.
208
213
  - `repair` refuses to overwrite an existing output file unless `--force` is given.
209
214
 
210
215
  ## Run slices
@@ -246,15 +251,22 @@ cd ~/docs/Calibre\ Library && bindery run phase3 --ids 5071,5072 --json phase3-r
246
251
 
247
252
  `scripts/` holds standalone, read-only utilities that are useful for EPUB maintenance but fall outside bindery-cli's repair contract (fixing what they find would be a content change, which bindery-cli makes only via the opt-in `--strip-pagination`):
248
253
 
249
- - `find_missing_images.py`: scans a library tree and reports every book whose `<img>` tags point at files that do not exist inside the archive (a common defect in converted EPUBs). Reads the archives in place; nothing is unpacked or written. The library path is set at the bottom of the script.
250
254
  - `fast_sweep.py`: compiles and drives `FastSweep.java`, a parallel JVM epubcheck sweep that pays JVM startup once instead of per book (a 7,000-book dry-run sweep drops from hours to minutes). `--mode=audit` emits the `fatals,errors,warnings,path` CSV that `bindery library --audit` reads; `--mode=extract` emits per-book error codes for aggregation (`--summary` prints the per-code report). The epubcheck jar is located from the `epubcheck` launcher or `EPUBCHECK_JAR`; the harness compiles once (`--release 25`) and is cached on mtime.
251
255
 
256
+ The older `find_*.py` detection wedges and the `sweep.sh`/`FastSweepExtract.java` prototypes were removed: every detection they performed is now a shipped, tested flag (missing images are `--prune-missing-resources`, and the rest of the RSC-005/PKG-010 family shipped through Phases 6-12), and keeping two copies meant future behavior changes landed in only one of them. Git history preserves them.
257
+
252
258
  ## Development
253
259
 
254
260
  ```sh
255
- ./run_tests.sh # unittest suite
261
+ ./run_tests.sh # ruff (CI's pin) + unittest suite
256
262
  ```
257
263
 
264
+ The suite builds its own synthetic EPUBs in temporary directories. `testing_facility/`
265
+ holds staged real-book copies for manual dry runs (gitignored); `test_facility/` holds
266
+ loose local books that stay on disk but out of git (untracked since v0.33.0,
267
+ forward-only, because 9 of the 10 are commercial and one untracked leak on a public
268
+ repo is one too many).
269
+
258
270
  See [spec.md](spec.md) for the full contract and [roadmap.md](roadmap.md) for what is planned.
259
271
 
260
272
  ## License
@@ -60,6 +60,8 @@ Every repair is gated by [epubcheck](https://github.com/w3c/epubcheck). The acce
60
60
 
61
61
  Introducing a net-new fatal is always rejected. If epubcheck itself fails to run (crash, timeout, unparsable output), the book is reported as an error and never applied; only an explicit `--no-validate` skips the gate. Originals are never modified except by an explicit, atomic in-place replace (see below), and even then only after the gate accepts the result.
62
62
 
63
+ Under the hood, the oracle can run two ways: the usual `epubcheck --json -` subprocess, or a small in-process daemon that compiles `FastDaemon.java` into a temp directory at runtime and drives a warm JVM over a pipe. The daemon is a pool bounded by `--workers` (serial runs use one), its every roundtrip is bounded by the caller's timeout, and any failure tears it down and falls back to the subprocess for good — the two paths must never disagree about a book, and the subprocess oracle is the fallback by design. The daemon is also the scariest under-tested code in the repo and depends on a matched javac/java pair; its status is recorded in the roadmap's Phase 12 postscript and Phase 14 backlog.
64
+
63
65
  The lossy modes (`--strip-pagination`, `--strip-broken-tags`, and `--strip-watermarks`) are the exception to the "must improve" rule. Since they remove visible markup rather than correcting XML schema violations, epubcheck counts often remain unchanged. They are accepted when the result is **no worse** (no net-new fatals or errors), relying on strict programmatic safety nets instead.
64
66
 
65
67
  ## Install
@@ -170,10 +172,12 @@ Apply all safe and lossy repairs directly into the Calibre database natively:
170
172
  bindery library ~/docs/Calibre\ Library --only all --apply --all --install-to-calibre
171
173
  ```
172
174
 
173
- - `--only {fatals,ncx,all}` restricts the candidate set. `ncx` targets NCX-001 mismatches (detected without epubcheck); `fatals` needs `--audit`.
175
+ - `--only {fatals,ncx,all}` restricts the candidate set. `ncx` targets NCX-001 mismatches (detected without epubcheck); `fatals` needs `--audit` or `--sweep`.
174
176
  - `--id <ids>` limits the sweep to a comma-separated list of Calibre book IDs, skipping the full library walk.
175
177
  - `--audit CSV` (the `fatals,errors,warnings,path` format produced by an epubcheck sweep) skips clean books so a run is fast. Paths are resolved on both sides, and a CSV that matches nothing triggers a loud warning instead of silently selecting zero books.
176
- - `--sweep` replaces the CSV step entirely: it runs a live epubcheck sweep for candidate selection and reuses each result as that book's before-measurement, so no book is checked twice. bindery-cli first tries a persistent Java daemon (`EpubcheckDaemon`) to eliminate the per-book JVM startup; in practice the daemon cannot boot on most installs (its classpath carries only epubcheck's launcher jar, not the `lib/` dependencies epubcheck itself puts on the classpath), and the run silently falls back to the standard per-book epubcheck subprocess at the usual seconds per book. Budget accordingly. Fixing the classpath is a recorded decision (it would also change the counts the gate measures), not a shipped behavior. Combine with `--only fatals` for a self-contained "find and fix the broken books" run.
178
+ - `--limit N` processes at most N candidates and stops opening archives after them (lazy within one worker window). A value below 1 is a usage error.
179
+ - `--min-chars N` and `--thin-chars N` are the audit's EMPTY and THIN body-text thresholds (defaults 2000 and 20000); `--min-chars` above `--thin-chars` is a usage error. `--max-doc-chars N` is the audit monolithic-document flag threshold (default 300,000).
180
+ - `--sweep` replaces the CSV step entirely: it runs a live epubcheck sweep for candidate selection and reuses each result as that book's before-measurement, so no book is checked twice. bindery-cli may serve checks from a small persistent Java daemon (see the gate section above for how it is bounded and how it fails safe); wherever the daemon cannot serve, the run falls back to the standard per-book epubcheck subprocess at the usual seconds per book. Combine with `--only fatals` for a self-contained "find and fix the broken books" run.
177
181
  - `--workers N` runs the sweep's candidate pass through N concurrent epubcheck workers (default 1: serial, unchanged). The subprocess releases the GIL, so threads parallelize the oracle honestly; books are checked in windows of N consumed in order, so the candidate set matches the serial sweep and `--limit` stays lazy within one window of overshoot. The repair phase stays serial: that is where the shared workdir and the atomic-replacement contract live.
178
182
  - `--json FILE` writes a machine-readable report of the whole run (per-book status, before/after counts, applied flag, summary totals). `--manual-list FILE` writes the paths of every book that was not auto-repaired, one per line, ready for manual follow-up.
179
183
  - `--apply` is required to write; the default is a dry run. `--backup DIR` mirrors originals before replacing; `--backup-inplace` writes `.epub.bak` beside each file.
@@ -182,7 +186,8 @@ bindery library ~/docs/Calibre\ Library --only all --apply --all --install-to-ca
182
186
  - `--all` automatically turns on all opt-in non-fatal fixes and lossy strips (pagination, watermarks, bad attributes, unknown entities, image alt tags, etc.) in a single run.
183
187
  - Only the `.epub` is replaced. `metadata.opf`, `cover.jpg`, and `metadata.db` are left for Calibre's Quality Check sync to reconcile.
184
188
  - A per-book progress line goes to stderr (stdout stays a clean report); `--quiet` suppresses it. A corrupt or unreadable book is reported and skipped, never aborting the sweep.
185
- - Exit codes: 0 for a clean sweep, 1 for a usage error, 2 when any book was rejected, unreadable, or failed epubcheck (for scripts and cron).
189
+ - Exit codes: 0 for a clean sweep, 1 for a usage error, 2 when any book was rejected, unreadable, or failed epubcheck (for scripts and cron). Note that argparse-level misuse (an unknown flag or a malformed argument) exits with 2 before any validation runs; the tool's own usage validations exit 1.
190
+ - A `partial` book (improved but still unable to open) is reported for manual follow-up and fails the run with exit 2, in `library`, `run phase1`, and `run phase3` alike: a book that still needs a human is trouble.
186
191
  - `repair` refuses to overwrite an existing output file unless `--force` is given.
187
192
 
188
193
  ## Run slices
@@ -224,15 +229,22 @@ cd ~/docs/Calibre\ Library && bindery run phase3 --ids 5071,5072 --json phase3-r
224
229
 
225
230
  `scripts/` holds standalone, read-only utilities that are useful for EPUB maintenance but fall outside bindery-cli's repair contract (fixing what they find would be a content change, which bindery-cli makes only via the opt-in `--strip-pagination`):
226
231
 
227
- - `find_missing_images.py`: scans a library tree and reports every book whose `<img>` tags point at files that do not exist inside the archive (a common defect in converted EPUBs). Reads the archives in place; nothing is unpacked or written. The library path is set at the bottom of the script.
228
232
  - `fast_sweep.py`: compiles and drives `FastSweep.java`, a parallel JVM epubcheck sweep that pays JVM startup once instead of per book (a 7,000-book dry-run sweep drops from hours to minutes). `--mode=audit` emits the `fatals,errors,warnings,path` CSV that `bindery library --audit` reads; `--mode=extract` emits per-book error codes for aggregation (`--summary` prints the per-code report). The epubcheck jar is located from the `epubcheck` launcher or `EPUBCHECK_JAR`; the harness compiles once (`--release 25`) and is cached on mtime.
229
233
 
234
+ The older `find_*.py` detection wedges and the `sweep.sh`/`FastSweepExtract.java` prototypes were removed: every detection they performed is now a shipped, tested flag (missing images are `--prune-missing-resources`, and the rest of the RSC-005/PKG-010 family shipped through Phases 6-12), and keeping two copies meant future behavior changes landed in only one of them. Git history preserves them.
235
+
230
236
  ## Development
231
237
 
232
238
  ```sh
233
- ./run_tests.sh # unittest suite
239
+ ./run_tests.sh # ruff (CI's pin) + unittest suite
234
240
  ```
235
241
 
242
+ The suite builds its own synthetic EPUBs in temporary directories. `testing_facility/`
243
+ holds staged real-book copies for manual dry runs (gitignored); `test_facility/` holds
244
+ loose local books that stay on disk but out of git (untracked since v0.33.0,
245
+ forward-only, because 9 of the 10 are commercial and one untracked leak on a public
246
+ repo is one too many).
247
+
236
248
  See [spec.md](spec.md) for the full contract and [roadmap.md](roadmap.md) for what is planned.
237
249
 
238
250
  ## License
@@ -1,4 +1,137 @@
1
1
  # bindery-cli Patch Notes
2
+ ## v0.34.0 (2026-09-10)
3
+
4
+ ### Brandon's three calls: partial books are trouble, the contract reconciled, the daemon decision informed
5
+
6
+ - **Partial books now fail every layer (exit-code decision, option A).**
7
+ A `partial` book (repaired, improved, but still unable to open) used to
8
+ exit 0 from `library` and `run phase3` while `run phase1` counted it
9
+ as trouble. All three layers now exit 2 on it: the run succeeded, but
10
+ a book still needs a human. README and spec updated to match, and a
11
+ regression test pins a 3f-to-1f book exiting 2 with `applied: 0`.
12
+ - **spec.md reconciled with the shipped opt-ins** (applied on Brandon's
13
+ go, from the draft filed in v0.33.0's roadmap box). The non-goals no
14
+ longer prohibit what ships: "fixing RSC-005 in bulk" now names the
15
+ scoped, version-gated repairs as the shipped exception, and "repairing
16
+ genuinely mangled structure" now describes `--reserialize`'s actual
17
+ bounds (html root required, non-HTML XML sidecars refused). The
18
+ missing `--fix-ids` OPF section is written (manifest item ids, spine
19
+ idref/toc, fallback, media-overlay, EPUB 2 cover meta, both quote
20
+ styles), the garbled NCX sentence is rewritten, and "the OPF is left
21
+ untouched" now reads "by the default pass", naming the five opt-ins
22
+ that edit it on request.
23
+ - **The daemon decision, informed:** Brandon asked whether upgrading
24
+ java to 27 fixes the epubcheck daemon. Investigated and answered: his
25
+ javac is Fedora's `java-latest-openjdk-devel` 27-ea while `java` is
26
+ `java-25-openjdk` (both official Fedora packages, no COPR involved),
27
+ but the version skew is not the real blocker. epubcheck's own JSON
28
+ output is deduplicated (it reports 4 errors for a book whose CLI
29
+ summary says 243), the daemon counts everything, and bindery's gate
30
+ has always measured with JSON-style counts, so a working daemon would
31
+ change what the gate measures. The skew itself is fixable in code
32
+ (Java's single-file source launcher needs no javac at all), but the
33
+ daemon should stay off until its counting matches the JSON oracle; it
34
+ already self-disables safely, and `--workers` parallelism works via
35
+ subprocesses. Retire-or-reconcile remains the open decision; recorded
36
+ in the roadmap's `:958` box.
37
+
38
+ ## v0.33.0 (2026-09-10)
39
+
40
+ ### Phase 14 hardening: the apply path, the audit, and the safety contract's tests
41
+
42
+ The rest of the 2026-09-08 audit sweep's backlog, in the same shape as
43
+ v0.32.0: every fix confirmed against current code first, each with its
44
+ regression test.
45
+
46
+ **Apply-path and oracle safety**
47
+
48
+ - **A failed write can no longer abort a sweep raw.** The backup,
49
+ replace, and install calls sat bare in the loop, so an ENOSPC or
50
+ EACCES partway through a multi-hour run died with a traceback and no
51
+ record. Failures are now error Outcomes; the summary and JSON always
52
+ land, and exit 2 says scripts should look.
53
+ - **Backups rotate and stay out of the candidate set.** A second
54
+ `--apply` used to clobber the first backup (the only copy of the
55
+ author original); backups now rotate (`book.epub.bak2`, `.bak3`, ...).
56
+ A `--backup` directory inside the library root is refused (its copies
57
+ would be swept as candidates on the next run).
58
+ - **`--workers N` parallelism is real.** With epubcheck present, every
59
+ worker serialized behind the daemon's one pipe. The daemon is now a
60
+ bounded pool sized by `--workers` (default 1: the old behavior), a
61
+ busy pool falls back to subprocesses instead of blocking, every
62
+ roundtrip is bounded by the caller's timeout, and a daemon that dies
63
+ or wedges is torn down and never retried; one failed attempt exhausts
64
+ the pool, so the sweep degrades to pure subprocess checks instead of
65
+ respawning a JVM per book. Measured: 8 books, 4 workers, two ~10s
66
+ waves. The evidence for the recorded daemon-classpath decision grew:
67
+ this machine's javac (27-ea) and java (25) cannot load each other's
68
+ classes, so the daemon has never served a count here, and when forced
69
+ to run its counts diverge from the CLI JSON oracle (243 vs 4 on one
70
+ book): enabling it would change gate outcomes.
71
+ - **Smaller apply papercuts:** `--limit < 1` and a missing `--audit`
72
+ file are usage errors, not tracebacks; the fresh-format install
73
+ registers the catalogued file's own name instead of `repaired`; and
74
+ phase1 notes the dry-run `--backup` no-op instead of ignoring it.
75
+
76
+ **audit.py correctness**
77
+
78
+ - **ToC accounting reads inside the open zip,** a declared-but-absent
79
+ nav/NCX no longer poisons `Book.corrupt` (a healthy book with a
80
+ leftover `toc.ncx` entry is not a damaged archive), NCX `content src`
81
+ resolves against the NCX's own directory (a nested NCX counted every
82
+ target absent), and ToC hrefs are entity-decoded before comparison.
83
+ - **Archive and spine verdicts are first-class in library mode.** Their
84
+ sections were dead code in the library report loop: a CRC-corrupt book
85
+ printed `emptytext CLEAN` and exited 0 there while directory mode said
86
+ CORRUPT and exited 1. Both modes now agree.
87
+ - **The record pipeline's edges hold:** a failed `--json` write becomes a
88
+ trouble exit instead of a post-scan traceback; `--tag` skips
89
+ expected-foreign content findings; an unresolvable spine itemref is
90
+ counted into the emptytext detail instead of silently reading EMPTY;
91
+ DRM-encrypted entries get their own ENCRYPTED verdict
92
+ ("not repairable, skip") instead of CORRUPT re-source; and
93
+ part1a/part1b/part2-style spines no longer read as a broken span.
94
+ - **Dead surfaces removed** (a duplicate argparse CLI, an unreachable
95
+ analyzer, duplicated imports) and directory mode catches uppercase
96
+ `.EPUB` like the library walk always did.
97
+ - **Seven audit papercuts** closed: the block-stack pop now matches the
98
+ closing tag, the nav property matches whole tokens (`data-nav` is not
99
+ the nav), exit 3 is documented, the JSON analyzers list includes the
100
+ always-on verdicts, the console problem counter counts books like
101
+ `summary.problems`, `--min-chars` above `--thin-chars` is a usage
102
+ error, and duplicate archive entries are counted instead of resolving
103
+ last-wins in silence.
104
+
105
+ **Tests, hygiene, and docs**
106
+
107
+ - **Direct tests for the safety contract:** gate()/no_worse() including
108
+ the error-unmasking branch, the watermark gated-apply path,
109
+ atomic_replace failure injection (temp cleaned, target untouched,
110
+ re-raise), `--install-to-calibre` wiring, and library-mode `--tag` end
111
+ to end through cquarry's write path. Suite grew 342 → 381.
112
+ - **The weak two percent cleaned:** mid-file `__main__` blocks,
113
+ constant-assertion tests, and module aliases gone; the
114
+ non-interactive wiring is asserted through the real runner's JSON.
115
+ - **run_tests.sh runs CI's exact ruff pins** before the suite, so the
116
+ local loop fails where CI fails.
117
+ - **Repo hygiene:** the ten subsumed `scripts/find_*.py` wedges plus
118
+ `sweep.sh` and `FastSweepExtract.java` are retired (git keeps them);
119
+ the ten commercial EPUBs in `test_facility/` are untracked
120
+ (forward-only, files stay on disk) and the dead never-run suite there
121
+ is deleted.
122
+ - **Docs:** the daemon is admitted to exist (README gate section, spec
123
+ gate section) with its bounding and fail-safe fallback; README's
124
+ `--sweep` bullet stopped describing the fixed classpath bug as current;
125
+ the exit-code contract documents argparse's own exit 2 and the partial
126
+ book divergence; CLAUDE.md's exception taxonomy names the safe
127
+ opt-ins group.
128
+
129
+ **Recorded for Brandon (unchanged, evidence sharpened):** the daemon
130
+ classpath decision, the exit-code-from-partial-books decision (both
131
+ options written into its roadmap box; unifying on "trouble" is the
132
+ recommendation), and the spec.md reconciliation draft (the non-goals
133
+ vs. the shipped opt-ins, ready to apply on a go).
134
+
2
135
  ## v0.32.0 (2026-09-10)
3
136
 
4
137
  ### The four Bug Reports fixes (2026-09-08 sweep) and the anchored-attribute cluster
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "bindery-cli"
7
- version = "0.32.0"
7
+ version = "0.34.0"
8
8
  description = "Audit EPUB body text and repair broken EPUBs with deterministic, epubcheck-gated fixes; atomic in-place replacement in a Calibre library."
9
9
  readme = "README.md"
10
10
  classifiers = [