bindery-cli 0.33.0__tar.gz → 0.34.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. {bindery_cli-0.33.0 → bindery_cli-0.34.0}/PKG-INFO +2 -2
  2. {bindery_cli-0.33.0 → bindery_cli-0.34.0}/README.md +1 -1
  3. {bindery_cli-0.33.0 → bindery_cli-0.34.0}/patchnotes.md +39 -3
  4. {bindery_cli-0.33.0 → bindery_cli-0.34.0}/pyproject.toml +1 -1
  5. {bindery_cli-0.33.0 → bindery_cli-0.34.0}/roadmap.md +21 -2
  6. {bindery_cli-0.33.0 → bindery_cli-0.34.0}/spec.md +28 -7
  7. {bindery_cli-0.33.0 → bindery_cli-0.34.0}/src/bindery/__init__.py +1 -1
  8. {bindery_cli-0.33.0 → bindery_cli-0.34.0}/src/bindery/cli.py +6 -1
  9. {bindery_cli-0.33.0 → bindery_cli-0.34.0}/tests/test_cli.py +43 -0
  10. {bindery_cli-0.33.0 → bindery_cli-0.34.0}/uv.lock +1 -1
  11. {bindery_cli-0.33.0 → bindery_cli-0.34.0}/.github/workflows/ci.yml +0 -0
  12. {bindery_cli-0.33.0 → bindery_cli-0.34.0}/.github/workflows/publish.yml +0 -0
  13. {bindery_cli-0.33.0 → bindery_cli-0.34.0}/.gitignore +0 -0
  14. {bindery_cli-0.33.0 → bindery_cli-0.34.0}/AGENTS.md +0 -0
  15. {bindery_cli-0.33.0 → bindery_cli-0.34.0}/CLAUDE.md +0 -0
  16. {bindery_cli-0.33.0 → bindery_cli-0.34.0}/LICENSE +0 -0
  17. {bindery_cli-0.33.0 → bindery_cli-0.34.0}/docs/screenshots/library-sweep.png +0 -0
  18. {bindery_cli-0.33.0 → bindery_cli-0.34.0}/logo.svg +0 -0
  19. {bindery_cli-0.33.0 → bindery_cli-0.34.0}/run_tests.sh +0 -0
  20. {bindery_cli-0.33.0 → bindery_cli-0.34.0}/scripts/FastSweep.java +0 -0
  21. {bindery_cli-0.33.0 → bindery_cli-0.34.0}/scripts/fast_sweep.py +0 -0
  22. {bindery_cli-0.33.0 → bindery_cli-0.34.0}/src/bindery/__main__.py +0 -0
  23. {bindery_cli-0.33.0 → bindery_cli-0.34.0}/src/bindery/audit.py +0 -0
  24. {bindery_cli-0.33.0 → bindery_cli-0.34.0}/src/bindery/epub.py +0 -0
  25. {bindery_cli-0.33.0 → bindery_cli-0.34.0}/src/bindery/library.py +0 -0
  26. {bindery_cli-0.33.0 → bindery_cli-0.34.0}/src/bindery/pagination.py +0 -0
  27. {bindery_cli-0.33.0 → bindery_cli-0.34.0}/src/bindery/reserialize.py +0 -0
  28. {bindery_cli-0.33.0 → bindery_cli-0.34.0}/src/bindery/transforms.py +0 -0
  29. {bindery_cli-0.33.0 → bindery_cli-0.34.0}/src/bindery/validate.py +0 -0
  30. {bindery_cli-0.33.0 → bindery_cli-0.34.0}/src/bindery/watermark.py +0 -0
  31. {bindery_cli-0.33.0 → bindery_cli-0.34.0}/testing_facility/books_to_fix/.gitkeep +0 -0
  32. {bindery_cli-0.33.0 → bindery_cli-0.34.0}/testing_facility/top500candidates/REPORT.md +0 -0
  33. {bindery_cli-0.33.0 → bindery_cli-0.34.0}/tests/test_audit.py +0 -0
  34. {bindery_cli-0.33.0 → bindery_cli-0.34.0}/tests/test_epub.py +0 -0
  35. {bindery_cli-0.33.0 → bindery_cli-0.34.0}/tests/test_library.py +0 -0
  36. {bindery_cli-0.33.0 → bindery_cli-0.34.0}/tests/test_pagination.py +0 -0
  37. {bindery_cli-0.33.0 → bindery_cli-0.34.0}/tests/test_reserialize.py +0 -0
  38. {bindery_cli-0.33.0 → bindery_cli-0.34.0}/tests/test_transforms.py +0 -0
  39. {bindery_cli-0.33.0 → bindery_cli-0.34.0}/tests/test_validate.py +0 -0
  40. {bindery_cli-0.33.0 → bindery_cli-0.34.0}/tests/test_version.py +0 -0
  41. {bindery_cli-0.33.0 → bindery_cli-0.34.0}/tests/test_watermark.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: bindery-cli
