bindery-cli 0.28.0__tar.gz → 0.29.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 (72) hide show
  1. {bindery_cli-0.28.0 → bindery_cli-0.29.0}/.gitignore +5 -0
  2. {bindery_cli-0.28.0 → bindery_cli-0.29.0}/CLAUDE.md +11 -9
  3. bindery_cli-0.28.0/README.md → bindery_cli-0.29.0/PKG-INFO +69 -4
  4. bindery_cli-0.28.0/PKG-INFO → bindery_cli-0.29.0/README.md +47 -26
  5. {bindery_cli-0.28.0 → bindery_cli-0.29.0}/patchnotes.md +53 -0
  6. {bindery_cli-0.28.0 → bindery_cli-0.29.0}/pyproject.toml +3 -5
  7. {bindery_cli-0.28.0 → bindery_cli-0.29.0}/roadmap.md +121 -0
  8. {bindery_cli-0.28.0 → bindery_cli-0.29.0}/scripts/fast_sweep.py +16 -7
  9. {bindery_cli-0.28.0 → bindery_cli-0.29.0}/spec.md +32 -0
  10. {bindery_cli-0.28.0 → bindery_cli-0.29.0}/src/bindery/__init__.py +1 -1
  11. {bindery_cli-0.28.0 → bindery_cli-0.29.0}/src/bindery/audit.py +186 -4
  12. {bindery_cli-0.28.0 → bindery_cli-0.29.0}/src/bindery/cli.py +412 -7
  13. {bindery_cli-0.28.0 → bindery_cli-0.29.0}/src/bindery/epub.py +1 -0
  14. {bindery_cli-0.28.0 → bindery_cli-0.29.0}/tests/test_audit.py +200 -0
  15. {bindery_cli-0.28.0 → bindery_cli-0.29.0}/tests/test_cli.py +309 -0
  16. {bindery_cli-0.28.0 → bindery_cli-0.29.0}/tests/test_epub.py +12 -12
  17. {bindery_cli-0.28.0 → bindery_cli-0.29.0}/uv.lock +4 -4
  18. {bindery_cli-0.28.0 → bindery_cli-0.29.0}/.github/workflows/ci.yml +0 -0
  19. {bindery_cli-0.28.0 → bindery_cli-0.29.0}/.github/workflows/publish.yml +0 -0
  20. {bindery_cli-0.28.0 → bindery_cli-0.29.0}/AGENTS.md +0 -0
  21. {bindery_cli-0.28.0 → bindery_cli-0.29.0}/LICENSE +0 -0
  22. {bindery_cli-0.28.0 → bindery_cli-0.29.0}/docs/screenshots/library-sweep.png +0 -0
  23. {bindery_cli-0.28.0 → bindery_cli-0.29.0}/logo.svg +0 -0
  24. {bindery_cli-0.28.0 → bindery_cli-0.29.0}/run_tests.sh +0 -0
  25. {bindery_cli-0.28.0 → bindery_cli-0.29.0}/scripts/FastSweep.java +0 -0
  26. {bindery_cli-0.28.0 → bindery_cli-0.29.0}/scripts/find_block_in_inline.py +0 -0
  27. {bindery_cli-0.28.0 → bindery_cli-0.29.0}/scripts/find_css_illegal_tags.py +0 -0
  28. {bindery_cli-0.28.0 → bindery_cli-0.29.0}/scripts/find_duplicate_playorder.py +0 -0
  29. {bindery_cli-0.28.0 → bindery_cli-0.29.0}/scripts/find_empty_body.py +0 -0
  30. {bindery_cli-0.28.0 → bindery_cli-0.29.0}/scripts/find_illegal_tags.py +0 -0
  31. {bindery_cli-0.28.0 → bindery_cli-0.29.0}/scripts/find_invalid_ids.py +0 -0
  32. {bindery_cli-0.28.0 → bindery_cli-0.29.0}/scripts/find_invalid_st_tag.py +0 -0
  33. {bindery_cli-0.28.0 → bindery_cli-0.29.0}/scripts/find_invalid_value_attr.py +0 -0
  34. {bindery_cli-0.28.0 → bindery_cli-0.29.0}/scripts/find_missing_images.py +0 -0
  35. {bindery_cli-0.28.0 → bindery_cli-0.29.0}/scripts/find_missing_title.py +0 -0
  36. {bindery_cli-0.28.0 → bindery_cli-0.29.0}/src/bindery/__main__.py +0 -0
  37. {bindery_cli-0.28.0 → bindery_cli-0.29.0}/src/bindery/library.py +0 -0
  38. {bindery_cli-0.28.0 → bindery_cli-0.29.0}/src/bindery/pagination.py +0 -0
  39. {bindery_cli-0.28.0 → bindery_cli-0.29.0}/src/bindery/reserialize.py +0 -0
  40. {bindery_cli-0.28.0 → bindery_cli-0.29.0}/src/bindery/transforms.py +0 -0
  41. {bindery_cli-0.28.0 → bindery_cli-0.29.0}/src/bindery/validate.py +0 -0
  42. {bindery_cli-0.28.0 → bindery_cli-0.29.0}/src/bindery/watermark.py +0 -0
  43. {bindery_cli-0.28.0 → bindery_cli-0.29.0}/test_facility/agda.epub +0 -0
  44. {bindery_cli-0.28.0 → bindery_cli-0.29.0}/test_facility/agda_fixed.epub +0 -0
  45. {bindery_cli-0.28.0 → bindery_cli-0.29.0}/test_facility/last_man_out.epub +0 -0
  46. {bindery_cli-0.28.0 → bindery_cli-0.29.0}/test_facility/last_man_out_fixed.epub +0 -0
  47. {bindery_cli-0.28.0 → bindery_cli-0.29.0}/test_facility/leaves_of_grass.epub +0 -0
  48. {bindery_cli-0.28.0 → bindery_cli-0.29.0}/test_facility/leaves_of_grass_fixed.epub +0 -0
  49. {bindery_cli-0.28.0 → bindery_cli-0.29.0}/test_facility/leaves_of_grass_st_fixed.epub +0 -0
  50. {bindery_cli-0.28.0 → bindery_cli-0.29.0}/test_facility/leaves_of_grass_tags_fixed.epub +0 -0
  51. {bindery_cli-0.28.0 → bindery_cli-0.29.0}/test_facility/muse_of_nightmares.epub +0 -0
  52. {bindery_cli-0.28.0 → bindery_cli-0.29.0}/test_facility/muse_of_nightmares_fixed.epub +0 -0
  53. {bindery_cli-0.28.0 → bindery_cli-0.29.0}/test_facility/poor_mans_fight.epub +0 -0
  54. {bindery_cli-0.28.0 → bindery_cli-0.29.0}/test_facility/poor_mans_fight_fixed.epub +0 -0
  55. {bindery_cli-0.28.0 → bindery_cli-0.29.0}/test_facility/test_block_in_inline.py +0 -0
  56. {bindery_cli-0.28.0 → bindery_cli-0.29.0}/test_facility/test_duplicate_playorder.py +0 -0
  57. {bindery_cli-0.28.0 → bindery_cli-0.29.0}/test_facility/test_empty_body.py +0 -0
  58. {bindery_cli-0.28.0 → bindery_cli-0.29.0}/test_facility/test_illegal_tags.py +0 -0
  59. {bindery_cli-0.28.0 → bindery_cli-0.29.0}/test_facility/test_invalid_ids.py +0 -0
  60. {bindery_cli-0.28.0 → bindery_cli-0.29.0}/test_facility/test_invalid_value.py +0 -0
  61. {bindery_cli-0.28.0 → bindery_cli-0.29.0}/test_facility/test_missing_title.py +0 -0
  62. {bindery_cli-0.28.0 → bindery_cli-0.29.0}/test_facility/unworthy_gods.epub +0 -0
  63. {bindery_cli-0.28.0 → bindery_cli-0.29.0}/test_facility/unworthy_gods_fixed.epub +0 -0
  64. {bindery_cli-0.28.0 → bindery_cli-0.29.0}/testing_facility/books_to_fix/.gitkeep +0 -0
  65. {bindery_cli-0.28.0 → bindery_cli-0.29.0}/testing_facility/top500candidates/REPORT.md +0 -0
  66. {bindery_cli-0.28.0 → bindery_cli-0.29.0}/tests/test_library.py +0 -0
  67. {bindery_cli-0.28.0 → bindery_cli-0.29.0}/tests/test_pagination.py +0 -0
  68. {bindery_cli-0.28.0 → bindery_cli-0.29.0}/tests/test_reserialize.py +0 -0
  69. {bindery_cli-0.28.0 → bindery_cli-0.29.0}/tests/test_transforms.py +0 -0
  70. {bindery_cli-0.28.0 → bindery_cli-0.29.0}/tests/test_validate.py +0 -0
  71. {bindery_cli-0.28.0 → bindery_cli-0.29.0}/tests/test_version.py +0 -0
  72. {bindery_cli-0.28.0 → bindery_cli-0.29.0}/tests/test_watermark.py +0 -0
