bindery-cli 0.43.0__tar.gz → 0.44.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. {bindery_cli-0.43.0 → bindery_cli-0.44.0}/.gitignore +1 -0
  2. {bindery_cli-0.43.0 → bindery_cli-0.44.0}/CLAUDE.md +8 -2
  3. {bindery_cli-0.43.0 → bindery_cli-0.44.0}/PKG-INFO +1 -1
  4. {bindery_cli-0.43.0 → bindery_cli-0.44.0}/patchnotes.md +11 -0
  5. {bindery_cli-0.43.0 → bindery_cli-0.44.0}/pyproject.toml +1 -1
  6. {bindery_cli-0.43.0 → bindery_cli-0.44.0}/roadmap.md +90 -9
  7. {bindery_cli-0.43.0 → bindery_cli-0.44.0}/spec.md +25 -7
  8. {bindery_cli-0.43.0 → bindery_cli-0.44.0}/src/bindery/__init__.py +1 -1
  9. {bindery_cli-0.43.0 → bindery_cli-0.44.0}/src/bindery/audit.py +301 -22
  10. {bindery_cli-0.43.0 → bindery_cli-0.44.0}/src/bindery/cli.py +15 -0
  11. {bindery_cli-0.43.0 → bindery_cli-0.44.0}/src/bindery/epub.py +244 -16
  12. {bindery_cli-0.43.0 → bindery_cli-0.44.0}/src/bindery/pagination.py +8 -4
  13. {bindery_cli-0.43.0 → bindery_cli-0.44.0}/src/bindery/transforms.py +7 -2
  14. bindery_cli-0.44.0/tests/test_cover_tocdrift.py +207 -0
  15. {bindery_cli-0.43.0 → bindery_cli-0.44.0}/tests/test_epub.py +17 -7
  16. bindery_cli-0.44.0/tests/test_space_renames.py +263 -0
  17. {bindery_cli-0.43.0 → bindery_cli-0.44.0}/uv.lock +1 -1
  18. bindery_cli-0.43.0/testing_facility/books_to_fix/.gitkeep +0 -0
  19. {bindery_cli-0.43.0 → bindery_cli-0.44.0}/.github/workflows/ci.yml +0 -0
  20. {bindery_cli-0.43.0 → bindery_cli-0.44.0}/.github/workflows/publish.yml +0 -0
  21. {bindery_cli-0.43.0 → bindery_cli-0.44.0}/AGENTS.md +0 -0
  22. {bindery_cli-0.43.0 → bindery_cli-0.44.0}/LICENSE +0 -0
  23. {bindery_cli-0.43.0 → bindery_cli-0.44.0}/README.md +0 -0
  24. {bindery_cli-0.43.0 → bindery_cli-0.44.0}/docs/screenshots/library-sweep.png +0 -0
  25. {bindery_cli-0.43.0 → bindery_cli-0.44.0}/logo.svg +0 -0
  26. {bindery_cli-0.43.0 → bindery_cli-0.44.0}/plugin/__init__.py +0 -0
  27. {bindery_cli-0.43.0 → bindery_cli-0.44.0}/project.done +0 -0
  28. {bindery_cli-0.43.0 → bindery_cli-0.44.0}/run_tests.sh +0 -0
  29. {bindery_cli-0.43.0 → bindery_cli-0.44.0}/scripts/FastSweep.java +0 -0
  30. {bindery_cli-0.43.0 → bindery_cli-0.44.0}/scripts/build_plugin.py +0 -0
  31. {bindery_cli-0.43.0 → bindery_cli-0.44.0}/scripts/fast_sweep.py +0 -0
  32. {bindery_cli-0.43.0 → bindery_cli-0.44.0}/src/bindery/__main__.py +0 -0
  33. {bindery_cli-0.43.0 → bindery_cli-0.44.0}/src/bindery/library.py +0 -0
  34. {bindery_cli-0.43.0 → bindery_cli-0.44.0}/src/bindery/reserialize.py +0 -0
  35. {bindery_cli-0.43.0 → bindery_cli-0.44.0}/src/bindery/validate.py +0 -0
  36. {bindery_cli-0.43.0 → bindery_cli-0.44.0}/src/bindery/watermark.py +0 -0
  37. {bindery_cli-0.43.0 → bindery_cli-0.44.0}/tests/test_audit.py +0 -0
  38. {bindery_cli-0.43.0 → bindery_cli-0.44.0}/tests/test_cli.py +0 -0
  39. {bindery_cli-0.43.0 → bindery_cli-0.44.0}/tests/test_library.py +0 -0
  40. {bindery_cli-0.43.0 → bindery_cli-0.44.0}/tests/test_matcher_hardening.py +0 -0
  41. {bindery_cli-0.43.0 → bindery_cli-0.44.0}/tests/test_pagination.py +0 -0
  42. {bindery_cli-0.43.0 → bindery_cli-0.44.0}/tests/test_plugin.py +0 -0
  43. {bindery_cli-0.43.0 → bindery_cli-0.44.0}/tests/test_reserialize.py +0 -0
  44. {bindery_cli-0.43.0 → bindery_cli-0.44.0}/tests/test_transforms.py +0 -0
  45. {bindery_cli-0.43.0 → bindery_cli-0.44.0}/tests/test_validate.py +0 -0
  46. {bindery_cli-0.43.0 → bindery_cli-0.44.0}/tests/test_version.py +0 -0
  47. {bindery_cli-0.43.0 → bindery_cli-0.44.0}/tests/test_watermark.py +0 -0
  48. {bindery_cli-0.43.0 → bindery_cli-0.44.0}/tests/test_xml_hardening.py +0 -0