3
- Version: 0.33.0
3
+ Version: 0.34.0
4
4
  Summary: Audit EPUB body text and repair broken EPUBs with deterministic, epubcheck-gated fixes; atomic in-place replacement in a Calibre library.
5
5
  Author: vrnvctss
6
6
  License: MIT
@@ -209,7 +209,7 @@ bindery library ~/docs/Calibre\ Library --only all --apply --all --install-to-ca
209
209
  - Only the `.epub` is replaced. `metadata.opf`, `cover.jpg`, and `metadata.db` are left for Calibre's Quality Check sync to reconcile.
210
210
  - A per-book progress line goes to stderr (stdout stays a clean report); `--quiet` suppresses it. A corrupt or unreadable book is reported and skipped, never aborting the sweep.
211
211
  - Exit codes: 0 for a clean sweep, 1 for a usage error, 2 when any book was rejected, unreadable, or failed epubcheck (for scripts and cron). Note that argparse-level misuse (an unknown flag or a malformed argument) exits with 2 before any validation runs; the tool's own usage validations exit 1.
212
- - A `partial` book (improved but still unable to open) is reported for manual follow-up and does not fail a `library` run; `run phase1` counts it as trouble and exits 2.
212
+ - A `partial` book (improved but still unable to open) is reported for manual follow-up and fails the run with exit 2, in `library`, `run phase1`, and `run phase3` alike: a book that still needs a human is trouble.
213
213
  - `repair` refuses to overwrite an existing output file unless `--force` is given.
214
214
 
215
215
  ## Run slices
@@ -187,7 +187,7 @@ bindery library ~/docs/Calibre\ Library --only all --apply --all --install-to-ca
187
187
  - Only the `.epub` is replaced. `metadata.opf`, `cover.jpg`, and `metadata.db` are left for Calibre's Quality Check sync to reconcile.
188
188
  - A per-book progress line goes to stderr (stdout stays a clean report); `--quiet` suppresses it. A corrupt or unreadable book is reported and skipped, never aborting the sweep.
189
189
  - Exit codes: 0 for a clean sweep, 1 for a usage error, 2 when any book was rejected, unreadable, or failed epubcheck (for scripts and cron). Note that argparse-level misuse (an unknown flag or a malformed argument) exits with 2 before any validation runs; the tool's own usage validations exit 1.
190
- - A `partial` book (improved but still unable to open) is reported for manual follow-up and does not fail a `library` run; `run phase1` counts it as trouble and exits 2.
190
+ - A `partial` book (improved but still unable to open) is reported for manual follow-up and fails the run with exit 2, in `library`, `run phase1`, and `run phase3` alike: a book that still needs a human is trouble.
191
191
  - `repair` refuses to overwrite an existing output file unless `--force` is given.
192
192
 
193
193
  ## Run slices
@@ -1,4 +1,40 @@
1
1
  # bindery-cli Patch Notes