@@ -14,3 +14,8 @@ testing_facility/top500candidates/*.epub
14
14
 
15
15
  # Compiled Java scratch (FastSweep harness)
16
16
  *.class
17
+
18
+ # Local virtualenvs (each carries its own auto-generated inner .gitignore,
19
+ # which is the only reason they were invisible until now)
20
+ venv/
21
+ .venv_ci/
@@ -14,13 +14,13 @@ Born from the 2026 library audit (see the user memory `calibre-library-epubcheck
14
14
 
15
15
  - **Minimal Dependencies.** Runtime deps are exactly: `tqdm` (progress/output),
16
16
  `vir-tui` (shared TUI rendering) and `cquarry` (read-only Calibre `metadata.db` access,
17
- adopted in v0.16.0). `vir-tui` is PINNED to an exact commit in pyproject.toml
18
- (bump deliberately); `cquarry` deliberately tracks `@main` since v0.19.0 so CI always
19
- exercises the current library. `html5lib` remains the one approved
17
+ adopted in v0.16.0), pinned as PyPI ranges in pyproject.toml (`vir-tui>=2.2.0`,
18
+ `cquarry>=1.9.0`); `uv.lock` records the resolved versions. `html5lib` remains the one approved
20
19
  heavy-parsing exception (used only for the `--reserialize` fix, imported lazily so every
21
20
  other mode runs without it). Tests use the standard `unittest` framework. epubcheck is an
22
- external CLI dependency, and `calibredb` is required for the `--install-to-calibre`
23
- feature. Before adding any further Python package, stop and ask.
21
+ external CLI dependency expected on PATH; `--install-to-calibre` needs no external
22
+ binary, it re-registers the format through cquarry's write module (the calibredb
23
+ subprocess was retired in v0.24.0). Before adding any further Python package, stop and ask.
24
24
  - **Semantics-preserving transforms by default, everything else fenced behind a flag.**
25
25
  The always-on core is exactly five well-formedness fixes (prolog junk, duplicate
26
26
  `xmlns`, bare `&`, named entities, void self-closing) plus the NCX pipeline; every core
@@ -51,8 +51,10 @@ Born from the 2026 library audit (see the user memory `calibre-library-epubcheck
51
51
  visible (they clear errors), so a run with no measurable improvement is a noop and
52
52
  nothing is applied. Changing either bar means re-running the library dry run.
53
53
  - **Library writes are sacred.** Replacement must stay atomic (temp in same dir, then
54
- `os.replace`), touch only the `.epub`, preserve mode, and be dry-run by default. Calibre integrations should seamlessly swap formats using `calibredb add_format -replace`.
55
- Never write to the library without `-apply`. Test every change on `/tmp` copies first.
54
+ `os.replace`), touch only the `.epub`, preserve mode, and be dry-run by default.
55
+ Calibre format installation goes through cquarry's write module (`--install-to-calibre`;
56
+ the calibredb subprocess was retired in v0.24.0). Never write to the library without
57
+ `--apply`. Test every change on `/tmp` copies first.
56
58
 
57
59
  ## Layout
58
60
 
@@ -60,11 +62,11 @@ Born from the 2026 library audit (see the user memory `calibre-library-epubcheck
60
62
  - `src/bindery/pagination.py`: the opt-in lossy page-number strip (runhead detection, page-layer decision, block-centric removal/merge, safety nets).
61
63
  - `src/bindery/watermark.py`: the opt-in lossy watermark strip (anchored and anchorless signature removal).
62
64
  - `src/bindery/reserialize.py`: structural repair via `html5lib`.
63
- - `src/bindery/audit.py`: read-only body-text audits (`content`, `pagenumbers`, `emptytext`, `ocr`, and `monolithic` since v0.21.0, `--max-doc-chars N`) behind the `audit` subcommand (v0.15.0). Since v0.18.0 library mode resolves EPUB paths through `cquarry.get_format_path()` and can apply a tag to flagged books via `cquarry.write.WritableCalibreDB` (only with explicit `--tag`; the only sanctioned write path). Since v0.19.0 `audit --id BOOK_ID` audits a single library book through cquarry's single-entity `get_book()` fetch (no library-wide cache; supports `--tag`; incompatible with the directory argument; comma lists since v0.23.0). Since v0.22.0 every archive entry is fully read (CRC + decompression) before analysis — a damaged entry reports its own CORRUPT verdict instead of feeding emptytext — and manifest/NCX references to absent files classify as `convention` (bloated ToC, consecutive span) or `fragment` (broken span). Hazard: the scan loop reuses `tag` as its per-book display column, so the `--tag` argument is captured as `audit_tag` at function entry — do not collapse them again.
65
+ - `src/bindery/audit.py`: read-only body-text audits (`content`, `pagenumbers`, `emptytext`, `ocr`, and `monolithic` since v0.21.0, `--max-doc-chars N`) behind the `audit` subcommand (v0.15.0). Since v0.18.0 library mode resolves EPUB paths through `cquarry.get_format_path()` and can apply a tag to flagged books via `cquarry.write.WritableCalibreDB` (only with explicit `--tag`; the only sanctioned write path). Since v0.19.0 `audit --id BOOK_ID` audits a single library book through cquarry's single-entity `get_book()` fetch (no library-wide cache; supports `--tag`; incompatible with the directory argument; comma lists since v0.23.0). Since v0.22.0 every archive entry is fully read (CRC + decompression) before analysis — a damaged entry reports its own CORRUPT verdict instead of feeding emptytext — and manifest/NCX references to absent files classify as `convention` (bloated ToC, consecutive span) or `fragment` (broken span). Hazard: the scan loop reuses `tag` as its per-book display column, so the `--tag` argument is captured as `audit_tag` at function entry — do not collapse them again. Since v0.29.0 `audit --json FILE` writes per-file analyzer verdicts in the `library --json` shape (directory, library, and single-book modes; with `--id`, exactly one id; emptytext omitted from a record when the archive verdict owns the body-text story).
64
66
  - `src/bindery/epub.py`: archive rewrite, NCX uid sync, RepairReport, mismatch detection, and the opt-in structural-repair plumbing (including the CSS precondition scan).
65
67
  - `src/bindery/validate.py`: epubcheck wrapper, the `gate` (improvement) and `no_worse` (no-regression, for the lossy strips) acceptance bars.
66
68
  - `src/bindery/library.py`: Calibre walk, atomic replace, backups, and native format installation. Since v0.19.0 `CalibreIdResolver` resolves the book id from `metadata.db` through cquarry (one lazy path→id map per run; since v0.20.0 the map comes from `CalibreDB.format_path_index()`, re-normalized with `resolve().lower()` for the resolver's symlinked-directory and case-insensitive matching) — the `(id)` directory-name regex is only the no-catalog fallback. Since v0.24.0 `install_format()` places the repaired file atomically and updates the `data` row through `cquarry.write.WritableCalibreDB` (remove+add in one `batch()`; `add_format` refuses duplicates); the external `calibredb` CLI is gone. Never reintroduce directory-name guessing as the primary source: renamed/mismatched directories would replace the wrong book.
67
- - `src/bindery/cli.py`: `repair` and `library` subcommands, including `--all` and `--install-to-calibre`.
69
+ - `src/bindery/cli.py`: `repair` and `library` subcommands, including `--all` and `--install-to-calibre`; since v0.29.0 also the `run` verbs — `phase1` (pre-import vetting over loose files: the audit battery composed with one gated repair sweep; read-only until `--apply-lossy`, which IS the lossy-strip consent) and `phase3` (the scoped post-import apply step: `library --id --sweep --only all --apply --all --install-to-calibre` plus a pre/post summary; mechanically refuses unscoped sweeps with exit 2). Both take `--non-interactive` and surface open questions as `decisions_needed` in their JSON, never prompts. They drive the shipped `run_library` through the real parser, so no flag can drift between the verb and the subcommand it wraps.
68
70
  - `tests/`: transforms, end-to-end repair, atomic replace, pagination, watermarks, the audit analyzers (content/pagenumbers/emptytext/ocr/monolithic, archive integrity, spine classification), the library sweep and `CalibreIdResolver`, validate, reserialize, CLI wiring, and the version pin.
69
71
 
70
72
  ## Conventions
@@ -1,3 +1,25 @@
1
+ Metadata-Version: 2.5
2
+ Name: bindery-cli
3
+ Version: 0.29.0
4
+ Summary: Audit EPUB body text and repair broken EPUBs with deterministic, epubcheck-gated fixes; atomic in-place replacement in a Calibre library.
5
+ Author: vrnvctss
6
+ License: MIT
7
+ License-File: LICENSE
8
+ Keywords: calibre,ebook,epub,epubcheck,html-repair,repair,watermarks
9
+ Classifier: Development Status :: 5 - Production/Stable
10
+ Classifier: Environment :: Console
11
+ Classifier: Intended Audience :: End Users/Desktop
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3.14
14
+ Classifier: Topic :: Utilities
15
+ Requires-Python: >=3.14
16
+ Requires-Dist: cquarry>=1.9.0
17
+ Requires-Dist: tqdm
18
+ Requires-Dist: vir-tui>=2.2.0
19
+ Provides-Extra: reserialize
20
+ Requires-Dist: html5lib>=1.1; extra == 'reserialize'
21
+ Description-Content-Type: text/markdown
22
+
1
23
  <div align="center">
2
24
  <img src="logo.svg" width="96" height="96" alt="bindery-cli logo"/>
3
25
  <h1>bindery-cli</h1>
@@ -73,9 +95,11 @@ uv tool install "bindery-cli[reserialize]" # incl. --reserialize
73
95
 
74
96
  ## Usage
75
97
 
76
- Three verbs: `bindery repair` fixes a single EPUB epubcheck-gated, `bindery audit`
77
- reports content flaws without touching anything, and `bindery library` sweeps a Calibre
78
- library tree (dry run by default; `--apply` replaces accepted books atomically in place).
98
+ Four verbs: `bindery repair` fixes a single EPUB epubcheck-gated, `bindery audit`
99
+ reports content flaws without touching anything, `bindery library` sweeps a Calibre
100
+ library tree (dry run by default; `--apply` replaces accepted books atomically in place),
101
+ and `bindery run` composes the acquisition pathway's EPUB slices (phase-1 pre-import
102
+ vetting, phase-3 scoped post-import repair) for a calling agent.
79
103
  The sections below take each in turn.
80
104
 
81
105
  ## Auditing
@@ -115,6 +139,12 @@ cd ~/docs/Calibre\ Library && bindery audit all --id 1234,1235
115
139
  ```
116
140
  `--id` composes with `--tag`: the book is tagged only if the audit flags it.
117
141
 
142
+ **Machine-readable reports:** `--json FILE` writes the run's verdicts in the same shape `library --json` uses: one record per book with a `status` (`clean`/`problem`/`error`) and per-analyzer verdicts (`problem`, `status`, `details`), plus the always-on archive/spine verdicts (OK when they were silent). This is the contract downstream consumers of the pre-import vetting slice read.
143
+ ```sh
144
+ bindery audit all ~/Downloads/epubs --json vetting-report.json
145
+ ```
146
+ With `--id`, `--json` accepts exactly one book id (each single-book run writes the file wholesale).
147
+
118
148
  **Spine-integrity reporting:** Both `audit` and `library` reports now classify manifest/NCX references that point to absent files. A `convention` verdict means the ToC is bloated but the present documents form a consecutive chapter span (e.g., the Wandering Inn official-build pattern, safe). A `fragment` verdict means the span itself is broken.
119
149
 
120
150
  **Archive integrity:** every audit fully reads each archive entry (CRC + decompression), so a damaged download is reported CORRUPT — with the first broken entry named — instead of being mislabeled EMPTY by `emptytext`. `library --sweep` splits its `unreadable` bucket into `not_a_zip` / `truncated` / `encrypted` / `corrupt_entry`, so the right disease is visible without leaving the sweep.
@@ -165,7 +195,7 @@ bindery library ~/docs/Calibre\ Library --only all --apply --all --install-to-ca
165
195
  - `--only {fatals,ncx,all}` restricts the candidate set. `ncx` targets NCX-001 mismatches (detected without epubcheck); `fatals` needs `--audit`.
166
196
  - `--id <ids>` limits the sweep to a comma-separated list of Calibre book IDs, skipping the full library walk.
167
197
  - `--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.
168
- - `--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 launches a transparent, persistent Java daemon (`EpubcheckDaemon`) to evaluate candidates in the background, eliminating the JVM startup penalty. This reduces validation time from ~5 seconds per book down to ~0.05 seconds, allowing `--sweep` to scan a 5,000-book library in just 10 minutes. Combine with `--only fatals` for a self-contained, lightning-fast "find and fix the broken books" run.
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.
169
199
  - `--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.
170
200
  - `--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.
171
201
  - `--install-to-calibre` resolves the Calibre book id from `metadata.db` via cquarry (one read-only path→id map per run), so a hand-renamed `Author/Title (id)/` directory can never send the repaired file to the wrong book. The directory-name regex is only a no-catalog fallback; with neither, the file is saved atomically in place.
@@ -176,6 +206,41 @@ bindery library ~/docs/Calibre\ Library --only all --apply --all --install-to-ca
176
206
  - 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).
177
207
  - `repair` refuses to overwrite an existing output file unless `--force` is given.
178
208
 
209
+ ## Run slices
210
+
211
+ `bindery run` wraps the acquisition pathway's EPUB steps as verbs. They add no
212
+ repair classes of their own: they compose the shipped audit and library runners
213
+ in the documented order, and both speak `--json` for a calling agent.
214
+
215
+ **`run phase1 DIR`** is the pre-import vetting slice for a directory of loose
216
+ files: the audit battery (corruption sweep, content battery, monolithic) in one
217
+ decompression pass per book, then the gated repair sweep (epubcheck, watermark
218
+ detection, repairability) over the same directory. The verb is read-only until
219
+ `--apply-lossy` is passed, and that flag IS the recorded lossy-strip consent;
220
+ `--backup DIR` mirrors originals first (keep backups outside the vetted
221
+ directory). Consent questions never block the verb: a read-only run that finds
222
+ repairable books records an `apply_lossy` entry in the JSON's
223
+ `decisions_needed` instead of prompting, and `--non-interactive` declares that
224
+ contract for callers. Exit codes per the `library` contract: 0 clean, 1 usage,
225
+ 2 when any book is flagged, rejected, unreadable, or failed epubcheck (a
226
+ consent question alone is not trouble).
227
+
228
+ ```sh
229
+ bindery run phase1 ~/Downloads --json phase1-report.json # read-only
230
+ bindery run phase1 ~/Downloads --apply-lossy --backup /tmp/phase1-backups
231
+ ```
232
+
233
+ **`run phase3 --ids IDS`** (from the library directory) is the post-import
234
+ scoped repair sweep: exactly `library --id IDS --sweep --only all --apply --all
235
+ --install-to-calibre` plus a pre/post epubcheck summary. It mechanically
236
+ refuses an unscoped library-wide sweep with exit 2 (a full-library sweep is a
237
+ dedicated hours-long task, never a verb call), and books left partial or
238
+ unreadable surface as `decisions_needed` in the JSON.
239
+
240
+ ```sh
241
+ cd ~/docs/Calibre\ Library && bindery run phase3 --ids 5071,5072 --json phase3-report.json
242
+ ```
243
+
179
244
  ## Companion scripts
180
245
 
181
246
  `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`):
@@ -1,25 +1,3 @@
1
- Metadata-Version: 2.5
2
- Name: bindery-cli
3
- Version: 0.28.0
4
- Summary: High-speed EPUB repair surgeon with a persistent Java validation daemon, safe HTML unwrapping, and native Calibre integration.
5
- Author: vrnvctss
6
- License: MIT
7
- License-File: LICENSE
8
- Keywords: calibre,ebook,epub,epubcheck,html-repair,repair,watermarks
9
- Classifier: Development Status :: 5 - Production/Stable
10
- Classifier: Environment :: Console
11
- Classifier: Intended Audience :: End Users/Desktop
12
- Classifier: License :: OSI Approved :: MIT License
13
- Classifier: Programming Language :: Python :: 3.14
14
- Classifier: Topic :: Utilities
15
- Requires-Python: >=3.14
16
- Requires-Dist: cquarry>=1.9.0
17
- Requires-Dist: tqdm
18
- Requires-Dist: vir-tui>=2.2.0
19
- Provides-Extra: reserialize
20
- Requires-Dist: html5lib>=1.1; extra == 'reserialize'
21
- Description-Content-Type: text/markdown
22
-
23
1
  <div align="center">
24
2
  <img src="logo.svg" width="96" height="96" alt="bindery-cli logo"/>
25
3
  <h1>bindery-cli</h1>
@@ -95,9 +73,11 @@ uv tool install "bindery-cli[reserialize]" # incl. --reserialize
95
73
 
96
74
  ## Usage
97
75
 
98
- Three verbs: `bindery repair` fixes a single EPUB epubcheck-gated, `bindery audit`
99
- reports content flaws without touching anything, and `bindery library` sweeps a Calibre
100
- library tree (dry run by default; `--apply` replaces accepted books atomically in place).
76
+ Four verbs: `bindery repair` fixes a single EPUB epubcheck-gated, `bindery audit`
77
+ reports content flaws without touching anything, `bindery library` sweeps a Calibre
78
+ library tree (dry run by default; `--apply` replaces accepted books atomically in place),
79
+ and `bindery run` composes the acquisition pathway's EPUB slices (phase-1 pre-import
80
+ vetting, phase-3 scoped post-import repair) for a calling agent.
101
81
  The sections below take each in turn.
102
82
 
103
83
  ## Auditing
@@ -137,6 +117,12 @@ cd ~/docs/Calibre\ Library && bindery audit all --id 1234,1235
137
117
  ```
138
118
  `--id` composes with `--tag`: the book is tagged only if the audit flags it.
139
119
 
120
+ **Machine-readable reports:** `--json FILE` writes the run's verdicts in the same shape `library --json` uses: one record per book with a `status` (`clean`/`problem`/`error`) and per-analyzer verdicts (`problem`, `status`, `details`), plus the always-on archive/spine verdicts (OK when they were silent). This is the contract downstream consumers of the pre-import vetting slice read.
121
+ ```sh
122
+ bindery audit all ~/Downloads/epubs --json vetting-report.json
123
+ ```
124
+ With `--id`, `--json` accepts exactly one book id (each single-book run writes the file wholesale).
125
+
140
126
  **Spine-integrity reporting:** Both `audit` and `library` reports now classify manifest/NCX references that point to absent files. A `convention` verdict means the ToC is bloated but the present documents form a consecutive chapter span (e.g., the Wandering Inn official-build pattern, safe). A `fragment` verdict means the span itself is broken.
141
127
 
142
128
  **Archive integrity:** every audit fully reads each archive entry (CRC + decompression), so a damaged download is reported CORRUPT — with the first broken entry named — instead of being mislabeled EMPTY by `emptytext`. `library --sweep` splits its `unreadable` bucket into `not_a_zip` / `truncated` / `encrypted` / `corrupt_entry`, so the right disease is visible without leaving the sweep.
@@ -187,7 +173,7 @@ bindery library ~/docs/Calibre\ Library --only all --apply --all --install-to-ca
187
173
  - `--only {fatals,ncx,all}` restricts the candidate set. `ncx` targets NCX-001 mismatches (detected without epubcheck); `fatals` needs `--audit`.
188
174
  - `--id <ids>` limits the sweep to a comma-separated list of Calibre book IDs, skipping the full library walk.
189
175
  - `--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.
190
- - `--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 launches a transparent, persistent Java daemon (`EpubcheckDaemon`) to evaluate candidates in the background, eliminating the JVM startup penalty. This reduces validation time from ~5 seconds per book down to ~0.05 seconds, allowing `--sweep` to scan a 5,000-book library in just 10 minutes. Combine with `--only fatals` for a self-contained, lightning-fast "find and fix the broken books" run.
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.
191
177
  - `--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.
192
178
  - `--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.
193
179
  - `--install-to-calibre` resolves the Calibre book id from `metadata.db` via cquarry (one read-only path→id map per run), so a hand-renamed `Author/Title (id)/` directory can never send the repaired file to the wrong book. The directory-name regex is only a no-catalog fallback; with neither, the file is saved atomically in place.
@@ -198,6 +184,41 @@ bindery library ~/docs/Calibre\ Library --only all --apply --all --install-to-ca
198
184
  - 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).
199
185
  - `repair` refuses to overwrite an existing output file unless `--force` is given.
200
186
 
187
+ ## Run slices
188
+
189
+ `bindery run` wraps the acquisition pathway's EPUB steps as verbs. They add no
190
+ repair classes of their own: they compose the shipped audit and library runners
191
+ in the documented order, and both speak `--json` for a calling agent.
192
+
193
+ **`run phase1 DIR`** is the pre-import vetting slice for a directory of loose
194
+ files: the audit battery (corruption sweep, content battery, monolithic) in one
195
+ decompression pass per book, then the gated repair sweep (epubcheck, watermark
196
+ detection, repairability) over the same directory. The verb is read-only until
197
+ `--apply-lossy` is passed, and that flag IS the recorded lossy-strip consent;
198
+ `--backup DIR` mirrors originals first (keep backups outside the vetted
199
+ directory). Consent questions never block the verb: a read-only run that finds
200
+ repairable books records an `apply_lossy` entry in the JSON's
201
+ `decisions_needed` instead of prompting, and `--non-interactive` declares that
202
+ contract for callers. Exit codes per the `library` contract: 0 clean, 1 usage,
203
+ 2 when any book is flagged, rejected, unreadable, or failed epubcheck (a
204
+ consent question alone is not trouble).
205
+
206
+ ```sh
207
+ bindery run phase1 ~/Downloads --json phase1-report.json # read-only
208
+ bindery run phase1 ~/Downloads --apply-lossy --backup /tmp/phase1-backups
209
+ ```
210
+
211
+ **`run phase3 --ids IDS`** (from the library directory) is the post-import
212
+ scoped repair sweep: exactly `library --id IDS --sweep --only all --apply --all
213
+ --install-to-calibre` plus a pre/post epubcheck summary. It mechanically
214
+ refuses an unscoped library-wide sweep with exit 2 (a full-library sweep is a
215
+ dedicated hours-long task, never a verb call), and books left partial or
216
+ unreadable surface as `decisions_needed` in the JSON.
217
+
218
+ ```sh
219
+ cd ~/docs/Calibre\ Library && bindery run phase3 --ids 5071,5072 --json phase3-report.json
220
+ ```
221
+
201
222
  ## Companion scripts
202
223
 
203
224
  `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`):
@@ -1,4 +1,57 @@
1
1
  # bindery-cli Patch Notes
2
+ ## v0.29.0 (2026-09-06)
3
+
4
+ ### Phase 13: machine-readable audit and the acquisition run slices
5
+
6
+ The acquisition pathway's EPUB steps are now first-class CLI surface. Nothing
7
+ here adds a repair class or changes an existing mode; the new verbs compose
8
+ what already ships, in the phase-1 and phase-3 skills' documented order.
9
+
10
+ - **`audit --json FILE`**: the audit subcommand writes machine-readable
11
+ per-file verdicts in the `library --json` shape (mode/root/summary plus one
12
+ record per book), from all three modes: directory, library, and single-book.
13
+ Records carry a status (clean/problem/error) and per-analyzer verdicts
14
+ (problem/status/details); the always-on archive/spine verdicts appear OK
15
+ when they were silent, emptytext is omitted from a record when the archive
16
+ verdict owns the body-text story, and a scan error becomes its own record
17
+ shape. `--json` with `--id` accepts exactly one book id, because each
18
+ single-book run writes the file wholesale. This is the contract downstream
19
+ consumers of the phase-1 EPUB slice read.
20
+ - **`run phase1 DIR [--json OUT] [--apply-lossy] [--backup DIR]`**: the
21
+ pre-import vetting slice over a directory of loose files, in the phase-1
22
+ skill's documented order: the audit battery (corruption sweep, content
23
+ battery, monolithic), then one fused repair sweep (epubcheck, watermark
24
+ detection, repairability; the skill's two sweeps collapse into one
25
+ epubcheck pass, with watermark hits read out of the per-book fix summary).
26
+ Read-only until `--apply-lossy`, which IS the recorded lossy-strip consent;
27
+ `--backup DIR` passes through (keep backups outside the vetted directory).
28
+ Exit codes 0/1/2 per the library contract; a consent question alone is not
29
+ trouble.
30
+ - **`run phase3 --ids CSV [--json OUT]`**: the post-import scoped repair step,
31
+ run from the library directory: exactly `library --id --sweep --only all
32
+ --apply --all --install-to-calibre` plus a pre/post epubcheck summary over
33
+ the swept books. The phase-3 skill's step-10 scope warning is mechanized: an
34
+ unscoped library-wide call is refused with exit 2, never run by accident.
35
+ The verb drives the shipped library runner through the real parser, so its
36
+ flags cannot drift from the subcommand it wraps.
37
+ - **Non-interactive contract**: both run verbs take `--non-interactive` and
38
+ never prompt (nothing in bindery prompts); open questions surface in the
39
+ JSON as `decisions_needed` entries: the `apply_lossy` consent question in a
40
+ read-only phase1, `manual_repair`/`investigate` for books left partial or
41
+ unreadable in phase3.
42
+ - **Skill sync**: the phase-1 and phase-3 skills name the run verbs and the
43
+ machine-readable contracts (the skill files live in the library directory,
44
+ outside this repo).
45
+ - **Dependencies**: vir-tui 2.3.0 folded into the lock; the pyproject floor
46
+ stays `vir-tui>=2.2.0` (additive bump at this release).
47
+ - **Docs and hygiene**: CLAUDE.md's last surviving `calibredb add_format`
48
+ line retired (it contradicted the v0.24.0 subprocess removal and carried a
49
+ `-apply` typo); README's `--sweep` bullet and the pyproject description no
50
+ longer advertise the validation daemon's speedup, which never engages
51
+ because the daemon's classpath omits epubcheck's `lib/` dependencies (the
52
+ fix is a recorded gate-behavior decision, not a drive-by); `venv/` and
53
+ `.venv_ci/` are gitignored. Test count: 277 → 294.
54
+
2
55
  ## v0.28.0 (2026-09-05)
3
56
 
4
57
  ### Phase 12: repairs for the top epubcheck error codes
@@ -4,8 +4,8 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "bindery-cli"
7
- version = "0.28.0"
8
- description = "High-speed EPUB repair surgeon with a persistent Java validation daemon, safe HTML unwrapping, and native Calibre integration."
7
+ version = "0.29.0"
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 = [
11
11
  "Development Status :: 5 - Production/Stable",
@@ -23,9 +23,7 @@ authors = [
23
23
  ]
24
24
  # Core dependencies are minimal. html5lib is needed only for the opt-in --reserialize structural
25
25
  # repair; it is imported lazily, so every other mode works without it installed.
26
- # vir-tui is PINNED to an exact commit so builds are reproducible; bump deliberately.
27
- # cquarry deliberately tracks @main (since v0.19.0): CI exercises current cquarry on
28
- # every run instead of a stale snapshot, matching CalibreQuarry's dependency policy.
26
+ # vir-tui and cquarry are pinned as PyPI ranges; uv.lock records the resolved versions.
29
27
  dependencies = [
30
28
  "tqdm",
31
29
  "vir-tui>=2.2.0",
@@ -321,6 +321,55 @@ Three consequences worth stating, because they change existing plans:
321
321
  This is one plugin *shape* serving both tools, not one shared plugin: they are
322
322
  separate repos with separate licences and no dependency between them.
323
323
 
324
+ **RESEARCH 2026-09-05 (against the calibre 9.14 reference clone, which matches the
325
+ installed Calibre). The decision holds. Three corrections and a scoped spec; the
326
+ plugin is approved to build (Brandon, 2026-09-05).**
327
+
328
+ - The `on_import` contract is exactly as decided: `run(path)` returns a
329
+ replacement built with `temporary_file()`, Calibre swaps which path flows
330
+ downstream, and the original on disk is never touched. A raising plugin cannot
331
+ abort an import and cannot lose the file (Calibre falls back to the original),
332
+ but the failure is silent: tracebacks land only in Calibre's debug log. The
333
+ plugin therefore never raises and keeps its own log under
334
+ `calibre.constants.config_dir`.
335
+ - Loader trap the decision missed: `supported_platforms` defaults to empty and
336
+ the loader rejects any plugin that declares no platforms. The plugin must set
337
+ `supported_platforms = ['linux']`, which also makes other OSes a
338
+ loader-enforced non-goal.
339
+ - The hook also fires when a format is added to an existing book
340
+ (`db.add_format(..., run_hooks=True)`), so the plugin must be byte-idempotent.
341
+ The core pass already is (deterministic since v0.10.2).
342
+ - The "both tools" wording is vacuous today: oceanstrip was absorbed at v0.12.0
343
+ and the standalone repo is gone. One plugin, in this repo, honoring the shape
344
+ claim retroactively.
345
+
346
+ Scoped spec:
347
+
348
+ - **Vendor slice**, generated at release from the tagged tree with a
349
+ byte-equality drift test: `transforms.py`, `epub.py`, `pagination.py`,
350
+ `watermark.py`, `reserialize.py` (~2,300 lines) plus a new plugin
351
+ `__init__.py`. `cli.py`/`library.py`/`audit.py`/`validate.py` stay out: they
352
+ carry tqdm, cquarry, vir-tui, and the epubcheck/JDK machinery, none of which
353
+ a plugin zip can carry (plugins cannot pip-install).
354
+ - **Active fix set = the gate-safe default pass only**: the five well-formedness
355
+ transforms plus the NCX pipeline. All twelve structural repairs and the three
356
+ lossy strips stay CLI-only: their acceptance IS the epubcheck gate, which
357
+ cannot run inside Calibre (no jar, no JVM, and seconds-per-book latency).
358
+ Opt-in flags are never enabled by the plugin; nothing runs ungated.
359
+ - **Behavior**: core pass on `*.epub` imports; zero fixes or any exception
360
+ returns the original path unchanged; the rewritten archive is re-opened and
361
+ `testzip()`-verified before it is handed back; one log line per book (fixed /
362
+ no fixes / refused / errored). Config via `site_customization` JSON
363
+ (`log`, `log_path`, `max_size_mb` refusal cap). No Qt config widget, no
364
+ `postimport` hook (that would write `metadata.db`, breaking the decision).
365
+ - **Release**: `publish.yml` gains a job that regenerates the vendor slice from
366
+ the tag, runs the tests against it, zips it, and attaches
367
+ `BinderyRepair-v<VERSION>.zip` to the GitHub release beside the PyPI artifact.
368
+
369
+ Open questions (Brandon): epubcheck-on-PATH experimental mode (default no, the
370
+ latency ruling stands); plugin identity `Bindery Repair` / `bindery_repair`;
371
+ size-cap default.
372
+
324
373
 
325
374
  ## Supplementary Phase (Based on Library Audit)
326
375
  Following a comprehensive epubcheck sweep of a 5,043-book Calibre library on 2026-08-22 (~3,228 candidates for repair), several recurring error schemas were identified; all have since shipped. (This section absorbs the one-off `BINDERY_REPORT.md`, retired in v0.17.0 — its performance notes live in the v0.13.0 patchnotes, and its findings are the checked items below.)
@@ -629,3 +678,75 @@ the underlying conditions are what the repairs target:
629
678
  gate that deserves its own decision, not a drive-by edit.
630
679
 
631
680
  Non-goals: No bulk repair of RSC-005 schema/content-model violations. With 164,302 instances, arbitrary schema repair violates the explicit non-goal in spec.md ("Fixing RSC-005 schema/content-model violations in bulk"). Restructuring arbitrary markup requires semantic human judgment; attempting to automate it risks silent content corruption, tag swallowing, or destroying valid styling. Specific, narrowly bounded RSC-005 sub-cases remain opt-in under their own flags (`--strip-epub3-attrs`, `--downgrade-epub3-tags`, `--unwrap-block-in-inline`, `--strip-invalid-value`, `--unwrap-illegal-tags`, `--add-img-alt`, `--fix-id-colons`); arbitrary bulk repair is rejected. No spine-item manifest pruning (missing spine documents indicate corrupt archives or broken fragments, not safe pruning candidates). No text deletion in anchor unwrapping (character conservation is absolute).
681
+
682
+ ## Phase 13: machine-readable audit + the acquisition run slices (proposed 2026-09-05)
683
+
684
+ *Context: the acquisition pathway (`~/docs/Calibre Library/.claude/skills/`) is
685
+ being promoted from agent-prose into first-class CLI subcommands across the lane.
686
+ Brandon's design: `bindery run phase1` for the EPUB slice of pre-import vetting,
687
+ `bindery run phase3` for the scoped post-import repair pass, with CalibreQuarry
688
+ owning the orchestration (`cquarry run phase1/2/3`, its roadmap Phase 17) and the
689
+ manifest that hands state between phases. The prerequisite on this side is a
690
+ machine-readable audit; the run verbs wrap what already ships. No new repair
691
+ classes, no behavior change to existing modes.*
692
+
693
+ - [x] **`audit --json FILE`**: per-file analyzer verdicts in the same shape
694
+ `library --json` already emits (audit is console-only today; the
695
+ subparser has no JSON flag). This is the contract every downstream
696
+ consumer of the phase-1 EPUB slice reads.
697
+ *(Shipped in v0.29.0. All three modes write it: directory, library,
698
+ and single-book. The always-on archive/spine verdicts appear OK when
699
+ they were silent; emptytext is omitted from a record when the archive
700
+ verdict owns the body-text story; a scan error becomes its own record
701
+ shape (`status: "error"`). `--json` with `--id` accepts exactly one id,
702
+ because each single-book run writes the file wholesale.)*
703
+ - [x] **`run phase1 DIR [--json OUT] [--apply-lossy] [--backup DIR]`**: the
704
+ EPUB slice in the phase-1 skill's documented order (corruption sweep,
705
+ epubcheck, content battery, monolithic, watermark dry-run,
706
+ repairability). Gated repair applies only under `--apply-lossy`, which is
707
+ the recorded lossy-strip consent; without it the verb is read-only.
708
+ Exit codes 0/1/2 per the `library` contract. No DRM scan, no duplicate
709
+ screen, no manifest assembly (CalibreQuarry's by the frontend split).
710
+ *(Shipped in v0.29.0. Two composed stages: the audit battery through its
711
+ own --json payload, then ONE fused repair sweep with the --all set — the
712
+ skill ran watermark detection and repairability as two sweeps, the verb
713
+ pays epubcheck once and reads watermark hits out of the per-book fix
714
+ summary. Read-only until --apply-lossy; --backup passes through (keep
715
+ backups outside the vetted directory). A consent question is not an
716
+ error: it lands in decisions_needed and the exit code stays on the
717
+ library contract.)*
718
+ - [x] **`run phase3 --ids CSV [--json OUT]`**: wraps
719
+ `library --id ... --sweep --only all --apply --all --install-to-calibre`
720
+ with a pre/post summary and mechanically refuses unscoped library-wide
721
+ sweeps (encodes the phase-3 skill's step-10 scope warning as an exit 2
722
+ instead of a prose warning).
723
+ *(Shipped in v0.29.0. Run from the library directory (cwd-resolved like
724
+ audit library mode). The verb drives the shipped library runner through
725
+ the real parser, so its flags cannot drift from the subcommand it wraps.
726
+ The pre/post summary sums before/after epubcheck counts over the swept
727
+ books; books left partial or unreadable surface as decisions_needed.
728
+ The verb is the apply step by design — the batch was vetted in phase 1,
729
+ and the gate plus the atomic-replacement contract still govern every
730
+ write.)*
731
+ - [x] **Non-interactive contract**: every `run` verb takes
732
+ `--non-interactive`; prompts never fire off a TTY, and open questions
733
+ surface in the JSON as `decisions_needed` for the calling agent or user.
734
+ *(Shipped in v0.29.0 with the verbs themselves. Nothing in a run verb
735
+ prompts (none ever did; the flag makes the orchestrator contract
736
+ explicit and is recorded in the payload). decisions_needed entries: the
737
+ apply_lossy consent question in a read-only phase1, manual_repair and
738
+ investigate for partial/unreadable books in phase3.)*
739
+ - [x] **Skill sync**: the phase-1 and phase-3 skills name the run verbs once
740
+ shipped. Floor, not ceiling, per the standing rule.
741
+ *(Done for v0.29.0, 2026-09-06: the phase-1 skill's EPUB section names
742
+ `run phase1` as the one-verb wrapper (the fused sweep pays epubcheck
743
+ once; the JSON contract and `--apply-lossy` consent named) and the
744
+ phase-3 skill's step 10 names `run phase3` with the mechanized scope
745
+ refusal. The skill files live in the library directory, outside this
746
+ repo; per the floor rule their per-check commands still stand on their
747
+ own.)*
748
+
749
+ Non-goals: no manifest format ownership (CalibreQuarry owns the
750
+ `acquisition-manifest` schema and the orchestration; this repo emits reports
751
+ and consumes id lists); no TUI beyond the existing audit rendering; no PDF/DJVU
752
+ work (never this repo's charter).
@@ -47,7 +47,9 @@ def find_jar(explicit: str | None) -> Path:
47
47
  elif launcher := shutil.which("epubcheck"):
48
48
  m = _JAR_RE.search(Path(launcher).read_text())
49
49
  if not m:
50
- sys.exit(f"error: cannot find a jar path in the epubcheck launcher {launcher}")
50
+ sys.exit(
51
+ f"error: cannot find a jar path in the epubcheck launcher {launcher}"
52
+ )
51
53
  jar = Path(os.path.expandvars(m.group(1))).expanduser()
52
54
  else:
53
55
  sys.exit("error: epubcheck not found; install it or pass --jar / EPUBCHECK_JAR")
@@ -80,9 +82,7 @@ def collect_paths(args: argparse.Namespace) -> list[str]:
80
82
  return [p.strip() for p in text.splitlines() if p.strip()]
81
83
  if not args.root.is_dir():
82
84
  sys.exit(f"error: not a directory: {args.root}")
83
- return sorted(
84
- str(p) for p in args.root.rglob("*") if p.suffix.lower() == ".epub"
85
- )
85
+ return sorted(str(p) for p in args.root.rglob("*") if p.suffix.lower() == ".epub")
86
86
 
87
87
 
88
88
  def run(jar: Path, paths: list[str], mode: str) -> list[str]:
@@ -129,12 +129,21 @@ def summarize(lines: list[str]) -> None:
129
129
 
130
130
  def main() -> int:
131
131
  ap = argparse.ArgumentParser(description=__doc__.splitlines()[0])
132
- ap.add_argument("root", nargs="?", type=Path, help="directory to scan for .epub files")
132
+ ap.add_argument(
133
+ "root", nargs="?", type=Path, help="directory to scan for .epub files"
134
+ )
133
135
  ap.add_argument("--mode", choices=("audit", "extract"), default="audit")
134
136
  ap.add_argument("--from-file", help="read EPUB paths from this file ('-' = stdin)")
135
- ap.add_argument("--jar", help="epubcheck.jar path (default: EPUBCHECK_JAR or the epubcheck launcher)")
137
+ ap.add_argument(
138
+ "--jar",
139
+ help="epubcheck.jar path (default: EPUBCHECK_JAR or the epubcheck launcher)",
140
+ )
136
141
  ap.add_argument("--out", type=Path, help="also write the raw output to this file")
137
- ap.add_argument("--summary", action="store_true", help="aggregate extract-mode output into a per-code report")
142
+ ap.add_argument(
143
+ "--summary",
144
+ action="store_true",
145
+ help="aggregate extract-mode output into a per-code report",
146
+ )
138
147
  args = ap.parse_args()
139
148
  if not args.from_file and args.root is None:
140
149
  ap.error("a directory or --from-file is required")
@@ -291,6 +291,38 @@ bumps `books.last_modified`, and cleans link tables before tag deletion). THIN e
291
291
  advisories stay untagged; already-tagged books are skipped; a missing file is a scan error, not a
292
292
  silent skip.
293
293
 
294
+ `--json FILE` (v0.29.0) writes the same verdicts machine-readably, in the `library --json`
295
+ shape: one record per file with a `status` (`clean`, `problem`, or `error`) and per-analyzer
296
+ verdicts (`problem`, `status`, `details`); the always-on archive/spine verdicts appear OK when
297
+ they were silent, emptytext is omitted when the archive verdict owns the book's body-text story,
298
+ and a scan error becomes its own record (`status: "error"`, an `error` message, no verdicts).
299
+ All three modes write it (directory, library, and single-book); `--json` with `--id` accepts
300
+ exactly one book id, since each single-book run writes the file wholesale.
301
+
302
+ ## Run verbs (acquisition slices)
303
+
304
+ `bindery run phase1 DIR [--json FILE] [--apply-lossy] [--backup DIR] [--non-interactive]`
305
+ composes the shipped read-only audit battery (corruption sweep, content battery,
306
+ monolithic) with the gated repair sweep (epubcheck, watermark detection,
307
+ repairability) over one directory of loose files, in the phase-1 skill's
308
+ documented order. It introduces no new repair classes: every fix, gate, and
309
+ safety net is the shipped one. The verb is read-only until `--apply-lossy` is
310
+ passed, and that flag is the recorded lossy-strip consent; `--backup DIR`
311
+ mirrors originals before any replacement. Consent questions never block the
312
+ verb: they surface as `decisions_needed` entries in the JSON (never prompts),
313
+ and `--non-interactive` declares that contract for callers. Exit codes per the
314
+ library contract: 0 clean, 1 usage, 2 when any book is flagged, rejected,
315
+ unreadable, or failed epubcheck; a consent question alone is not trouble.
316
+
317
+ `bindery run phase3 --ids IDS [--json FILE] [--non-interactive]` (run from the
318
+ library directory) is exactly `library --id IDS --sweep --only all --apply
319
+ --all --install-to-calibre` plus a pre/post epubcheck summary over the swept
320
+ books. It is the post-import apply step by design: the batch was vetted in
321
+ phase 1, the epubcheck gate still governs every replacement, and the atomic
322
+ replacement contract is untouched. The scope refusal is mechanical: no `--ids`,
323
+ no sweep, exit 2 (a library-wide sweep is a dedicated hours-long task, never a
324
+ verb call). Books left partial or unreadable surface as `decisions_needed`.
325
+
294
326
  ## Out of scope (non-goals)
295
327
 
296
328
  - Fixing RSC-005 schema/content-model violations in bulk.
@@ -4,7 +4,7 @@ strip), epubcheck-gated.
4
4
  Single source of truth for the version. pyproject.toml must match VERSION below.
5
5
  """
6
6
 
7
- VERSION = "0.28.0"
7
+ VERSION = "0.29.0"
8
8
  __version__ = VERSION
9
9
 
10
10
  from .epub import RepairReport, repair_epub # noqa: E402