bindery-cli 0.28.0__tar.gz → 0.30.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.
- {bindery_cli-0.28.0 → bindery_cli-0.30.0}/.gitignore +5 -0
- {bindery_cli-0.28.0 → bindery_cli-0.30.0}/CLAUDE.md +11 -9
- bindery_cli-0.28.0/README.md → bindery_cli-0.30.0/PKG-INFO +70 -4
- bindery_cli-0.28.0/PKG-INFO → bindery_cli-0.30.0/README.md +48 -26
- {bindery_cli-0.28.0 → bindery_cli-0.30.0}/patchnotes.md +69 -0
- {bindery_cli-0.28.0 → bindery_cli-0.30.0}/pyproject.toml +3 -5
- {bindery_cli-0.28.0 → bindery_cli-0.30.0}/roadmap.md +130 -2
- {bindery_cli-0.28.0 → bindery_cli-0.30.0}/scripts/fast_sweep.py +16 -7
- {bindery_cli-0.28.0 → bindery_cli-0.30.0}/spec.md +37 -0
- {bindery_cli-0.28.0 → bindery_cli-0.30.0}/src/bindery/__init__.py +1 -1
- {bindery_cli-0.28.0 → bindery_cli-0.30.0}/src/bindery/audit.py +186 -4
- {bindery_cli-0.28.0 → bindery_cli-0.30.0}/src/bindery/cli.py +489 -11
- {bindery_cli-0.28.0 → bindery_cli-0.30.0}/src/bindery/epub.py +1 -0
- {bindery_cli-0.28.0 → bindery_cli-0.30.0}/tests/test_audit.py +200 -0
- {bindery_cli-0.28.0 → bindery_cli-0.30.0}/tests/test_cli.py +431 -0
- {bindery_cli-0.28.0 → bindery_cli-0.30.0}/tests/test_epub.py +12 -12
- {bindery_cli-0.28.0 → bindery_cli-0.30.0}/uv.lock +4 -4
- {bindery_cli-0.28.0 → bindery_cli-0.30.0}/.github/workflows/ci.yml +0 -0
- {bindery_cli-0.28.0 → bindery_cli-0.30.0}/.github/workflows/publish.yml +0 -0
- {bindery_cli-0.28.0 → bindery_cli-0.30.0}/AGENTS.md +0 -0
- {bindery_cli-0.28.0 → bindery_cli-0.30.0}/LICENSE +0 -0
- {bindery_cli-0.28.0 → bindery_cli-0.30.0}/docs/screenshots/library-sweep.png +0 -0
- {bindery_cli-0.28.0 → bindery_cli-0.30.0}/logo.svg +0 -0
- {bindery_cli-0.28.0 → bindery_cli-0.30.0}/run_tests.sh +0 -0
- {bindery_cli-0.28.0 → bindery_cli-0.30.0}/scripts/FastSweep.java +0 -0
- {bindery_cli-0.28.0 → bindery_cli-0.30.0}/scripts/find_block_in_inline.py +0 -0
- {bindery_cli-0.28.0 → bindery_cli-0.30.0}/scripts/find_css_illegal_tags.py +0 -0
- {bindery_cli-0.28.0 → bindery_cli-0.30.0}/scripts/find_duplicate_playorder.py +0 -0
- {bindery_cli-0.28.0 → bindery_cli-0.30.0}/scripts/find_empty_body.py +0 -0
- {bindery_cli-0.28.0 → bindery_cli-0.30.0}/scripts/find_illegal_tags.py +0 -0
- {bindery_cli-0.28.0 → bindery_cli-0.30.0}/scripts/find_invalid_ids.py +0 -0
- {bindery_cli-0.28.0 → bindery_cli-0.30.0}/scripts/find_invalid_st_tag.py +0 -0
- {bindery_cli-0.28.0 → bindery_cli-0.30.0}/scripts/find_invalid_value_attr.py +0 -0
- {bindery_cli-0.28.0 → bindery_cli-0.30.0}/scripts/find_missing_images.py +0 -0
- {bindery_cli-0.28.0 → bindery_cli-0.30.0}/scripts/find_missing_title.py +0 -0
- {bindery_cli-0.28.0 → bindery_cli-0.30.0}/src/bindery/__main__.py +0 -0
- {bindery_cli-0.28.0 → bindery_cli-0.30.0}/src/bindery/library.py +0 -0
- {bindery_cli-0.28.0 → bindery_cli-0.30.0}/src/bindery/pagination.py +0 -0
- {bindery_cli-0.28.0 → bindery_cli-0.30.0}/src/bindery/reserialize.py +0 -0
- {bindery_cli-0.28.0 → bindery_cli-0.30.0}/src/bindery/transforms.py +0 -0
- {bindery_cli-0.28.0 → bindery_cli-0.30.0}/src/bindery/validate.py +0 -0
- {bindery_cli-0.28.0 → bindery_cli-0.30.0}/src/bindery/watermark.py +0 -0
- {bindery_cli-0.28.0 → bindery_cli-0.30.0}/test_facility/agda.epub +0 -0
- {bindery_cli-0.28.0 → bindery_cli-0.30.0}/test_facility/agda_fixed.epub +0 -0
- {bindery_cli-0.28.0 → bindery_cli-0.30.0}/test_facility/last_man_out.epub +0 -0
- {bindery_cli-0.28.0 → bindery_cli-0.30.0}/test_facility/last_man_out_fixed.epub +0 -0
- {bindery_cli-0.28.0 → bindery_cli-0.30.0}/test_facility/leaves_of_grass.epub +0 -0
- {bindery_cli-0.28.0 → bindery_cli-0.30.0}/test_facility/leaves_of_grass_fixed.epub +0 -0
- {bindery_cli-0.28.0 → bindery_cli-0.30.0}/test_facility/leaves_of_grass_st_fixed.epub +0 -0
- {bindery_cli-0.28.0 → bindery_cli-0.30.0}/test_facility/leaves_of_grass_tags_fixed.epub +0 -0
- {bindery_cli-0.28.0 → bindery_cli-0.30.0}/test_facility/muse_of_nightmares.epub +0 -0
- {bindery_cli-0.28.0 → bindery_cli-0.30.0}/test_facility/muse_of_nightmares_fixed.epub +0 -0
- {bindery_cli-0.28.0 → bindery_cli-0.30.0}/test_facility/poor_mans_fight.epub +0 -0
- {bindery_cli-0.28.0 → bindery_cli-0.30.0}/test_facility/poor_mans_fight_fixed.epub +0 -0
- {bindery_cli-0.28.0 → bindery_cli-0.30.0}/test_facility/test_block_in_inline.py +0 -0
- {bindery_cli-0.28.0 → bindery_cli-0.30.0}/test_facility/test_duplicate_playorder.py +0 -0
- {bindery_cli-0.28.0 → bindery_cli-0.30.0}/test_facility/test_empty_body.py +0 -0
- {bindery_cli-0.28.0 → bindery_cli-0.30.0}/test_facility/test_illegal_tags.py +0 -0
- {bindery_cli-0.28.0 → bindery_cli-0.30.0}/test_facility/test_invalid_ids.py +0 -0
- {bindery_cli-0.28.0 → bindery_cli-0.30.0}/test_facility/test_invalid_value.py +0 -0
- {bindery_cli-0.28.0 → bindery_cli-0.30.0}/test_facility/test_missing_title.py +0 -0
- {bindery_cli-0.28.0 → bindery_cli-0.30.0}/test_facility/unworthy_gods.epub +0 -0
- {bindery_cli-0.28.0 → bindery_cli-0.30.0}/test_facility/unworthy_gods_fixed.epub +0 -0
- {bindery_cli-0.28.0 → bindery_cli-0.30.0}/testing_facility/books_to_fix/.gitkeep +0 -0
- {bindery_cli-0.28.0 → bindery_cli-0.30.0}/testing_facility/top500candidates/REPORT.md +0 -0
- {bindery_cli-0.28.0 → bindery_cli-0.30.0}/tests/test_library.py +0 -0
- {bindery_cli-0.28.0 → bindery_cli-0.30.0}/tests/test_pagination.py +0 -0
- {bindery_cli-0.28.0 → bindery_cli-0.30.0}/tests/test_reserialize.py +0 -0
- {bindery_cli-0.28.0 → bindery_cli-0.30.0}/tests/test_transforms.py +0 -0
- {bindery_cli-0.28.0 → bindery_cli-0.30.0}/tests/test_validate.py +0 -0
- {bindery_cli-0.28.0 → bindery_cli-0.30.0}/tests/test_version.py +0 -0
- {bindery_cli-0.28.0 → bindery_cli-0.30.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)
|
|
18
|
-
|
|
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
|
|
23
|
-
|
|
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.
|
|
55
|
-
|
|
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. `library --workers N` (v0.30.0) parallelizes only the sweep's candidate pass, in windows of N consumed in order so `--limit` stays lazy; the repair phase is never parallel (shared workdir, atomic-replacement contract).
|
|
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.30.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
|
-
|
|
77
|
-
reports content flaws without touching anything,
|
|
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,8 @@ 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
|
|
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.
|
|
199
|
+
- `--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.
|
|
169
200
|
- `--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
201
|
- `--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
202
|
- `--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 +207,41 @@ bindery library ~/docs/Calibre\ Library --only all --apply --all --install-to-ca
|
|
|
176
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).
|
|
177
208
|
- `repair` refuses to overwrite an existing output file unless `--force` is given.
|
|
178
209
|
|
|
210
|
+
## Run slices
|
|
211
|
+
|
|
212
|
+
`bindery run` wraps the acquisition pathway's EPUB steps as verbs. They add no
|
|
213
|
+
repair classes of their own: they compose the shipped audit and library runners
|
|
214
|
+
in the documented order, and both speak `--json` for a calling agent.
|
|
215
|
+
|
|
216
|
+
**`run phase1 DIR`** is the pre-import vetting slice for a directory of loose
|
|
217
|
+
files: the audit battery (corruption sweep, content battery, monolithic) in one
|
|
218
|
+
decompression pass per book, then the gated repair sweep (epubcheck, watermark
|
|
219
|
+
detection, repairability) over the same directory. The verb is read-only until
|
|
220
|
+
`--apply-lossy` is passed, and that flag IS the recorded lossy-strip consent;
|
|
221
|
+
`--backup DIR` mirrors originals first (keep backups outside the vetted
|
|
222
|
+
directory). Consent questions never block the verb: a read-only run that finds
|
|
223
|
+
repairable books records an `apply_lossy` entry in the JSON's
|
|
224
|
+
`decisions_needed` instead of prompting, and `--non-interactive` declares that
|
|
225
|
+
contract for callers. Exit codes per the `library` contract: 0 clean, 1 usage,
|
|
226
|
+
2 when any book is flagged, rejected, unreadable, or failed epubcheck (a
|
|
227
|
+
consent question alone is not trouble).
|
|
228
|
+
|
|
229
|
+
```sh
|
|
230
|
+
bindery run phase1 ~/Downloads --json phase1-report.json # read-only
|
|
231
|
+
bindery run phase1 ~/Downloads --apply-lossy --backup /tmp/phase1-backups
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
**`run phase3 --ids IDS`** (from the library directory) is the post-import
|
|
235
|
+
scoped repair sweep: exactly `library --id IDS --sweep --only all --apply --all
|
|
236
|
+
--install-to-calibre` plus a pre/post epubcheck summary. It mechanically
|
|
237
|
+
refuses an unscoped library-wide sweep with exit 2 (a full-library sweep is a
|
|
238
|
+
dedicated hours-long task, never a verb call), and books left partial or
|
|
239
|
+
unreadable surface as `decisions_needed` in the JSON.
|
|
240
|
+
|
|
241
|
+
```sh
|
|
242
|
+
cd ~/docs/Calibre\ Library && bindery run phase3 --ids 5071,5072 --json phase3-report.json
|
|
243
|
+
```
|
|
244
|
+
|
|
179
245
|
## Companion scripts
|
|
180
246
|
|
|
181
247
|
`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
|
-
|
|
99
|
-
reports content flaws without touching anything,
|
|
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,8 @@ 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
|
|
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.
|
|
177
|
+
- `--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.
|
|
191
178
|
- `--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
179
|
- `--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
180
|
- `--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 +185,41 @@ bindery library ~/docs/Calibre\ Library --only all --apply --all --install-to-ca
|
|
|
198
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).
|
|
199
186
|
- `repair` refuses to overwrite an existing output file unless `--force` is given.
|
|
200
187
|
|
|
188
|
+
## Run slices
|
|
189
|
+
|
|
190
|
+
`bindery run` wraps the acquisition pathway's EPUB steps as verbs. They add no
|
|
191
|
+
repair classes of their own: they compose the shipped audit and library runners
|
|
192
|
+
in the documented order, and both speak `--json` for a calling agent.
|
|
193
|
+
|
|
194
|
+
**`run phase1 DIR`** is the pre-import vetting slice for a directory of loose
|
|
195
|
+
files: the audit battery (corruption sweep, content battery, monolithic) in one
|
|
196
|
+
decompression pass per book, then the gated repair sweep (epubcheck, watermark
|
|
197
|
+
detection, repairability) over the same directory. The verb is read-only until
|
|
198
|
+
`--apply-lossy` is passed, and that flag IS the recorded lossy-strip consent;
|
|
199
|
+
`--backup DIR` mirrors originals first (keep backups outside the vetted
|
|
200
|
+
directory). Consent questions never block the verb: a read-only run that finds
|
|
201
|
+
repairable books records an `apply_lossy` entry in the JSON's
|
|
202
|
+
`decisions_needed` instead of prompting, and `--non-interactive` declares that
|
|
203
|
+
contract for callers. Exit codes per the `library` contract: 0 clean, 1 usage,
|
|
204
|
+
2 when any book is flagged, rejected, unreadable, or failed epubcheck (a
|
|
205
|
+
consent question alone is not trouble).
|
|
206
|
+
|
|
207
|
+
```sh
|
|
208
|
+
bindery run phase1 ~/Downloads --json phase1-report.json # read-only
|
|
209
|
+
bindery run phase1 ~/Downloads --apply-lossy --backup /tmp/phase1-backups
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
**`run phase3 --ids IDS`** (from the library directory) is the post-import
|
|
213
|
+
scoped repair sweep: exactly `library --id IDS --sweep --only all --apply --all
|
|
214
|
+
--install-to-calibre` plus a pre/post epubcheck summary. It mechanically
|
|
215
|
+
refuses an unscoped library-wide sweep with exit 2 (a full-library sweep is a
|
|
216
|
+
dedicated hours-long task, never a verb call), and books left partial or
|
|
217
|
+
unreadable surface as `decisions_needed` in the JSON.
|
|
218
|
+
|
|
219
|
+
```sh
|
|
220
|
+
cd ~/docs/Calibre\ Library && bindery run phase3 --ids 5071,5072 --json phase3-report.json
|
|
221
|
+
```
|
|
222
|
+
|
|
201
223
|
## Companion scripts
|
|
202
224
|
|
|
203
225
|
`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,73 @@
|
|
|
1
1
|
# bindery-cli Patch Notes
|
|
2
|
+
## v0.30.0 (2026-09-06)
|
|
3
|
+
|
|
4
|
+
### Phase 8 stretch: concurrent sweep workers
|
|
5
|
+
|
|
6
|
+
- **`library --workers N`**: the `--sweep` candidate pass runs through N
|
|
7
|
+
concurrent epubcheck workers (default 1: serial, byte-for-byte unchanged).
|
|
8
|
+
epubcheck runs in a subprocess that releases the GIL, so threads parallelize
|
|
9
|
+
the oracle honestly; this is the pass that measured ~4.4 s/book on the
|
|
10
|
+
2026-08-27 full-library walk (5,070 EPUBs ≈ 6 hours serial). Books are
|
|
11
|
+
checked in windows of N consumed in input order, so the candidate set, the
|
|
12
|
+
before-measurements, and every emitted line match the serial sweep, and
|
|
13
|
+
`--limit` stays lazy within one window of overshoot. The repair phase stays
|
|
14
|
+
serial on purpose: that is where the shared workdir and the
|
|
15
|
+
atomic-replacement contract live. Without `--sweep`, `--workers` is a
|
|
16
|
+
no-op (with a note); counts below 1 are a usage error.
|
|
17
|
+
|
|
18
|
+
## v0.29.0 (2026-09-06)
|
|
19
|
+
|
|
20
|
+
### Phase 13: machine-readable audit and the acquisition run slices
|
|
21
|
+
|
|
22
|
+
The acquisition pathway's EPUB steps are now first-class CLI surface. Nothing
|
|
23
|
+
here adds a repair class or changes an existing mode; the new verbs compose
|
|
24
|
+
what already ships, in the phase-1 and phase-3 skills' documented order.
|
|
25
|
+
|
|
26
|
+
- **`audit --json FILE`**: the audit subcommand writes machine-readable
|
|
27
|
+
per-file verdicts in the `library --json` shape (mode/root/summary plus one
|
|
28
|
+
record per book), from all three modes: directory, library, and single-book.
|
|
29
|
+
Records carry a status (clean/problem/error) and per-analyzer verdicts
|
|
30
|
+
(problem/status/details); the always-on archive/spine verdicts appear OK
|
|
31
|
+
when they were silent, emptytext is omitted from a record when the archive
|
|
32
|
+
verdict owns the body-text story, and a scan error becomes its own record
|
|
33
|
+
shape. `--json` with `--id` accepts exactly one book id, because each
|
|
34
|
+
single-book run writes the file wholesale. This is the contract downstream
|
|
35
|
+
consumers of the phase-1 EPUB slice read.
|
|
36
|
+
- **`run phase1 DIR [--json OUT] [--apply-lossy] [--backup DIR]`**: the
|
|
37
|
+
pre-import vetting slice over a directory of loose files, in the phase-1
|
|
38
|
+
skill's documented order: the audit battery (corruption sweep, content
|
|
39
|
+
battery, monolithic), then one fused repair sweep (epubcheck, watermark
|
|
40
|
+
detection, repairability; the skill's two sweeps collapse into one
|
|
41
|
+
epubcheck pass, with watermark hits read out of the per-book fix summary).
|
|
42
|
+
Read-only until `--apply-lossy`, which IS the recorded lossy-strip consent;
|
|
43
|
+
`--backup DIR` passes through (keep backups outside the vetted directory).
|
|
44
|
+
Exit codes 0/1/2 per the library contract; a consent question alone is not
|
|
45
|
+
trouble.
|
|
46
|
+
- **`run phase3 --ids CSV [--json OUT]`**: the post-import scoped repair step,
|
|
47
|
+
run from the library directory: exactly `library --id --sweep --only all
|
|
48
|
+
--apply --all --install-to-calibre` plus a pre/post epubcheck summary over
|
|
49
|
+
the swept books. The phase-3 skill's step-10 scope warning is mechanized: an
|
|
50
|
+
unscoped library-wide call is refused with exit 2, never run by accident.
|
|
51
|
+
The verb drives the shipped library runner through the real parser, so its
|
|
52
|
+
flags cannot drift from the subcommand it wraps.
|
|
53
|
+
- **Non-interactive contract**: both run verbs take `--non-interactive` and
|
|
54
|
+
never prompt (nothing in bindery prompts); open questions surface in the
|
|
55
|
+
JSON as `decisions_needed` entries: the `apply_lossy` consent question in a
|
|
56
|
+
read-only phase1, `manual_repair`/`investigate` for books left partial or
|
|
57
|
+
unreadable in phase3.
|
|
58
|
+
- **Skill sync**: the phase-1 and phase-3 skills name the run verbs and the
|
|
59
|
+
machine-readable contracts (the skill files live in the library directory,
|
|
60
|
+
outside this repo).
|
|
61
|
+
- **Dependencies**: vir-tui 2.3.0 folded into the lock; the pyproject floor
|
|
62
|
+
stays `vir-tui>=2.2.0` (additive bump at this release).
|
|
63
|
+
- **Docs and hygiene**: CLAUDE.md's last surviving `calibredb add_format`
|
|
64
|
+
line retired (it contradicted the v0.24.0 subprocess removal and carried a
|
|
65
|
+
`-apply` typo); README's `--sweep` bullet and the pyproject description no
|
|
66
|
+
longer advertise the validation daemon's speedup, which never engages
|
|
67
|
+
because the daemon's classpath omits epubcheck's `lib/` dependencies (the
|
|
68
|
+
fix is a recorded gate-behavior decision, not a drive-by); `venv/` and
|
|
69
|
+
`.venv_ci/` are gitignored. Test count: 277 → 294.
|
|
70
|
+
|
|
2
71
|
## v0.28.0 (2026-09-05)
|
|
3
72
|
|
|
4
73
|
### 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.
|
|
8
|
-
description = "
|
|
7
|
+
version = "0.30.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
|
|
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.)
|
|
@@ -461,12 +510,19 @@ by hand.*
|
|
|
461
510
|
release.
|
|
462
511
|
- [x] **`audit --id` comma lists**: v0.19.0 took a single book id; support the
|
|
463
512
|
same comma-separated form as the new `library --id` so the two modes match.
|
|
464
|
-
- [
|
|
513
|
+
- [x] **Concurrent validation workers (stretch; not shipped)**: the 2026-08-27 full-library
|
|
465
514
|
walk measured ~4.4 s/book on a single daemon (5,070 EPUBs ≈ 6 h). Gate k
|
|
466
515
|
validation workers behind `--workers N`, default 1 (behavior unchanged).
|
|
467
516
|
*(Un-ticked in the 2026-09-02 docs re-read (NEW-AUDIT.md Stage 0): the box was
|
|
468
517
|
ticked at the v0.23.0 ship, but no `--workers` code, test, or doc line ever
|
|
469
|
-
landed.
|
|
518
|
+
landed.)*
|
|
519
|
+
*(Shipped for real in v0.30.0: `library --workers N` runs the --sweep
|
|
520
|
+
candidate pass through N concurrent epubcheck workers, default 1 (serial,
|
|
521
|
+
unchanged). Threads parallelize the oracle honestly (the subprocess releases
|
|
522
|
+
the GIL); books are checked in windows of N consumed in input order, so the
|
|
523
|
+
candidate set matches the serial sweep and `--limit` stays lazy within one
|
|
524
|
+
window of overshoot. The repair phase stays serial, where the shared workdir
|
|
525
|
+
and the atomic-replacement contract live.)*
|
|
470
526
|
|
|
471
527
|
Non-goals: no auto-trim of bloated ToCs (semantics-preserving charter); no
|
|
472
528
|
content-doc synthesis; no PDF equivalent.
|
|
@@ -629,3 +685,75 @@ the underlying conditions are what the repairs target:
|
|
|
629
685
|
gate that deserves its own decision, not a drive-by edit.
|
|
630
686
|
|
|
631
687
|
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).
|
|
688
|
+
|
|
689
|
+
## Phase 13: machine-readable audit + the acquisition run slices (proposed 2026-09-05)
|
|
690
|
+
|
|
691
|
+
*Context: the acquisition pathway (`~/docs/Calibre Library/.claude/skills/`) is
|
|
692
|
+
being promoted from agent-prose into first-class CLI subcommands across the lane.
|
|
693
|
+
Brandon's design: `bindery run phase1` for the EPUB slice of pre-import vetting,
|
|
694
|
+
`bindery run phase3` for the scoped post-import repair pass, with CalibreQuarry
|
|
695
|
+
owning the orchestration (`cquarry run phase1/2/3`, its roadmap Phase 17) and the
|
|
696
|
+
manifest that hands state between phases. The prerequisite on this side is a
|
|
697
|
+
machine-readable audit; the run verbs wrap what already ships. No new repair
|
|
698
|
+
classes, no behavior change to existing modes.*
|
|
699
|
+
|
|
700
|
+
- [x] **`audit --json FILE`**: per-file analyzer verdicts in the same shape
|
|
701
|
+
`library --json` already emits (audit is console-only today; the
|
|
702
|
+
subparser has no JSON flag). This is the contract every downstream
|
|
703
|
+
consumer of the phase-1 EPUB slice reads.
|
|
704
|
+
*(Shipped in v0.29.0. All three modes write it: directory, library,
|
|
705
|
+
and single-book. The always-on archive/spine verdicts appear OK when
|
|
706
|
+
they were silent; emptytext is omitted from a record when the archive
|
|
707
|
+
verdict owns the body-text story; a scan error becomes its own record
|
|
708
|
+
shape (`status: "error"`). `--json` with `--id` accepts exactly one id,
|
|
709
|
+
because each single-book run writes the file wholesale.)*
|
|
710
|
+
- [x] **`run phase1 DIR [--json OUT] [--apply-lossy] [--backup DIR]`**: the
|
|
711
|
+
EPUB slice in the phase-1 skill's documented order (corruption sweep,
|
|
712
|
+
epubcheck, content battery, monolithic, watermark dry-run,
|
|
713
|
+
repairability). Gated repair applies only under `--apply-lossy`, which is
|
|
714
|
+
the recorded lossy-strip consent; without it the verb is read-only.
|
|
715
|
+
Exit codes 0/1/2 per the `library` contract. No DRM scan, no duplicate
|
|
716
|
+
screen, no manifest assembly (CalibreQuarry's by the frontend split).
|
|
717
|
+
*(Shipped in v0.29.0. Two composed stages: the audit battery through its
|
|
718
|
+
own --json payload, then ONE fused repair sweep with the --all set — the
|
|
719
|
+
skill ran watermark detection and repairability as two sweeps, the verb
|
|
720
|
+
pays epubcheck once and reads watermark hits out of the per-book fix
|
|
721
|
+
summary. Read-only until --apply-lossy; --backup passes through (keep
|
|
722
|
+
backups outside the vetted directory). A consent question is not an
|
|
723
|
+
error: it lands in decisions_needed and the exit code stays on the
|
|
724
|
+
library contract.)*
|
|
725
|
+
- [x] **`run phase3 --ids CSV [--json OUT]`**: wraps
|
|
726
|
+
`library --id ... --sweep --only all --apply --all --install-to-calibre`
|
|
727
|
+
with a pre/post summary and mechanically refuses unscoped library-wide
|
|
728
|
+
sweeps (encodes the phase-3 skill's step-10 scope warning as an exit 2
|
|
729
|
+
instead of a prose warning).
|
|
730
|
+
*(Shipped in v0.29.0. Run from the library directory (cwd-resolved like
|
|
731
|
+
audit library mode). The verb drives the shipped library runner through
|
|
732
|
+
the real parser, so its flags cannot drift from the subcommand it wraps.
|
|
733
|
+
The pre/post summary sums before/after epubcheck counts over the swept
|
|
734
|
+
books; books left partial or unreadable surface as decisions_needed.
|
|
735
|
+
The verb is the apply step by design — the batch was vetted in phase 1,
|
|
736
|
+
and the gate plus the atomic-replacement contract still govern every
|
|
737
|
+
write.)*
|
|
738
|
+
- [x] **Non-interactive contract**: every `run` verb takes
|
|
739
|
+
`--non-interactive`; prompts never fire off a TTY, and open questions
|
|
740
|
+
surface in the JSON as `decisions_needed` for the calling agent or user.
|
|
741
|
+
*(Shipped in v0.29.0 with the verbs themselves. Nothing in a run verb
|
|
742
|
+
prompts (none ever did; the flag makes the orchestrator contract
|
|
743
|
+
explicit and is recorded in the payload). decisions_needed entries: the
|
|
744
|
+
apply_lossy consent question in a read-only phase1, manual_repair and
|
|
745
|
+
investigate for partial/unreadable books in phase3.)*
|
|
746
|
+
- [x] **Skill sync**: the phase-1 and phase-3 skills name the run verbs once
|
|
747
|
+
shipped. Floor, not ceiling, per the standing rule.
|
|
748
|
+
*(Done for v0.29.0, 2026-09-06: the phase-1 skill's EPUB section names
|
|
749
|
+
`run phase1` as the one-verb wrapper (the fused sweep pays epubcheck
|
|
750
|
+
once; the JSON contract and `--apply-lossy` consent named) and the
|
|
751
|
+
phase-3 skill's step 10 names `run phase3` with the mechanized scope
|
|
752
|
+
refusal. The skill files live in the library directory, outside this
|
|
753
|
+
repo; per the floor rule their per-check commands still stand on their
|
|
754
|
+
own.)*
|
|
755
|
+
|
|
756
|
+
Non-goals: no manifest format ownership (CalibreQuarry owns the
|
|
757
|
+
`acquisition-manifest` schema and the orchestration; this repo emits reports
|
|
758
|
+
and consumes id lists); no TUI beyond the existing audit rendering; no PDF/DJVU
|
|
759
|
+
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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")
|
|
@@ -267,6 +267,11 @@ For a Calibre library (`Author/Title (id)/Title - Author.epub`):
|
|
|
267
267
|
reported and counted as `unreadable`; it never aborts the rest of the sweep.
|
|
268
268
|
- The `library` exit code is 0 for a clean sweep, 1 for a usage error, and 2 when any
|
|
269
269
|
book was rejected, unreadable, or failed epubcheck, so scripts can detect trouble.
|
|
270
|
+
- With `--sweep`, `--workers N` runs the candidate-selection epubcheck pass through N
|
|
271
|
+
concurrent workers (default 1: serial, unchanged). Books are checked in windows of N
|
|
272
|
+
consumed in input order, so the candidate set and the before-measurements are identical
|
|
273
|
+
to the serial sweep; the repair phase is never parallel (the shared workdir and the
|
|
274
|
+
atomic-replacement contract live there).
|
|
270
275
|
|
|
271
276
|
### Native format installation (`--install-to-calibre`)
|
|
272
277
|
Optionally, bindery-cli installs the repaired EPUB as the book's format through cquarry's write module (`WritableCalibreDB`): the file is placed atomically — an in-place replace over the catalogued file when one exists (same path, same `data.name`), or a fresh placement under the repaired file's stem otherwise — and the `data` row follows in one `batch()` transaction (`remove_format` + `add_format`, since `add_format` refuses duplicates), keeping the size truthful and queuing the book in `metadata_dirtied` so Calibre regenerates its sidecar .opf. The external `calibredb` CLI is no longer used (the v0.23.1 `--replace` crash class is gone with it). It automatically falls back to atomic filesystem replacement if a valid Calibre ID cannot be extracted, and a database failure degrades to the in-place save with a warning rather than losing the repair.
|
|
@@ -291,6 +296,38 @@ bumps `books.last_modified`, and cleans link tables before tag deletion). THIN e
|
|
|
291
296
|
advisories stay untagged; already-tagged books are skipped; a missing file is a scan error, not a
|
|
292
297
|
silent skip.
|
|
293
298
|
|
|
299
|
+
`--json FILE` (v0.29.0) writes the same verdicts machine-readably, in the `library --json`
|
|
300
|
+
shape: one record per file with a `status` (`clean`, `problem`, or `error`) and per-analyzer
|
|
301
|
+
verdicts (`problem`, `status`, `details`); the always-on archive/spine verdicts appear OK when
|
|
302
|
+
they were silent, emptytext is omitted when the archive verdict owns the book's body-text story,
|
|
303
|
+
and a scan error becomes its own record (`status: "error"`, an `error` message, no verdicts).
|
|
304
|
+
All three modes write it (directory, library, and single-book); `--json` with `--id` accepts
|
|
305
|
+
exactly one book id, since each single-book run writes the file wholesale.
|
|
306
|
+
|
|
307
|
+
## Run verbs (acquisition slices)
|
|
308
|
+
|
|
309
|
+
`bindery run phase1 DIR [--json FILE] [--apply-lossy] [--backup DIR] [--non-interactive]`
|
|
310
|
+
composes the shipped read-only audit battery (corruption sweep, content battery,
|
|
311
|
+
monolithic) with the gated repair sweep (epubcheck, watermark detection,
|
|
312
|
+
repairability) over one directory of loose files, in the phase-1 skill's
|
|
313
|
+
documented order. It introduces no new repair classes: every fix, gate, and
|
|
314
|
+
safety net is the shipped one. The verb is read-only until `--apply-lossy` is
|
|
315
|
+
passed, and that flag is the recorded lossy-strip consent; `--backup DIR`
|
|
316
|
+
mirrors originals before any replacement. Consent questions never block the
|
|
317
|
+
verb: they surface as `decisions_needed` entries in the JSON (never prompts),
|
|
318
|
+
and `--non-interactive` declares that contract for callers. Exit codes per the
|
|
319
|
+
library contract: 0 clean, 1 usage, 2 when any book is flagged, rejected,
|
|
320
|
+
unreadable, or failed epubcheck; a consent question alone is not trouble.
|
|
321
|
+
|
|
322
|
+
`bindery run phase3 --ids IDS [--json FILE] [--non-interactive]` (run from the
|
|
323
|
+
library directory) is exactly `library --id IDS --sweep --only all --apply
|
|
324
|
+
--all --install-to-calibre` plus a pre/post epubcheck summary over the swept
|
|
325
|
+
books. It is the post-import apply step by design: the batch was vetted in
|
|
326
|
+
phase 1, the epubcheck gate still governs every replacement, and the atomic
|
|
327
|
+
replacement contract is untouched. The scope refusal is mechanical: no `--ids`,
|
|
328
|
+
no sweep, exit 2 (a library-wide sweep is a dedicated hours-long task, never a
|
|
329
|
+
verb call). Books left partial or unreadable surface as `decisions_needed`.
|
|
330
|
+
|
|
294
331
|
## Out of scope (non-goals)
|
|
295
332
|
|
|
296
333
|
- Fixing RSC-005 schema/content-model violations in bulk.
|