2
+ ## v0.34.0 (2026-09-10)
3
+
4
+ ### Brandon's three calls: partial books are trouble, the contract reconciled, the daemon decision informed
5
+
6
+ - **Partial books now fail every layer (exit-code decision, option A).**
7
+ A `partial` book (repaired, improved, but still unable to open) used to
8
+ exit 0 from `library` and `run phase3` while `run phase1` counted it
9
+ as trouble. All three layers now exit 2 on it: the run succeeded, but
10
+ a book still needs a human. README and spec updated to match, and a
11
+ regression test pins a 3f-to-1f book exiting 2 with `applied: 0`.
12
+ - **spec.md reconciled with the shipped opt-ins** (applied on Brandon's
13
+ go, from the draft filed in v0.33.0's roadmap box). The non-goals no
14
+ longer prohibit what ships: "fixing RSC-005 in bulk" now names the
15
+ scoped, version-gated repairs as the shipped exception, and "repairing
16
+ genuinely mangled structure" now describes `--reserialize`'s actual
17
+ bounds (html root required, non-HTML XML sidecars refused). The
18
+ missing `--fix-ids` OPF section is written (manifest item ids, spine
19
+ idref/toc, fallback, media-overlay, EPUB 2 cover meta, both quote
20
+ styles), the garbled NCX sentence is rewritten, and "the OPF is left
21
+ untouched" now reads "by the default pass", naming the five opt-ins
22
+ that edit it on request.
23
+ - **The daemon decision, informed:** Brandon asked whether upgrading
24
+ java to 27 fixes the epubcheck daemon. Investigated and answered: his
25
+ javac is Fedora's `java-latest-openjdk-devel` 27-ea while `java` is
26
+ `java-25-openjdk` (both official Fedora packages, no COPR involved),
27
+ but the version skew is not the real blocker. epubcheck's own JSON
28
+ output is deduplicated (it reports 4 errors for a book whose CLI
29
+ summary says 243), the daemon counts everything, and bindery's gate
30
+ has always measured with JSON-style counts, so a working daemon would
31
+ change what the gate measures. The skew itself is fixable in code
32
+ (Java's single-file source launcher needs no javac at all), but the
33
+ daemon should stay off until its counting matches the JSON oracle; it
34
+ already self-disables safely, and `--workers` parallelism works via
35
+ subprocesses. Retire-or-reconcile remains the open decision; recorded
36
+ in the roadmap's `:958` box.
37
+
2
38
  ## v0.33.0 (2026-09-10)
3
39
 
4
40
  ### Phase 14 hardening: the apply path, the audit, and the safety contract's tests
@@ -24,14 +60,14 @@ regression test.
24
60
  bounded pool sized by `--workers` (default 1: the old behavior), a
25
61
  busy pool falls back to subprocesses instead of blocking, every
26
62
  roundtrip is bounded by the caller's timeout, and a daemon that dies
27
- or wedges is torn down and never retried — one failed attempt exhausts
28
- the pool so the sweep degrades to pure subprocess checks instead of
63
+ or wedges is torn down and never retried; one failed attempt exhausts
64
+ the pool, so the sweep degrades to pure subprocess checks instead of
29
65
  respawning a JVM per book. Measured: 8 books, 4 workers, two ~10s
30
66
  waves. The evidence for the recorded daemon-classpath decision grew:
31
67
  this machine's javac (27-ea) and java (25) cannot load each other's
32
68
  classes, so the daemon has never served a count here, and when forced
33
69
  to run its counts diverge from the CLI JSON oracle (243 vs 4 on one
34
- book) — enabling it would change gate outcomes.
70
+ book): enabling it would change gate outcomes.
35
71
  - **Smaller apply papercuts:** `--limit < 1` and a missing `--audit`
36
72
  file are usage errors, not tracebacks; the fresh-format install
37
73
  registers the catalogued file's own name instead of `repaired`; and
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "bindery-cli"
7
- version = "0.33.0"
7
+ version = "0.34.0"
8
8
  description = "Audit EPUB body text and repair broken EPUBs with deterministic, epubcheck-gated fixes; atomic in-place replacement in a Calibre library."
9
9
  readme = "README.md"
10
10
  classifiers = [
@@ -1068,7 +1068,7 @@ can be net-neutral and ships silently.*
1068
1068
  and JSON with exit 2; the resumable run journal is deliberately not
1069
1069
  built, since isolation plus `--json` already preserves the record
1070
1070
  and the journal is a second write surface next to the library.)*
1071
- - [ ] **Drive the exit code from partial books.** The documented contract
1071
+ - [x] **Drive the exit code from partial books.** The documented contract
1072
1072
  says exit 2 means "ran fine but some books are in trouble", but
1073
1073
  partial books land in `still_fatal` and return 0, in both `library`
1074
1074
  and phase3, while phase1 maps partial to problem/exit 2; the layers
@@ -1089,6 +1089,11 @@ can be net-neutral and ships silently.*
1089
1089
  cost: scripts that care must parse the report, and the contract's
1090
1090
  exit-2 sentence stays subtly incomplete.
1091
1091
  Either way the exit-code documentation lands with the decision.
1092
+ *(DECIDED 2026-09-10: option A, "unify on trouble". `run_library`
1093
+ now folds `partials` into its trouble exit, and phase3 propagates
1094
+ that rc, so all three layers exit 2 on a partial book. README and
1095
+ spec updated to match; regression test pins a 3f->1f book exiting
1096
+ 2 with applied=0. Shipped in v0.34.0.)*
1092
1097
  - [x] **Give backups overwrite protection and keep them out of the
1093
1098
  candidate set.** `make_backup` clobbers an existing backup, so a
1094
1099
  second `--apply` destroys the only copy of the author original
@@ -1137,7 +1142,21 @@ can be net-neutral and ships silently.*
1137
1142
  `--only ncx` encrypted-archive crash is also fixed here:
1138
1143
  `ncx_uid_mismatch` now catches the RuntimeError that `z.read`
1139
1144
  raises on zip-encrypted entries, so a DRM'd book is not a candidate
1140
- rather than a crash.)*
1145
+ rather than a crash.)
1146
+ *(DECISION NOTE 2026-09-10: Brandon asked whether upgrading his
1147
+ java to 27 would fix the daemon. Answered no: his javac is Fedora's
1148
+ java-latest-openjdk-devel 27-ea while java is java-25-openjdk (both
1149
+ official Fedora packages, no COPR involved), but the version skew
1150
+ is not the real blocker: epubcheck's own JSON output is
1151
+ deduplicated (it reports 4 errors for a book its CLI summary calls
1152
+ 243), the daemon counts everything, and bindery's gate has always
1153
+ measured with JSON-style counts. Upgrading java would make the
1154
+ daemon boot and start feeding different numbers into the gate. The
1155
+ skew itself is fixable in code (single-file source launcher, no
1156
+ javac at all), but the daemon should stay off until its counting
1157
+ matches the JSON oracle; retire-or-reconcile remains the open
1158
+ decision. The daemon already self-disables safely, and --workers
1159
+ parallelism works via subprocesses.)**
1141
1160
  - [x] **Smaller apply papercuts:** `--limit -1` and a missing