@@ -29,3 +29,4 @@ test_facility/*.epub
29
29
  # which is the only reason they were invisible until now)
30
30
  venv/
31
31
  .venv_ci/
32
+ testing_facility/top500candidates/*.txt
@@ -37,7 +37,13 @@ Born from the 2026 library audit (see the user memory `calibre-library-epubcheck
37
37
  `--unwrap-block-in-inline`, `--strip-invalid-value`, `--unwrap-illegal-tags`,
38
38
  `--prune-missing-resources`, `--strip-broken-anchors`, `--encode-url-spaces`,
39
39
  `--fix-container`, `--fix-media-types`, `--fix-cover`; transforms.py, threaded
40
- through epub.py):
40
+ through epub.py). Since v0.44.0 `--encode-url-spaces` also RENAMES the
41
+ archive entries whose names carry raw spaces to their underscore spellings
42
+ and rewrites every reference (OPF, NCX, content, CSS `url()`); underscore,
43
+ never percent-encoding (epubcheck decodes references before entry lookup,
44
+ verified against 5.3), ambiguous renames refused per entry (`space_rename_map`),
45
+ accepted under `no_worse` because PKG-010 sits on the warning axis the gate
46
+ does not measure:
41
47
  they alter markup structure or fabricate minimal content. v0.14–v0.16 ran these unconditionally, which broke this rule;
42
48
  v0.17.0 restored it. `--unwrap-illegal-tags` additionally protects any illegal-tag
43
49
  name that an EPUB stylesheet styles as an element selector (`css_protected_tags`,
@@ -86,7 +92,7 @@ Born from the 2026 library audit (see the user memory `calibre-library-epubcheck
86
92
  - `src/bindery/pagination.py`: the opt-in lossy page-number strip (runhead detection, page-layer decision, block-centric removal/merge, safety nets).
87
93
  - `src/bindery/watermark.py`: the opt-in lossy watermark strip (anchored and anchorless signature removal).
88
94
  - `src/bindery/reserialize.py`: structural repair via `html5lib`.
89
- - `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.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).
95
+ - `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).
90
96
  - `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.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).
91
97
  - `src/bindery/validate.py`: epubcheck wrapper, the `gate` (improvement) and `no_worse` (no-regression, for the lossy strips) acceptance bars.
92
98
  - `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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: bindery-cli
3
- Version: 0.43.0
3
+ Version: 0.44.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
@@ -1,4 +1,15 @@
1
1
  # bindery-cli Patch Notes
2
+ ## v0.44.0 (2026-09-17)
3
+
4
+ ### The wave-2 batch: PKG-010 entry renames, the cover and ToC-drift audit slices, and the roman grammar unified
5
+
6
+ * **`--encode-url-spaces` gained its rename half**, the biggest real-prevalence repair class left (PKG-010: 8,237 occurrences in 284 books at the 2026-09-17 refresh). Archive entries whose names carry raw spaces are renamed to their underscore spellings and every reference is rewritten (OPF, NCX, content `src`/`href`, stylesheet `url()`). The design was decided by experiment, not by the roadmap box's plan: epubcheck 5.3 DECODES references before entry lookup, so percent-encoded entries are unfindable by the encoded references pointing at them (all four raw/percent/underscore combinations built and checked); underscore on both sides resolves and clears PKG-010. Ambiguous renames are refused per entry (a target that already exists, two names converging, a duplicated archive name), a book with any non-UTF-8 document refuses all of its renames, a renamed OPF regenerates a stale container, and the acceptance rides `no_worse` because the gain sits on the warning axis the improvement gate does not measure (same bar as `--fix-cover`).
7
+ * **The matcher fix that explains Theaetetus** (the 2026-09-16 hand-rescue): the encode pass's value class `[^"']` stopped at apostrophes inside double-quoted hrefs, so the 270 raw-space OPF hrefs never matched and the fatal survived. The matcher now takes the tempered-quote idiom (possessive, house style), and an apostrophe in a double-quoted filename is ordinary.
8
+ * **`bindery audit cover`** (advisory, the EPUB3 half of the ruled hybrid): dangling EPUB2 `<meta name="cover">` (the `--fix-cover` class), the EPUB3 `properties~="cover-image"` declaration, and whether the named cover file exists. **`bindery audit tocdrift`**: the structural diff between the NCX navMap and the EPUB3 nav toc, keyed on the full target including fragments -- entries each side is missing plus label mismatches. Both never flag a book and never move the exit code; ToC synthesis stays out of the repair charter permanently. The loader exposes the parsed OPF, the NCX text, and the nav HTML on `Book`.
9
+ * **The audit-refresh deep-dive ran** (read-only FastSweep extract over the full 5,228-book library; raw report in the facility): RSC-005 164,302 -> 13,782 occurrences, RSC-007 3,219 -> 14,162 in 604 books (the classics-wave imports), RSC-020 9,953 in 337 books, PKG-010 8,237 in 284 books (population flat: the rename class was still live, hence this ship).
10
+ * **The roman grammar is one copy again**: pagination.py is the canonical home (it vendors into the plugin; audit does not), audit.py imports `ROMAN_RE`/`ROMAN_MAX`/`roman_value`, and the two byte-identical copies cannot drift. The RepairFlags dataclass refactor stays boxed with its reason (pure churn on the signatures this release touched).
11
+ * Suite: 497 -> 521 tests (tests/test_space_renames.py, tests/test_cover_tocdrift.py, and the updated `--encode-url-spaces` end-to-end pins).
12
+
2
13
  ## v0.43.0 (2026-09-16)
3
14
 
4
15
  ### The stdlib XML hardening: untrusted metadata is bounded and entity-refused, no dependency
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "bindery-cli"
7
- version = "0.43.0"
7
+ version = "0.44.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 = [
@@ -1812,25 +1812,72 @@ four interpreter families.
1812
1812
 
1813
1813
  ### Reopened / boxed by the final blitz (2026-09-15)
1814
1814
 
1815
- - [ ] **--encode-url-spaces entry renames** (PKG-010, top-6 warning
1815
+ - [x] **--encode-url-spaces entry renames** (PKG-010, top-6 warning
1816
1816
  class, 280 books): rename archive entries whose filenames carry
1817
1817
  raw spaces, rewriting every reference; the reference half
1818
1818
  shipped v0.28.0. Biggest real-prevalence repair class left; the
1819
- gate must re-measure the whole book.
1820
- - [ ] **Cover advisory analyzer**: the EPUB3 `properties="cover-image"`
1819
+ gate must re-measure the whole book. *(SHIPPED 2026-09-17,
1820
+ v0.44.0 -- with the plan CHANGED by experiment. epubcheck 5.3
1821
+ DECODES references before entry lookup, so percent-encoded entry
1822
+ names are unfindable by the encoded refs pointing at them
1823
+ (variants built and checked: raw/raw = PKG-010 only,
1824
+ pct/pct = broken lookups, pct-raw = broken, underscore/underscore
1825
+ = clean); renames therefore use the underscore spelling, and
1826
+ references are rewritten to the identical form. Ambiguous renames
1827
+ are refused per entry (target exists, two names converging, a
1828
+ duplicated archive name); a book with any non-UTF-8 document
1829
+ refuses all renames. The rename rides the existing
1830
+ --encode-url-spaces flag and is accepted under no_worse (its gain
1831
+ sits on the warning axis the gate does not measure; same bar as
1832
+ fix-cover). The refresh also found the real reason Theaetetus's
1833
+ OPF fatal survived the 0.28 reference half: the value class
1834
+ [^"'] stopped at apostrophes inside double-quoted hrefs; the
1835
+ matcher now takes the tempered-quote idiom. Refresh counts:
1836
+ PKG-010 at 8,237 occurrences in 284 books (was 4,112 in ~280;
1837
+ the class persisted). Pinned by tests/test_space_renames.py
1838
+ plus the updated test_epub pins.)*
1839
+ - [x] **Cover advisory analyzer**: the EPUB3 `properties="cover-image"`
1821
1840
  audit slice completing the ruled hybrid (21 dangling EPUB2 metas
1822
- and 3 absent cover files measured behind it).
1823
- - [ ] **Audit-refresh deep-dive**: per-class deltas vs the 2026-09-12
1824
- baselines; sizes the two boxes above for free.
1825
- - [ ] **NCX<->nav drift detector** (audit-only): the remaining
1826
- unticked Phase 16 C box; size it after the deep-dive.
1841
+ and 3 absent cover files measured behind it). *(SHIPPED 2026-09-17,
1842
+ v0.44.0: `bindery audit cover` reads the package's own wiring --
1843
+ dangling EPUB2 meta, EPUB3 property declaration, cover-file
1844
+ presence -- advisory by contract like completeness; the loader
1845
+ now exposes the parsed OPF.)*
1846
+ - [x] **Audit-refresh deep-dive**: per-class deltas vs the 2026-09-12
1847
+ baselines; sizes the two boxes above for free. *(DONE 2026-09-17:
1848
+ read-only FastSweep extract over the full 5,228-book library;
1849
+ raw report at testing_facility/top500candidates/
1850
+ extract-2026-09-17.txt. RSC-005 164,302 -> 13,782 occurrences
1851
+ (the repair waves did land); RSC-007 3,219 -> 14,162 in 604
1852
+ books (up: the classics-wave imports); RSC-020 8,431 -> 9,953
1853
+ in 337 books; PKG-010 4,112 -> 8,237 in 284 books (population
1854
+ flat -- the rename class stayed live, hence the ship). 2,910
1855
+ books carry findings.)*
1856
+ - [x] **NCX<->nav drift detector** (audit-only): the remaining
1857
+ unticked Phase 16 C box; size it after the deep-dive. *(SHIPPED
1858
+ 2026-09-17, v0.44.0: `bindery audit tocdrift` diffs the NCX
1859
+ navMap against the EPUB3 nav toc -- entries each side is missing
1860
+ plus label mismatches, keyed on the full target including the
1861
+ fragment. Advisory by contract; the loader exposes the NCX text
1862
+ and the nav document's HTML. ToC synthesis stays out of charter
1863
+ permanently.)*
1827
1864
  - [ ] **Internal refactors (polish, no behavior change)**: a
1828
1865
  RepairFlags dataclass for the triplicated ~25-kwarg
1829
1866
  process_book/repair_epub signature; shared roman/arabic number
1830
1867
  helpers for pagination.py and audit.py (their year-range
1831
1868
  grammars differ by design: document or unify, never a third
1832
1869
  copy). Deliberately not refactored mid-blitz: pure churn risk
1833
- around five releases of work.
1870
+ around five releases of work. *(Roman/arabic half DONE
1871
+ 2026-09-17, v0.44.0: the two copies were byte-identical, so the
1872
+ box resolves to UNIFY -- pagination.py is the canonical home
1873
+ (it vendors into the plugin; audit does not), audit.py imports
1874
+ ROMAN_RE/ROMAN_MAX/roman_value from it, and the comment records
1875
+ that any future divergence is a decision at pagination, not a
1876
+ fork. RepairFlags dataclass DEFERRED with reason: pure churn on
1877
+ a ~25-kwarg surface immediately after the v0.44 batch touched
1878
+ exactly those signatures; do it as a standalone lane with the
1879
+ full suite and the plugin byte-compat job green at every step,
1880
+ not as a rider.)*
1834
1881
  - [ ] **MobileRead listing for the Bindery Repair plugin** (Brandon's
1835
1882
  manual step): post the listing with name/identity, the
1836
1883
  release-attached zip, minimum Calibre 7.0, Linux platform note.
@@ -1902,3 +1949,37 @@ four interpreter families.
1902
1949
  15 tests (spin shapes alarm-guarded per greedy site, unterminated-
1903
1950
  quote skips, well-formed byte-identity, prune spin over the
1904
1951
  truncated-OPF shape). Never a third grammar copy.)*
1952
+
1953
+ - [ ] **`--encode-url-spaces` left an OPF-manifest RSC-020 FATAL standing
1954
+ on the Theaetetus fixture** (observed 2026-09-16, classics wave
1955
+ phase 1): the Hackett Aristotle *Theaetetus* EPUB (McDowell) arrived
1956
+ with 270 raw-space zip entry names AND 270 raw-space hrefs in
1957
+ content.opf — epubcheck fatal RSC-020 at content.opf(33,110). The
1958
+ `--all` sweep's encode pass recorded `url_spaces_encoded:351` and
1959
+ dropped errors 356→6, but the fatal survived it (1f before → 1f
1960
+ after). A hand rebuild (percent-encode every reference in
1961
+ .opf/.ncx/.xhtml while KEEPING the raw-space entry names — epubcheck
1962
+ decodes hrefs to find entries, so renaming entries to encoded form
1963
+ breaks lookups the other way) cleared the fatal entirely: 0f.
1964
+ Suspect the encode pass covers content docs and NCX but not the OPF
1965
+ manifest's own href attributes, or a reserialize round-trip writes
1966
+ them back decoded. Fixture: the imported book (id 9135, the file was
1967
+ hand-fixed in place) — the defects and the fix script's operations
1968
+ are documented in the library's phase-1 record for 2026-09-16.
1969
+ *(RESOLVED 2026-09-17, v0.44.0: neither suspicion was right. The
1970
+ real cause was the value class [^"'] in the encode matcher, which
1971
+ stopped at the apostrophes in the OPF's filenames -- the OPF
1972
+ branch DID run but matched nothing. The tempered-quote idiom
1973
+ fixes the matcher, and the entry-rename half clears the class
1974
+ outright.)*
1975
+
1976
+ - [ ] **Candidate transform: `--` inside XML comments (RSC-016 fatal)**.
1977
+ Same book: `OEBPS/...split_269.html` carried a `--` inside an XML
1978
+ comment (illegal XML, fatal RSC-016), costing the file its only
1979
+ fatal after the OPF fix. Hand fix applied: replace `--` with an
1980
+ en-dash inside comment nodes (2 sites, prose preserved). A small
1981
+ `fix_comment_double_hyphen` transform (comment nodes only, `--` ->
1982
+ en-dash, byte-counted) would make the always-on core able to parse
1983
+ such books at all — until then the book parses in lenient readers
1984
+ but never passes epubcheck. Low frequency (one book in the wave),
1985
+ noted with the fixture reference above.
@@ -184,11 +184,20 @@ every EPUB3 book in an `--all` sweep taking exactly that damage).
184
184
  reader resolves (`kindle:embed:`, `file:`, ...) are stripped under the same flag; the
185
185
  resolvable set is fixed (`http`, `https`, `mailto`) and extends only with a named
186
186
  finding.
187
- - **`--encode-url-spaces`**: percent-encode raw spaces in `src`/`href` attribute values
188
- across the package (OPF manifest, NCX `content src`, content documents). A URL with a
189
- literal space is not a valid URL (RSC-020 "not a valid URL") and unresolvable on
190
- strict readers; the encoded form denotes the same file and renders identically. Scope
191
- is fixed to the space character: extend only with a named epubcheck finding.
187
+ - **`--encode-url-spaces`**: repairs raw-space URLs two ways. The reference half
188
+ percent-encodes raw spaces in `src`/`href` attribute values across the package
189
+ (OPF manifest, NCX `content src`, content documents): a URL with a literal space is
190
+ not a valid URL (RSC-020 "not a valid URL") and unresolvable on strict readers; the
191
+ encoded form denotes the same file and renders identically. The rename half (v0.44.0)
192
+ renames archive entries whose names carry raw spaces to their underscore spellings
193
+ and rewrites every reference to them (OPF, NCX, content `src`/`href`, stylesheet
194
+ `url()`), clearing PKG-010: underscore, never percent-encoding, because epubcheck
195
+ decodes references before entry lookup (both-sides-%20 breaks lookups; verified
196
+ against epubcheck 5.3). Ambiguous renames (a target that already exists, two names
197
+ converging, a duplicated entry name) are refused per entry, and a book with any
198
+ non-UTF-8 document refuses all of its renames (those entries pass through
199
+ byte-for-byte with no chance to rewrite their references). Scope stays fixed to the
200
+ space character: extend only with a named epubcheck finding.
192
201
 
193
202
  - **`--fix-container`**: generate `META-INF/container.xml` at the located OPF when the
194
203
  container is missing, unparseable, or names a file the archive does not contain. This is the
@@ -400,8 +409,8 @@ crashing the sweep.
400
409
 
401
410
  ## Audit subcommand (read-only)
402
411
 
403
- `bindery audit {content,pagenumbers,emptytext,ocr,monolithic,completeness,all} [PATH] [--max-doc-chars N]
404
- [--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) inspects
412
+ `bindery audit {content,pagenumbers,emptytext,ocr,monolithic,completeness,cover,tocdrift,all} [PATH] [--max-doc-chars N]
413
+ [--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
405
414
  EPUB body text for flaws epubcheck cannot see: non-English script blocks, baked-in page-number
406
415
  layers (sliding-window density heuristics), empty or thin books, systemic OCR damage, and
407
416
  single oversized content documents (one spine doc at or above 300k characters: readers refuse
@@ -417,6 +426,15 @@ read fine are the OBFUSCATED advisory (publisher embedding, benign, never a fail
417
426
  unreadable obfuscation entry is CORRUPT (a broken font, not a business model), and only
418
427
  non-obfuscation algorithms give the ENCRYPTED verdict with its DRM skip advice.
419
428
 
429
+ Two more advisory analyzers ship in v0.44.0. `cover` reads the package's own cover
430
+ wiring (the EPUB3 half of the ruled hybrid): a dangling EPUB2 `<meta name="cover">`
431
+ (the `--fix-cover` repair class), an EPUB3 `properties~="cover-image"` declaration,
432
+ and whether the named cover file exists. `tocdrift` diffs the two tables of contents
433
+ an EPUB 3 book may carry -- the NCX navMap against the nav document -- reporting
434
+ entries each side is missing and labels that disagree; it feeds decisions, never a
435
+ rewrite (ToC synthesis is out of the repair charter permanently). Both never flag a
436
+ book and never move the exit code.
437
+
420
438
  The completeness analyzer is the phase-1 spot-check and is advisory by contract: it never
421
439
  flags a book and never moves the exit code. Per book it reports the spine doc count, the
422
440
  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.43.0"
14
+ VERSION = "0.44.0"
15
15
  __version__ = VERSION
16
16
 
17
17
  from .epub import RepairReport, repair_epub # noqa: E402