bindery-cli 0.49.0__tar.gz → 0.50.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.49.0 → bindery_cli-0.50.0}/AGENTS.md +2 -2
- {bindery_cli-0.49.0 → bindery_cli-0.50.0}/PKG-INFO +6 -1
- {bindery_cli-0.49.0 → bindery_cli-0.50.0}/README.md +5 -0
- {bindery_cli-0.49.0 → bindery_cli-0.50.0}/patchnotes.md +59 -0
- {bindery_cli-0.49.0 → bindery_cli-0.50.0}/pyproject.toml +1 -1
- {bindery_cli-0.49.0 → bindery_cli-0.50.0}/roadmap.md +1 -0
- {bindery_cli-0.49.0 → bindery_cli-0.50.0}/spec.md +24 -2
- {bindery_cli-0.49.0 → bindery_cli-0.50.0}/src/bindery/__init__.py +1 -1
- {bindery_cli-0.49.0 → bindery_cli-0.50.0}/src/bindery/audit.py +453 -37
- {bindery_cli-0.49.0 → bindery_cli-0.50.0}/src/bindery/cli.py +6 -4
- bindery_cli-0.50.0/tests/test_contamination.py +396 -0
- {bindery_cli-0.49.0 → bindery_cli-0.50.0}/uv.lock +1 -1
- {bindery_cli-0.49.0 → bindery_cli-0.50.0}/.github/FUNDING.yml +0 -0
- {bindery_cli-0.49.0 → bindery_cli-0.50.0}/.github/workflows/ci.yml +0 -0
- {bindery_cli-0.49.0 → bindery_cli-0.50.0}/.github/workflows/publish.yml +0 -0
- {bindery_cli-0.49.0 → bindery_cli-0.50.0}/.gitignore +0 -0
- {bindery_cli-0.49.0 → bindery_cli-0.50.0}/LICENSE +0 -0
- {bindery_cli-0.49.0 → bindery_cli-0.50.0}/docs/screenshots/library-sweep.png +0 -0
- {bindery_cli-0.49.0 → bindery_cli-0.50.0}/logo.svg +0 -0
- {bindery_cli-0.49.0 → bindery_cli-0.50.0}/plugin/__init__.py +0 -0
- {bindery_cli-0.49.0 → bindery_cli-0.50.0}/project.done +0 -0
- {bindery_cli-0.49.0 → bindery_cli-0.50.0}/run_tests.sh +0 -0
- {bindery_cli-0.49.0 → bindery_cli-0.50.0}/scripts/FastSweep.java +0 -0
- {bindery_cli-0.49.0 → bindery_cli-0.50.0}/scripts/build_plugin.py +0 -0
- {bindery_cli-0.49.0 → bindery_cli-0.50.0}/scripts/fast_sweep.py +0 -0
- {bindery_cli-0.49.0 → bindery_cli-0.50.0}/src/bindery/__main__.py +0 -0
- {bindery_cli-0.49.0 → bindery_cli-0.50.0}/src/bindery/epub.py +0 -0
- {bindery_cli-0.49.0 → bindery_cli-0.50.0}/src/bindery/library.py +0 -0
- {bindery_cli-0.49.0 → bindery_cli-0.50.0}/src/bindery/pagination.py +0 -0
- {bindery_cli-0.49.0 → bindery_cli-0.50.0}/src/bindery/reserialize.py +0 -0
- {bindery_cli-0.49.0 → bindery_cli-0.50.0}/src/bindery/transforms.py +0 -0
- {bindery_cli-0.49.0 → bindery_cli-0.50.0}/src/bindery/validate.py +0 -0
- {bindery_cli-0.49.0 → bindery_cli-0.50.0}/src/bindery/watermark.py +0 -0
- {bindery_cli-0.49.0 → bindery_cli-0.50.0}/tests/test_audit.py +0 -0
- {bindery_cli-0.49.0 → bindery_cli-0.50.0}/tests/test_cli.py +0 -0
- {bindery_cli-0.49.0 → bindery_cli-0.50.0}/tests/test_comment_hyphens.py +0 -0
- {bindery_cli-0.49.0 → bindery_cli-0.50.0}/tests/test_cover_tocdrift.py +0 -0
- {bindery_cli-0.49.0 → bindery_cli-0.50.0}/tests/test_epub.py +0 -0
- {bindery_cli-0.49.0 → bindery_cli-0.50.0}/tests/test_flags_real_core.py +0 -0
- {bindery_cli-0.49.0 → bindery_cli-0.50.0}/tests/test_flags_wiring.py +0 -0
- {bindery_cli-0.49.0 → bindery_cli-0.50.0}/tests/test_help.py +0 -0
- {bindery_cli-0.49.0 → bindery_cli-0.50.0}/tests/test_library.py +0 -0
- {bindery_cli-0.49.0 → bindery_cli-0.50.0}/tests/test_matcher_hardening.py +0 -0
- {bindery_cli-0.49.0 → bindery_cli-0.50.0}/tests/test_pagination.py +0 -0
- {bindery_cli-0.49.0 → bindery_cli-0.50.0}/tests/test_plugin.py +0 -0
- {bindery_cli-0.49.0 → bindery_cli-0.50.0}/tests/test_reserialize.py +0 -0
- {bindery_cli-0.49.0 → bindery_cli-0.50.0}/tests/test_space_renames.py +0 -0
- {bindery_cli-0.49.0 → bindery_cli-0.50.0}/tests/test_transforms.py +0 -0
- {bindery_cli-0.49.0 → bindery_cli-0.50.0}/tests/test_validate.py +0 -0
- {bindery_cli-0.49.0 → bindery_cli-0.50.0}/tests/test_version.py +0 -0
- {bindery_cli-0.49.0 → bindery_cli-0.50.0}/tests/test_watermark.py +0 -0
- {bindery_cli-0.49.0 → bindery_cli-0.50.0}/tests/test_xml_hardening.py +0 -0
|
@@ -114,7 +114,7 @@ Born from the 2026 library audit (see the user memory `calibre-library-epubcheck
|
|
|
114
114
|
- `src/bindery/pagination.py`: the opt-in lossy page-number strip (runhead detection, page-layer decision, block-centric removal/merge, safety nets).
|
|
115
115
|
- `src/bindery/watermark.py`: the opt-in lossy watermark strip (anchored and anchorless signature removal).
|
|
116
116
|
- `src/bindery/reserialize.py`: structural repair via `html5lib`.
|
|
117
|
-
- `src/bindery/audit.py`: read-only body-text audits (`content`, `pagenumbers`, `emptytext`, `ocr`, `monolithic` since v0.21.0 with `--max-doc-chars N`, and `completeness` since v0.36.0) 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). Since v0.44.0 two more advisory analyzers ride the single pass: `cover` (the EPUB3 `properties~="cover-image"` slice of the ruled hybrid: dangling EPUB2 meta, EPUB3 declaration, cover-file presence) and `tocdrift` (the NCX navMap vs EPUB3 nav toc structural diff, fragments included; audit-only, ToC synthesis out of charter permanently); the loader now exposes the parsed OPF, the NCX text, and the nav HTML on `Book`. Since v0.36.0 the completeness analyzer (Phase 15) reports the phase-1 spot-check per book: spine/prose-doc counts (prose = >= 400 visible chars), first/middle/last prose-doc opening/closing excerpts, trailing-ToC classification (`_trailing_toc`: short link lines dominate, no paragraph-length blocks), and the unreadable fraction. It is ADVISORY by contract (problem always False; ADVISORY on a trailing ToC or unreadable fraction >= 10%), never moves the exit code, and is skipped like emptytext when the archive verdict owns the book. Since v0.38.0 the archive verdict distinguishes font obfuscation from DRM (`OBFUSCATION_ALGOS`: IDPF 2008/embedding + both Adobe URIs; readable obfuscated entries are the benign OBFUSCATED advisory, an unreadable one is CORRUPT, and only non-obfuscation algorithms give ENCRYPTED). Since v0.43.0 untrusted metadata XML is parsed through `_safe_xml_parse` (DOCTYPE/ENTITY declarations refused before xml.etree sees them; the stdlib shape of defusedxml's entity protection) and single entries above `_MAX_ENTRY_BYTES` (32 MB) are refused before decompression; a refused metadata file returns the shell book. Since v0.40.0 the vir-tui console renderer loads through the module-level `_LazyUI` proxy (a plain `import vir_tui as ui` at module level would make 3.12/3.13 installs unimportable now that the stack is marker-gated; LOAD_GLOBAL never consults module `__getattr__`, so a proxy object is the lazy shape that works).
|
|
117
|
+
- `src/bindery/audit.py`: read-only body-text audits (`content`, `pagenumbers`, `emptytext`, `ocr`, `monolithic` since v0.21.0 with `--max-doc-chars N`, and `completeness` since v0.36.0) 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). Since v0.44.0 two more advisory analyzers ride the single pass: `cover` (the EPUB3 `properties~="cover-image"` slice of the ruled hybrid: dangling EPUB2 meta, EPUB3 declaration, cover-file presence) and `tocdrift` (the NCX navMap vs EPUB3 nav toc structural diff, fragments included; audit-only, ToC synthesis out of charter permanently); the loader now exposes the parsed OPF, the NCX text, and the nav HTML on `Book`. Since v0.50.0 a ninth analyzer, `contamination` (issue #8, the judge-and-import class, advisory by contract like completeness/cover/tocdrift, never moves the exit code): foreign-title template stamps (head `<title>` repeats or a volume/book marker with named words in front of it, judged against the OPF's own title/creator/publisher/series with a marker-stripped comparison; a part label whose marker leads never flags), fan-bundle ad signatures (jnovels, kokoro connect, zerobooks, a light-novel plug), and review-paste fingerprints (strong flags alone, weak needs corroboration) in the first five content docs; its corpus is spine PLUS `Book.extra_docs` (the archive's non-spine html, retained by the loader in archive order under a 64 MiB cap, because a broken manifest can leave the junk entirely off the spine: Hothouse). Since v0.50.0 `run_directory`/`run_single` dispatch analyzers through `_dir_dispatch` (one entry per ALL key; the old if/elif chain's `else` ran ocr for cover/tocdrift in those modes since v0.44.0) and `tests/test_contamination.py` pins the map's key set to ALL. Since v0.36.0 the completeness analyzer (Phase 15) reports the phase-1 spot-check per book: spine/prose-doc counts (prose = >= 400 visible chars), first/middle/last prose-doc opening/closing excerpts, trailing-ToC classification (`_trailing_toc`: short link lines dominate, no paragraph-length blocks), and the unreadable fraction. It is ADVISORY by contract (problem always False; ADVISORY on a trailing ToC or unreadable fraction >= 10%), never moves the exit code, and is skipped like emptytext when the archive verdict owns the book. Since v0.38.0 the archive verdict distinguishes font obfuscation from DRM (`OBFUSCATION_ALGOS`: IDPF 2008/embedding + both Adobe URIs; readable obfuscated entries are the benign OBFUSCATED advisory, an unreadable one is CORRUPT, and only non-obfuscation algorithms give ENCRYPTED). Since v0.43.0 untrusted metadata XML is parsed through `_safe_xml_parse` (DOCTYPE/ENTITY declarations refused before xml.etree sees them; the stdlib shape of defusedxml's entity protection) and single entries above `_MAX_ENTRY_BYTES` (32 MB) are refused before decompression; a refused metadata file returns the shell book. Since v0.40.0 the vir-tui console renderer loads through the module-level `_LazyUI` proxy (a plain `import vir_tui as ui` at module level would make 3.12/3.13 installs unimportable now that the stack is marker-gated; LOAD_GLOBAL never consults module `__getattr__`, so a proxy object is the lazy shape that works).
|
|
118
118
|
- `src/bindery/epub.py`: archive rewrite, NCX uid sync, RepairReport, mismatch detection, and the opt-in structural-repair plumbing (including the CSS precondition scan). Since v0.46.0 the opt-in selection is the `RepairFlags` dataclass (one field per flag, all off by default; the all-off instance IS the default pass): `repair_epub(src, dst, flags)` and `process_book(..., flags)` take one object instead of a ~25-kwarg signature, the EPUB2-targeted fixes' version gate swaps in a `dataclasses.replace` copy (never mutating the caller's instance), and the plugin's bare `repair_epub(src, dst)` call constructs exactly the all-off default pass. Since v0.38.0/v0.39.0 (Phase 16): `generate_container` + the `--fix-container` gateway (insert after mimetype when missing, replace in-loop when stale; constant-epoch timestamp keeps bytes deterministic), `prune_dangling_edges` (prune_missing_manifest_items returns the pruned ids and the edges that pointed at them (spine@toc, media-overlay, fallback, EPUB2 cover meta) are rewritten so pruning cannot manufacture the regression the gate rejects), and `fix_manifest_media_types` (extension map + magic-byte confirmation via a `peek` callable over the open source zip; jpg/png/gif only), and `fix_cover_meta` (the ruling's deterministic half: dangling EPUB2 cover meta re-pointed from the guide's cover reference when exactly one manifest item carries that file, removed otherwise; accepted under no_worse like the lossy strips because cover wiring is invisible to epubcheck).
|
|
119
119
|
- `src/bindery/validate.py`: epubcheck wrapper, the `gate` (improvement) and `no_worse` (no-regression, for the lossy strips) acceptance bars.
|
|
120
120
|
- `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` (since v0.31.0 via `set_format`, cquarry 1.17's sanctioned remove+add in one transaction); the external `calibredb` CLI is gone. Since v0.32.0 a guessed id drives a row update only when metadata.db corroborates it (`_verify_guess`: books row exists, file in the book's own directory, catalogued `data.name` when a row exists); anything else saves in place and leaves the catalog untouched: never reintroduce directory-name guessing as the primary source: renamed/mismatched directories would replace the wrong book. Since 2026-09-15 `make_backup` takes the opt-in `--backup-keep N` ring: at most N backup files per book, the author original `.bak` never deleted, newest content at the highest name; default remains an unbounded rotation.
|
|
@@ -122,7 +122,7 @@ Born from the 2026 library audit (see the user memory `calibre-library-epubcheck
|
|
|
122
122
|
custom-rendered and colors itself with the CPython 3.14 argparse theme,
|
|
123
123
|
gated by argparse's own _colorize detection so the levels always agree) and the repair-flag inventory is one table: `_REPAIR_FLAG_TABLE` (dest + one-line short help + long reference text) registers the compact grouped `--help` (argparse argument groups, hand-short usage line) and `--help-repairs` renders the long descriptions (a custom argparse Action, deliberately NOT store_true so the wiring test's selection count ignores it); adding a flag means adding a table row. Since v0.48.0 the metadata.db doors are enforced: `_require_closed_calibre` (pgrep fail-closed, exit 1) guards `audit --tag` and `library --apply --install-to-calibre` (run phase3 transitively), and `--help-json` (a custom argparse Action like --help-repairs, never colored) dumps the per-verb exit contract and the door list; since v0.48.1 the per-verb exit contracts tell the code's truth (repair: 0 probed/written incl. partial / 1 trouble, never 2; audit: 0/1/2 plus 3 for findings+write-failure; library and the run slices: the unified 0/2/1); since v0.49.0 the single-file verb's no-output form is a probe (the gate runs, the record's `output` is null and `applied` false, nothing is written anywhere; the old implicit `(repaired).epub` sibling was issue #7's import litter in a vetted tree, and writing now needs an explicit DST). `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). Since v0.40.0 `main()` guards the VirInvictus stack: a `ModuleNotFoundError` naming `vir_tui`/`cquarry` (marker-gated to 3.14+ on the 3.12 floor) becomes one stderr line and trouble exit 2 instead of a traceback; anything else missing still raises.
|
|
124
124
|
- `plugin/__init__.py` + `scripts/build_plugin.py`: the Bindery Repair Calibre plugin (v0.37.0; identity `Bindery Repair` / `bindery_repair`, approved and built per the scoped spec in roadmap.md Phase 3). The builder vendors `transforms.py`, `epub.py`, `pagination.py`, `watermark.py`, `reserialize.py` byte-identically into the zip (the zip root is a package, so their relative imports resolve unchanged; the suite's drift test pins the equality) with the version tuple substituted from the single-source VERSION; `publish.yml` attaches `BinderyRepair-v<VERSION>.zip` to each release. The plugin runs ONLY the default pass (epubcheck cannot gate inside Calibre), never raises, is byte-idempotent (zero fixes returns the original path), testzip()-verifies its output, logs one line per book, and takes JSON config via `site_customization` (`log`, `log_path`, `max_size_mb` default 150, `max_log_mb` rotation cap default 2, `epubcheck_path` experimental validation default OFF). Load it in tests through the `tests/test_plugin.py` stubs: the calibre namespace is installed in sys.modules and the package loads with a two-dir `__path__` (plugin dir first, then src/bindery) exactly like calibre's zipplugin loader shape. **Plugin/interpreter compatibility (v0.40.0):** v0.39.0 shipped PEP 758 unparenthesized except tuples, and the plugin died with a bare SyntaxError on every released Calibre (7.x and the whole 8.x series embed Python 3.11, 9.0+ embeds 3.14; from calibre's bypy sources.json, checked against the upstream clone). The core is now grammar-pinned: parenthesized except tuples everywhere, `ruff`'s `target-version = "py312"` pin (inference from requires-python would rewrite the parens back off), `tests/test_version.py`'s parse guard, and CI's `plugin-compat` job byte-compiling the built zip under 3.11/3.12/3.13/3.14. `minimum_calibre_version` is (7, 0, 0): the oldest series whose interpreter CI covers, replacing the hollow (2, 0, 0); `supported_platforms` stays `["linux"]` (developed and tested on Linux only). Any new syntax in the vendored modules must clear the guard before it ships.
|
|
125
|
-
- `tests/`: transforms, end-to-end repair, atomic replace, pagination, watermarks, the audit analyzers (content/pagenumbers/emptytext/ocr/monolithic/completeness, archive integrity, spine classification), the library sweep and `CalibreIdResolver`, validate, reserialize, CLI wiring, the version pin and the syntax-floor grammar guard, and the Calibre plugin (build drift + run behavior under calibre stubs). Since the v0.46.0 follow-ups: `tests/test_flags_wiring.py` reflectively pins the RepairFlags inventory to the argparse dests and `_flags_from_args` mapping (a flag added without a field, a field without a flag, or a remapped line fails there), the all-off defaults, and the version gate's caller-immutability contract (test_epub's `TestVersionGateKeepsFlagsIntact`); `tests/test_comment_hyphens.py` carries the RSC-016 fix; `tests/test_flags_real_core.py` drives every repair flag from argv through the unmocked core (--no-validate; the gate itself stays pinned by the mocked-oracle tests and runs for real wherever epubcheck exists).
|
|
125
|
+
- `tests/`: transforms, end-to-end repair, atomic replace, pagination, watermarks, the audit analyzers (content/pagenumbers/emptytext/ocr/monolithic/completeness, archive integrity, spine classification, contamination since v0.50.0), the library sweep and `CalibreIdResolver`, validate, reserialize, CLI wiring, the version pin and the syntax-floor grammar guard, and the Calibre plugin (build drift + run behavior under calibre stubs). Since the v0.46.0 follow-ups: `tests/test_flags_wiring.py` reflectively pins the RepairFlags inventory to the argparse dests and `_flags_from_args` mapping (a flag added without a field, a field without a flag, or a remapped line fails there), the all-off defaults, and the version gate's caller-immutability contract (test_epub's `TestVersionGateKeepsFlagsIntact`); `tests/test_comment_hyphens.py` carries the RSC-016 fix; `tests/test_flags_real_core.py` drives every repair flag from argv through the unmocked core (--no-validate; the gate itself stays pinned by the mocked-oracle tests and runs for real wherever epubcheck exists). `tests/test_contamination.py` (v0.50.0) carries the three contamination sweeps, the advisory contract, the `_dir_dispatch` key pin, and the cover-routing regression. The v0.46.0 trio (test_flags_wiring, test_comment_hyphens, test_flags_real_core) rides CI's core-compat 3.12/3.13 leg; the hand-listed module list there must gain an entry for every new stack-free test module (audit-side test modules like test_contamination stay on the 3.14 leg by convention).
|
|
126
126
|
|
|
127
127
|
## Conventions
|
|
128
128
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: bindery-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.50.0
|
|
4
4
|
Summary: Audit EPUB body text and repair broken EPUBs with deterministic, epubcheck-gated fixes; atomic in-place replacement in a Calibre library.
|
|
5
5
|
Project-URL: Homepage, https://github.com/VirInvictus/bindery-cli
|
|
6
6
|
Project-URL: Repository, https://github.com/VirInvictus/bindery-cli
|
|
@@ -242,12 +242,17 @@ bindery audit completeness ~/docs/Calibre\ Library
|
|
|
242
242
|
bindery audit cover ~/docs/Calibre\ Library
|
|
243
243
|
bindery audit tocdrift ~/docs/Calibre\ Library
|
|
244
244
|
|
|
245
|
+
# Fan-bundle contamination: foreign-title template stamps, ad pages, review paste
|
|
246
|
+
bindery audit contamination ~/docs/Calibre\ Library
|
|
247
|
+
|
|
245
248
|
# Run all audits and report to the console (--json FILE for machine output)
|
|
246
249
|
cd ~/docs/Calibre\ Library && bindery audit all
|
|
247
250
|
```
|
|
248
251
|
|
|
249
252
|
**Completeness spot-check:** the `completeness` audit answers the pre-import question "does this book reach its end?" It reports, per book: the prose-document count against the spine (docs with 400+ visible characters), the opening and closing text of the first/middle/last prose doc, a trailing-ToC classification of the final spine doc (a link-dense chapter list after the real back matter), and the fraction of unreadable docs. A trailing ToC is reported as an advisory, never a failure: the closing excerpt always comes from the real back matter, so a human can confirm the book ends where the author ended it.
|
|
250
253
|
|
|
254
|
+
**Contamination scan:** the `contamination` audit is advisory too: fan-bundle cosmetics are the judge-and-import class. Three sweeps over the archive's content documents (spine plus the non-spine html a broken manifest can strand): another series' name where the book's own identity belongs, judged against the package's own metadata (the head-`<title>` template stamps a sloppy fan bundle leaves on every doc, a volume marker with named words in front of it, a repeated foreign running header); known fan-bundle ad signatures (jnovels, kokoro connect, zerobooks, light-novel download plugs); and pasted review-site text in the first few content docs (helpful counts, "was this review helpful", "out of 5 stars", verified purchase). Every finding is a report a human judges, never a failure.
|
|
255
|
+
|
|
251
256
|
Audits can also be run on a directory of loose `.epub` files by passing the path as the second argument:
|
|
252
257
|
```sh
|
|
253
258
|
bindery audit pagenumbers /path/to/loose/epubs
|
|
@@ -215,12 +215,17 @@ bindery audit completeness ~/docs/Calibre\ Library
|
|
|
215
215
|
bindery audit cover ~/docs/Calibre\ Library
|
|
216
216
|
bindery audit tocdrift ~/docs/Calibre\ Library
|
|
217
217
|
|
|
218
|
+
# Fan-bundle contamination: foreign-title template stamps, ad pages, review paste
|
|
219
|
+
bindery audit contamination ~/docs/Calibre\ Library
|
|
220
|
+
|
|
218
221
|
# Run all audits and report to the console (--json FILE for machine output)
|
|
219
222
|
cd ~/docs/Calibre\ Library && bindery audit all
|
|
220
223
|
```
|
|
221
224
|
|
|
222
225
|
**Completeness spot-check:** the `completeness` audit answers the pre-import question "does this book reach its end?" It reports, per book: the prose-document count against the spine (docs with 400+ visible characters), the opening and closing text of the first/middle/last prose doc, a trailing-ToC classification of the final spine doc (a link-dense chapter list after the real back matter), and the fraction of unreadable docs. A trailing ToC is reported as an advisory, never a failure: the closing excerpt always comes from the real back matter, so a human can confirm the book ends where the author ended it.
|
|
223
226
|
|
|
227
|
+
**Contamination scan:** the `contamination` audit is advisory too: fan-bundle cosmetics are the judge-and-import class. Three sweeps over the archive's content documents (spine plus the non-spine html a broken manifest can strand): another series' name where the book's own identity belongs, judged against the package's own metadata (the head-`<title>` template stamps a sloppy fan bundle leaves on every doc, a volume marker with named words in front of it, a repeated foreign running header); known fan-bundle ad signatures (jnovels, kokoro connect, zerobooks, light-novel download plugs); and pasted review-site text in the first few content docs (helpful counts, "was this review helpful", "out of 5 stars", verified purchase). Every finding is a report a human judges, never a failure.
|
|
228
|
+
|
|
224
229
|
Audits can also be run on a directory of loose `.epub` files by passing the path as the second argument:
|
|
225
230
|
```sh
|
|
226
231
|
bindery audit pagenumbers /path/to/loose/epubs
|
|
@@ -1,5 +1,64 @@
|
|
|
1
1
|
# bindery-cli Patch Notes
|
|
2
2
|
|
|
3
|
+
## v0.50.0 (2026-10-14)
|
|
4
|
+
|
|
5
|
+
### The contamination analyzer: fan-bundle cosmetics get reported (issue #8)
|
|
6
|
+
|
|
7
|
+
Three fixtures from the 2026-10-14 phase-1 wave slipped past the content
|
|
8
|
+
analyzer's ad-notice detection (the importknig class): two Grandmaster of
|
|
9
|
+
Demonic Cultivation volumes packaged by a sloppy fan bundle, carrying
|
|
10
|
+
"Full Metal Panic! Volume 5" in every content document's head `<title>` (the
|
|
11
|
+
bundle template's original book) plus a whole Jnovels/Zerobooks ad page whose
|
|
12
|
+
own head title names a third series; and Hothouse, whose front matter is
|
|
13
|
+
pasted Amazon review text ("3 of 4 people found the following review
|
|
14
|
+
helpful"). All three were approved for import regardless, so the new
|
|
15
|
+
detection is advisory by contract, like completeness/cover/tocdrift: it
|
|
16
|
+
never flags a book and never moves an exit code. The disposition for this
|
|
17
|
+
class is judge-and-import.
|
|
18
|
+
|
|
19
|
+
- **`bindery audit contamination`** (new analyzer, ninth; rides in `all`)
|
|
20
|
+
runs three sweeps over the archive's content documents:
|
|
21
|
+
- *foreign titles*: a title or series named where the book's own identity
|
|
22
|
+
belongs, judged against the package's own metadata (dc:title, creator,
|
|
23
|
+
publisher, series). Two triggers, both tuned on the fixtures: a
|
|
24
|
+
volume/book marker with named words in front of it ("Full Metal Panic!
|
|
25
|
+
Volume 5"; a part label whose marker leads, "BOOK ONE: THE RING SETS
|
|
26
|
+
OUT", never flags), or a head `<title>` repeated across the book. The
|
|
27
|
+
head-`<title>` channel matters because the MDZS stamps are invisible to
|
|
28
|
+
rendered text; a marker-stripped own-name comparison keeps a book's own
|
|
29
|
+
running headers clean when the metadata spells the volume differently.
|
|
30
|
+
- *ad signatures*: jnovels, kokoro connect, zerobooks, and a generic
|
|
31
|
+
light-novel download plug, in visible body text.
|
|
32
|
+
- *review paste*: review-site fingerprints (helpful counts, "was this
|
|
33
|
+
review helpful", "out of 5 stars", verified purchase) in the first five
|
|
34
|
+
content docs; a strong fingerprint flags a doc alone, weak ones
|
|
35
|
+
(reviewer bylines, star rows) need corroboration.
|
|
36
|
+
- **The corpus is the archive's content documents, not just the spine.**
|
|
37
|
+
Hothouse's manifest is broken: 52 of its 53 content docs sit in no
|
|
38
|
+
manifest item, the spine resolves to one document, and a spine-only sweep
|
|
39
|
+
would have read the book clean. The loader now retains the archive's
|
|
40
|
+
non-spine html on `Book.extra_docs` (archive order, 64 MiB retention cap;
|
|
41
|
+
a pathological archive stops feeding the corpus there and stays the
|
|
42
|
+
corruption/completeness verdicts' business), and the contamination sweeps
|
|
43
|
+
run over spine plus extras.
|
|
44
|
+
- **Directory and single-book mode actually run cover and tocdrift now.**
|
|
45
|
+
`run_directory`/`run_single` dispatched analyzers through an if/elif chain
|
|
46
|
+
whose terminal `else` ran the ocr analyzer, so since v0.44.0 an
|
|
47
|
+
`audit all` over a directory (or `audit all --id`) reported ocr verdicts
|
|
48
|
+
under the cover and tocdrift keys and never analyzed cover wiring or ToC
|
|
49
|
+
drift. Library mode was unaffected. Both modes now share a dispatch map
|
|
50
|
+
keyed on the analyzer names (`_dir_dispatch`), a missing key is a loud
|
|
51
|
+
KeyError instead of a mislabeled verdict, and `tests/test_contamination.py`
|
|
52
|
+
pins the map's key set to the analyzer list. Advisory analyzers never
|
|
53
|
+
answer problem, so `audit all` and multi-analyzer runs cannot change their
|
|
54
|
+
verdicts; the one behavioral corner is a single-key `audit cover DIR` (or
|
|
55
|
+
`--id`) run, which at HEAD actually executed the ocr analyzer and could
|
|
56
|
+
exit 1 on an ocr-damaged book under the cover key, and now runs cover
|
|
57
|
+
wiring and exits on cover's own (always-advisory) verdict.
|
|
58
|
+
- CalibreQuarry-style `--json` consumers see the new analyzer's verdicts as
|
|
59
|
+
an additive `contamination` key (problem always false); nothing else in
|
|
60
|
+
the audit record shape moved.
|
|
61
|
+
|
|
3
62
|
## v0.49.0 (2026-10-13)
|
|
4
63
|
|
|
5
64
|
### The single-file repair probe writes nothing (issue #7)
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "bindery-cli"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.50.0"
|
|
8
8
|
description = "Audit EPUB body text and repair broken EPUBs with deterministic, epubcheck-gated fixes; atomic in-place replacement in a Calibre library."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
classifiers = [
|
|
@@ -43,6 +43,7 @@ The plugin vendors the core and never gates (the epubcheck latency ruling).
|
|
|
43
43
|
| 0.47 | The intake-wave batch (five open issues): the stub classifier's body-pool + image-carrier exemption, the NCName-complete `--fix-ids`, and `--fix-svg-dup-ids`, `--fix-cdata-terminator`, `--fix-misnested-inline`, `--fix-stray-close`, `--fix-unterminated-attr` | v0.47.0 |
|
|
44
44
|
| 0.48 | The AI-probe batch: the enforced closed-Calibre gate on both metadata.db doors (`audit --tag`, `library --apply --install-to-calibre`), the stated contracts (exit codes, no-prompt, NCX-001, --id scoping, json envelope keys), and `--help-json` | v0.48.0 |
|
|
45
45
|
| 0.49 | The probe form (issue #7): single-file repair without an output path runs the gate and writes nothing; writing needs an explicit DST | v0.49.0 |
|
|
46
|
+
| 0.50 | The contamination analyzer (issue #8): foreign-title template stamps, fan-bundle ad signatures, review-paste front matter; advisory by contract, archive-wide corpus (`Book.extra_docs`, capped), and the `_dir_dispatch` map that fixes the v0.44.0 cover/tocdrift ocr misroute in directory/single mode | v0.50.0 |
|
|
46
47
|
|
|
47
48
|
## Open work
|
|
48
49
|
|
|
@@ -451,8 +451,8 @@ This door is lock-class: `--install-to-calibre` is REFUSED with exit 1 while Cal
|
|
|
451
451
|
|
|
452
452
|
## Audit subcommand (read-only)
|
|
453
453
|
|
|
454
|
-
`bindery audit {content,pagenumbers,emptytext,ocr,monolithic,completeness,cover,tocdrift,all} [PATH] [--max-doc-chars N]
|
|
455
|
-
[--tag TAG] [--id IDs]` (v0.15.0, `audit.py`; `--tag` since v0.18.0; `monolithic` since v0.21.0, `--max-doc-chars N`; `--id` since v0.19.0, comma-lists in v0.23.0; `completeness` since v0.36.0; `cover` and `tocdrift` since v0.44.0) inspects
|
|
454
|
+
`bindery audit {content,pagenumbers,emptytext,ocr,monolithic,completeness,cover,tocdrift,contamination,all} [PATH] [--max-doc-chars N]
|
|
455
|
+
[--tag TAG] [--id IDs]` (v0.15.0, `audit.py`; `--tag` since v0.18.0; `monolithic` since v0.21.0, `--max-doc-chars N`; `--id` since v0.19.0, comma-lists in v0.23.0; `completeness` since v0.36.0; `cover` and `tocdrift` since v0.44.0; `contamination` since v0.50.0) inspects
|
|
456
456
|
EPUB body text for flaws epubcheck cannot see: non-English script blocks, baked-in page-number
|
|
457
457
|
layers (sliding-window density heuristics), empty or thin books, systemic OCR damage, and
|
|
458
458
|
single oversized content documents (one spine doc at or above 300k characters: readers refuse
|
|
@@ -477,6 +477,28 @@ entries each side is missing and labels that disagree; it feeds decisions, never
|
|
|
477
477
|
rewrite (ToC synthesis is out of the repair charter permanently). Both never flag a
|
|
478
478
|
book and never move the exit code.
|
|
479
479
|
|
|
480
|
+
The contamination analyzer (v0.50.0, issue #8) is advisory by the same contract:
|
|
481
|
+
fan-bundle cosmetics are the judge-and-import class, so it never flags a book and
|
|
482
|
+
never moves an exit code. Three sweeps run over the archive's content documents
|
|
483
|
+
(the spine plus the non-spine html the loader retains on `Book.extra_docs`,
|
|
484
|
+
archive order, 64 MiB retention cap): *foreign titles* name a title or series that
|
|
485
|
+
matches none of the package's own metadata (dc:title, creator, publisher, series),
|
|
486
|
+
flagged either by a volume/book marker with named words in front of it or by a
|
|
487
|
+
head `<title>` repeated across the book (a part label whose marker leads, "BOOK
|
|
488
|
+
ONE: ...", never flags; the comparison also runs on the volume-marker-stripped
|
|
489
|
+
form so a book's own running headers stay clean); *ad signatures* (jnovels,
|
|
490
|
+
kokoro connect, zerobooks, a generic light-novel download plug) are searched in
|
|
491
|
+
visible body text, with a signature that matches the book's own metadata
|
|
492
|
+
skipped outright (a legitimate Kokoro Connect volume names its own series on
|
|
493
|
+
every doc); *review paste* (helpful counts, review-helpful prompts,
|
|
494
|
+
"out of 5 stars", verified purchase, with reviewer bylines and star rows as
|
|
495
|
+
corroborating-only weak fingerprints) is scoped to the first five content docs.
|
|
496
|
+
The archive-wide corpus exists because fan-bundle junk is package furniture a
|
|
497
|
+
broken manifest can leave entirely off the spine (Hothouse: 52 of 53 content
|
|
498
|
+
docs in no manifest item, a resolved spine of one). On a shell book (no package
|
|
499
|
+
metadata) the analyzer reports nothing: there is no "own" to judge foreignness
|
|
500
|
+
against.
|
|
501
|
+
|
|
480
502
|
The completeness analyzer is the phase-1 spot-check and is advisory by contract: it never
|
|
481
503
|
flags a book and never moves the exit code. Per book it reports the spine doc count, the
|
|
482
504
|
prose-doc count (spine docs carrying at least 400 visible characters), the opening and closing
|
|
@@ -11,7 +11,7 @@ Calibre plugin built from this core.
|
|
|
11
11
|
Single source of truth for the version. pyproject.toml must match VERSION below.
|
|
12
12
|
"""
|
|
13
13
|
|
|
14
|
-
VERSION = "0.
|
|
14
|
+
VERSION = "0.50.0"
|
|
15
15
|
__version__ = VERSION
|
|
16
16
|
|
|
17
17
|
from .epub import RepairReport, repair_epub # noqa: E402
|