1142
1161
  `--audit` file raise tracebacks instead of usage errors
1143
1162
  (`cli.py:434`, `373`); the fresh-format branch of `install_format`
@@ -50,7 +50,11 @@ Applied to content documents (`.xhtml`, `.html`, `.htm`, `.xml`), in order:
50
50
  change what renders. Removed end tags are counted in the fix total.
51
51
 
52
52
  Applied to the NCX sidecar (`.ncx`): strip_prolog_junk, escape_bare_amp,
53
- fix_named_entities, plus **dtb:uid sync**, **fix_ncx_playorder** (sequentially rewrites duplicate playOrder attributes) to the OPF unique identifier (NCX-001).
53
+ fix_named_entities, plus **dtb:uid sync** (rewriting the NCX's `dtb:uid`
54
+ meta to match the OPF's unique identifier when they drift, NCX-001) and
55
+ **fix_ncx_playorder** (resequencing `playOrder` attributes on
56
+ `<navPoint>` start tags so they are strictly sequential; nav-label text
57
+ is never touched).
54
58
  With `--fix-ids`, ids in the NCX that are not valid XML names (digit-led, as when a
55
59
  converter stamps navPoint ids from UUIDs; colon-bearing) are renamed with the same
56
60
  `id_` scheme as OPF manifest ids. NCX ids are internal to the NCX (nothing in the
@@ -58,7 +62,10 @@ OPF or content documents references them), so the rename needs no cross-file
58
62
  bookkeeping.
59
63
 
60
64
  The OPF is located via `META-INF/container.xml` (falling back to the first `.opf`
61
- in the archive) and is left untouched, to keep Calibre's embedded metadata pristine.
65
+ in the archive). The default pass leaves it untouched, to keep Calibre's embedded
66
+ metadata pristine; the opt-in `--fix-ids`, `--strip-epub3-attrs`, `--fix-page-map`,
67
+ `--prune-missing-resources`, and `--encode-url-spaces` each edit it when requested,
68
+ and the human-facing `dc:` metadata is never altered by any of them.
62
69
 
63
70
  ### Opt-in: escape unknown entities (`--escape-unknown-entities`)
64
71
 
@@ -97,6 +104,12 @@ part of the default pipeline:
97
104
  external URL names a position in that other document and survives verbatim, and the
98
105
  NCX's `content src` fragments follow the rename (`fix_ncx_src_fragments`), so a ToC
99
106
  never dangles against the ids it references.
107
+ - **`--fix-ids`**: renames manifest `item` ids that are not valid XML names
108
+ (digit-led, colon-bearing) with the deterministic `id_` scheme, and updates
109
+ every reference to them: spine `idref` and `toc`, item `fallback` and
110
+ `media-overlay`, and the EPUB 2 cover meta, in both quote styles. NCX ids are
111
+ renamed by the same pass (`fix_ncx_ids`); colon-bearing fragments follow the
112
+ rename via `--fix-id-colons`' NCX half. Href paths and filenames are untouched.
100
113
  - **`--unwrap-block-in-inline`**: drop a `<span>` that illegally wraps a
101
114
  `<div>/<p>/<blockquote>`, keeping the block element and its text.
102
115
  - **`--strip-invalid-value`**: remove misplaced `value="..."` attributes from non-form
@@ -302,8 +315,8 @@ For a Calibre library (`Author/Title (id)/Title - Author.epub`):
302
315
  book was rejected, unreadable, or failed epubcheck, so scripts can detect trouble.
303
316
  Argparse-level misuse (unknown flag, malformed argument) exits 2 before the tool's
304
317
  own validation runs; the tool's own usage validations exit 1. A `partial` book
305
- (improved but still fatal) is reported for manual follow-up and does not fail the
306
- run; `run phase1` counts it as trouble.
318
+ (improved but still fatal) is reported for manual follow-up and counts as trouble:
319
+ `library`, `run phase1`, and `run phase3` all exit 2 on it (unified 2026-09-10).
307
320
  - With `--sweep`, `--workers N` runs the candidate-selection epubcheck pass through N
308
321
  concurrent workers (default 1: serial, unchanged). Books are checked in windows of N
309
322
  consumed in input order, so the candidate set and the before-measurements are identical
@@ -376,7 +389,15 @@ verb call). Books left partial or unreadable surface as `decisions_needed`.
376
389
 
377
390
  ## Out of scope (non-goals)
378
391
 
379
- - Fixing RSC-005 schema/content-model violations in bulk.
380
- - Repairing genuinely mangled structure (unclosed non-void elements, corrupted tag
381
- names, embedded VML/SVG). These are detected as `partial`/`nochange` and reported.
392
+ - Fixing RSC-005 schema/content-model violations *in bulk*: the scoped, opt-in
393
+ RSC-005 repairs (`--strip-epub3-attrs`, `--downgrade-epub3-tags`, both gated on
394
+ the package version) ship and stay, but an indiscriminate RSC-005 sweep remains
395
+ out of scope.
396
+ - Repairing genuinely mangled structure remains opt-in and bounded:
397
+ `--reserialize` (the lone html5lib dependency) re-parses malformed documents
398
+ carrying an `<html>` root and re-emits XHTML, refusing non-HTML XML sidecars;
399
+ corrupted tag names are covered by `--strip-bad-attrs` and
400
+ `--unwrap-illegal-tags`. Wholesale structural rewrites without a root to anchor
401
+ on stay out of scope, and anything the opt-ins cannot make deterministically
402
+ safe is detected as `partial`/`nochange` and reported.
382
403
  - Editing human-facing dc: metadata or creating content.
@@ -4,7 +4,7 @@ strip), epubcheck-gated.
4
4
  Single source of truth for the version. pyproject.toml must match VERSION below.
5
5
  """
6
6
 
7
- VERSION = "0.33.0"
7
+ VERSION = "0.34.0"
8
8
  __version__ = VERSION
9
9
 
10
10
  from .epub import RepairReport, repair_epub # noqa: E402
@@ -680,7 +680,12 @@ def run_library(args) -> int:
680
680
 
681
681
  # 2 lets scripts and cron distinguish "ran fine but some books are in trouble"
682
682
  # from a clean sweep (0) and a usage error (1).
683
- return 2 if (rejected + errors + unreadable) > 0 else 0
683
+ # Exit codes per the documented contract, unified on "trouble" (Brandon's
684
+ # option-A call, 2026-09-10): a partial book (improved but still unable
685
+ # to open) IS trouble -- the run itself succeeded, but a book still needs
686
+ # a human. phase1 already mapped partial to 2; library and phase3 (which
687
+ # propagates this rc) now agree with it.
688
+ return 2 if (rejected + errors + unreadable + partials) > 0 else 0
684
689
 
685
690
 
686
691
  def run_repair(args) -> int:
@@ -1309,3 +1309,46 @@ class TestSweepWorkers(unittest.TestCase):
1309
1309
  self.assertEqual(rc, 0)
1310
1310
  # 1 sweep call + 1 after-validation on the single candidate
1311
1311
  self.assertEqual(len(checked), 2)
1312
+
1313
+
1314
+ class TestPartialBooksAreTrouble(unittest.TestCase):
1315
+ """Brandon's option-A call (2026-09-10): a partial book (improved but
1316
+ still unable to open) is trouble -- library and phase3 exit 2 on it,
1317
+ agreeing with phase1's existing mapping."""
1318
+
1319
+ def test_library_exits_2_on_a_partial_book(self):
1320
+ with tempfile.TemporaryDirectory() as td:
1321
+ root = Path(td)
1322
+ _phase1_book(root / "a.epub", broken=True)
1323
+ jout = root / "lib.json"
1324
+ results = [
1325
+ CheckResult(3, 0, 0), # the sweep's before-measurement
1326
+ CheckResult(1, 0, 0), # after: fewer fatals, still fatal
1327
+ ]
1328
+ out, err = io.StringIO(), io.StringIO()
1329
+ with (
1330
+ mock.patch("bindery.cli.epubcheck_available", return_value=True),
1331
+ mock.patch("bindery.cli.run_epubcheck", side_effect=results),
1332
+ redirect_stdout(out),
1333
+ redirect_stderr(err),
1334
+ ):
1335
+ rc = cli.run_library(
1336
+ build_parser().parse_args(
1337
+ [
1338
+ "library",
1339
+ str(root),
1340
+ "--sweep",
1341
+ "--only",
1342
+ "all",
1343
+ "--all",
1344
+ "--apply",
1345
+ "--json",
1346
+ str(jout),
1347
+ ]
1348
+ )
1349
+ )
1350
+ data = json.loads(jout.read_text())
1351
+ self.assertEqual(rc, 2)
1352
+ self.assertEqual(data["summary"]["partial"], 1)
1353
+ # a partial book is never auto-applied
1354
+ self.assertEqual(data["summary"]["applied"], 0)
@@ -4,7 +4,7 @@ requires-python = ">=3.14"
4
4
 
5
5
  [[package]]
6
6
  name = "bindery-cli"
7
- version = "0.33.0"
7
+ version = "0.34.0"
8
8
  source = { editable = "." }
9
9
  dependencies = [
10
10
  { name = "cquarry" },
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes