bindery-cli 0.48.2__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.48.2/CLAUDE.md → bindery_cli-0.50.0/AGENTS.md +14 -5
- {bindery_cli-0.48.2 → bindery_cli-0.50.0}/PKG-INFO +17 -6
- {bindery_cli-0.48.2 → bindery_cli-0.50.0}/README.md +16 -5
- {bindery_cli-0.48.2 → bindery_cli-0.50.0}/patchnotes.md +96 -0
- {bindery_cli-0.48.2 → bindery_cli-0.50.0}/pyproject.toml +1 -1
- {bindery_cli-0.48.2 → bindery_cli-0.50.0}/roadmap.md +2 -0
- {bindery_cli-0.48.2 → bindery_cli-0.50.0}/spec.md +34 -2
- {bindery_cli-0.48.2 → bindery_cli-0.50.0}/src/bindery/__init__.py +1 -1
- {bindery_cli-0.48.2 → bindery_cli-0.50.0}/src/bindery/audit.py +453 -37
- {bindery_cli-0.48.2 → bindery_cli-0.50.0}/src/bindery/cli.py +58 -30
- {bindery_cli-0.48.2 → bindery_cli-0.50.0}/tests/test_cli.py +73 -0
- bindery_cli-0.50.0/tests/test_contamination.py +396 -0
- {bindery_cli-0.48.2 → bindery_cli-0.50.0}/uv.lock +1 -1
- bindery_cli-0.48.2/AGENTS.md +0 -1
- {bindery_cli-0.48.2 → bindery_cli-0.50.0}/.github/FUNDING.yml +0 -0
- {bindery_cli-0.48.2 → bindery_cli-0.50.0}/.github/workflows/ci.yml +0 -0
- {bindery_cli-0.48.2 → bindery_cli-0.50.0}/.github/workflows/publish.yml +0 -0
- {bindery_cli-0.48.2 → bindery_cli-0.50.0}/.gitignore +0 -0
- {bindery_cli-0.48.2 → bindery_cli-0.50.0}/LICENSE +0 -0
- {bindery_cli-0.48.2 → bindery_cli-0.50.0}/docs/screenshots/library-sweep.png +0 -0
- {bindery_cli-0.48.2 → bindery_cli-0.50.0}/logo.svg +0 -0
- {bindery_cli-0.48.2 → bindery_cli-0.50.0}/plugin/__init__.py +0 -0
- {bindery_cli-0.48.2 → bindery_cli-0.50.0}/project.done +0 -0
- {bindery_cli-0.48.2 → bindery_cli-0.50.0}/run_tests.sh +0 -0
- {bindery_cli-0.48.2 → bindery_cli-0.50.0}/scripts/FastSweep.java +0 -0
- {bindery_cli-0.48.2 → bindery_cli-0.50.0}/scripts/build_plugin.py +0 -0
- {bindery_cli-0.48.2 → bindery_cli-0.50.0}/scripts/fast_sweep.py +0 -0
- {bindery_cli-0.48.2 → bindery_cli-0.50.0}/src/bindery/__main__.py +0 -0
- {bindery_cli-0.48.2 → bindery_cli-0.50.0}/src/bindery/epub.py +0 -0
- {bindery_cli-0.48.2 → bindery_cli-0.50.0}/src/bindery/library.py +0 -0
- {bindery_cli-0.48.2 → bindery_cli-0.50.0}/src/bindery/pagination.py +0 -0
- {bindery_cli-0.48.2 → bindery_cli-0.50.0}/src/bindery/reserialize.py +0 -0
- {bindery_cli-0.48.2 → bindery_cli-0.50.0}/src/bindery/transforms.py +0 -0
- {bindery_cli-0.48.2 → bindery_cli-0.50.0}/src/bindery/validate.py +0 -0
- {bindery_cli-0.48.2 → bindery_cli-0.50.0}/src/bindery/watermark.py +0 -0
- {bindery_cli-0.48.2 → bindery_cli-0.50.0}/tests/test_audit.py +0 -0
- {bindery_cli-0.48.2 → bindery_cli-0.50.0}/tests/test_comment_hyphens.py +0 -0
- {bindery_cli-0.48.2 → bindery_cli-0.50.0}/tests/test_cover_tocdrift.py +0 -0
- {bindery_cli-0.48.2 → bindery_cli-0.50.0}/tests/test_epub.py +0 -0
- {bindery_cli-0.48.2 → bindery_cli-0.50.0}/tests/test_flags_real_core.py +0 -0
- {bindery_cli-0.48.2 → bindery_cli-0.50.0}/tests/test_flags_wiring.py +0 -0
- {bindery_cli-0.48.2 → bindery_cli-0.50.0}/tests/test_help.py +0 -0
- {bindery_cli-0.48.2 → bindery_cli-0.50.0}/tests/test_library.py +0 -0
- {bindery_cli-0.48.2 → bindery_cli-0.50.0}/tests/test_matcher_hardening.py +0 -0
- {bindery_cli-0.48.2 → bindery_cli-0.50.0}/tests/test_pagination.py +0 -0
- {bindery_cli-0.48.2 → bindery_cli-0.50.0}/tests/test_plugin.py +0 -0
- {bindery_cli-0.48.2 → bindery_cli-0.50.0}/tests/test_reserialize.py +0 -0
- {bindery_cli-0.48.2 → bindery_cli-0.50.0}/tests/test_space_renames.py +0 -0
- {bindery_cli-0.48.2 → bindery_cli-0.50.0}/tests/test_transforms.py +0 -0
- {bindery_cli-0.48.2 → bindery_cli-0.50.0}/tests/test_validate.py +0 -0
- {bindery_cli-0.48.2 → bindery_cli-0.50.0}/tests/test_version.py +0 -0
- {bindery_cli-0.48.2 → bindery_cli-0.50.0}/tests/test_watermark.py +0 -0
- {bindery_cli-0.48.2 → bindery_cli-0.50.0}/tests/test_xml_hardening.py +0 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#
|
|
1
|
+
# AGENTS.md (bindery-cli)
|
|
2
2
|
|
|
3
3
|
Per-project guidance. Overrides the global file where they conflict.
|
|
4
4
|
|
|
@@ -114,19 +114,19 @@ 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.
|
|
121
121
|
- `src/bindery/cli.py`: `repair` and `library` subcommands, including `--all` and `--install-to-calibre`; `repair --json` emits the library per-book record vocabulary (2026-09-15; since v0.45.0 every per-book record, the phase1 payload included, also carries the structured fix breakdown: the `fixes` dict plus `ncx_uid_synced`/`watermark_refusals` off the `Outcome`, and `_phase1_decisions` reads that data instead of substring-matching the rendered `summary`, which stays for human eyes; CalibreQuarry's lossy-consent mirror is the waiting consumer, floor `bindery>=0.45.0`); 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; since 2026-09-15 the summary sums applied/equal states only and carries rejected projections as `rejected_projection`). Plus `bindery doctor` (2026-09-15): the environment self-check that imports none of the VirInvictus stack, never raises, and always exits 0. Both run verbs 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. Since v0.46.0 `cli._flags_from_args(args)` is the one place the argparse names map onto `RepairFlags` fields (each `or --all`), shared by `run_library` and `run_repair`; the old per-verb ~25-kwarg blocks are gone. Since v0.47.1 the help is two-level (the --help-repairs reference is
|
|
122
122
|
custom-rendered and colors itself with the CPython 3.14 argparse theme,
|
|
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 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). `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.
|
|
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
|
|
|
129
|
-
-
|
|
129
|
+
- Python style follows the global conventions (type hints, `from __future__ import annotations`, ruff for lint and format; `~/.zcode/AGENTS.md`).
|
|
130
130
|
- `./run_tests.sh` runs the suite in the project venv. Since the 3.12 floor, `uv run --python X` REBUILDS `.venv` for interpreter X, and the `python_version >= '3.14'` markers then gate vir-tui/cquarry OUT of it below 3.14 (the suite goes red with ModuleNotFoundError: cquarry). After interpreter experiments, rebuild with `uv run --python 3.14` before running the suite again.
|
|
131
131
|
- `VERSION` lives in `src/bindery/__init__.py`, mirrored in `pyproject.toml`. Bump both.
|
|
132
132
|
- Run tests with `./run_tests.sh`.
|
|
@@ -136,3 +136,12 @@ Born from the 2026 library audit (see the user memory `calibre-library-epubcheck
|
|
|
136
136
|
The library is real data. The loop is always: dry run on `/tmp` copies, inspect the
|
|
137
137
|
report, then apply with backups. epubcheck is the oracle; a repaired book that still has
|
|
138
138
|
fatals is `partial` and must be left for manual work, never auto-applied.
|
|
139
|
+
|
|
140
|
+
## Release flow
|
|
141
|
+
|
|
142
|
+
Commit and push verified work (the standing push grant in the global file; force-pushes
|
|
143
|
+
still need approval), wait for CI to go green, then tag: annotated `vX.Y.Z` at the release
|
|
144
|
+
commit, message = that release's patchnotes entry verbatim (`--cleanup=verbatim`; the full
|
|
145
|
+
tag procedure lives in `~/.zcode/AGENTS.md`). The tag drives `publish.yml`: the PyPI push
|
|
146
|
+
plus the Bindery Repair plugin zip attached to the GitHub release. Version carriers:
|
|
147
|
+
`src/bindery/__init__.py`'s VERSION and `pyproject.toml`; bump both.
|
|
@@ -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
|
|
@@ -290,11 +295,15 @@ next startup: no manual resync needed.
|
|
|
290
295
|
|
|
291
296
|
## Repairing a book
|
|
292
297
|
|
|
293
|
-
Repair one book to a new file (gated; writes only if it is an improvement)
|
|
298
|
+
Repair one book to a new file (gated; writes only if it is an improvement). Without an
|
|
299
|
+
output path the verb is a probe: the gate runs and the report says what would change,
|
|
300
|
+
and nothing is written anywhere, so probing a file can never litter the directory
|
|
301
|
+
under vetting:
|
|
294
302
|
|
|
295
303
|
```sh
|
|
296
|
-
bindery repair broken.epub #
|
|
297
|
-
bindery repair broken.epub
|
|
304
|
+
bindery repair broken.epub # probe: report only, nothing written
|
|
305
|
+
bindery repair broken.epub --all --json report.json # probe with a machine-readable record
|
|
306
|
+
bindery repair broken.epub fixed.epub # rescue form: write the repaired file
|
|
298
307
|
bindery repair scanned.epub --strip-pagination # also remove baked-in page numbers
|
|
299
308
|
bindery repair stubs.epub --strip-stub-docs # drop repeated placeholder chapters
|
|
300
309
|
bindery repair broken.epub fixed.epub --json report.json # machine-readable record
|
|
@@ -303,7 +312,9 @@ bindery repair broken.epub fixed.epub --json report.json # machine-readable rec
|
|
|
303
312
|
`repair --json FILE` writes one record in the `library --json` per-book
|
|
304
313
|
vocabulary (`status`, `applied`, `before`/`after` counts, the fix `summary`),
|
|
305
314
|
on every processing outcome including nochange and reject: a calling agent
|
|
306
|
-
gets the same facts the console lines carry.
|
|
315
|
+
gets the same facts the console lines carry. On a probe the record's
|
|
316
|
+
`output` is null and `applied` stays false; the counts and the fix
|
|
317
|
+
breakdown still describe the gate-accepted projection.
|
|
307
318
|
|
|
308
319
|
## Sweeping a library
|
|
309
320
|
|
|
@@ -338,7 +349,7 @@ bindery library ~/docs/Calibre\ Library --only all --apply --all --install-to-ca
|
|
|
338
349
|
- `--all` automatically turns on all opt-in non-fatal fixes and lossy strips (pagination, watermarks, bad attributes, unknown entities, image alt tags, etc.) in a single run.
|
|
339
350
|
- Only the `.epub` is replaced. `metadata.opf`, `cover.jpg`, and `metadata.db` are left for Calibre's Quality Check sync to reconcile.
|
|
340
351
|
- 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.
|
|
341
|
-
- Exit codes, per verb (for scripts and cron; `--help-json`'s `exit_contract` carries this machine-side): `repair` exits 0 when clean OR written (a PARTIAL repair writes its best output and exits 0; the `--json` record's status says so) and 1 on trouble (gate-rejected, epubcheck failure, unreadable input) or invocation problems (bad args, an existing output without `--force`)
|
|
352
|
+
- Exit codes, per verb (for scripts and cron; `--help-json`'s `exit_contract` carries this machine-side): `repair` exits 0 when clean, probed, OR written (a probe writes nothing and exits 0; a PARTIAL repair writes its best output and exits 0; the `--json` record's status says so) and 1 on trouble (gate-rejected, epubcheck failure, unreadable input) or invocation problems (bad args, an existing output without `--force`); it never exits 2. `library`, `run phase1`, and `run phase3` use the unified contract: 0 clean, 2 when any book was flagged, rejected, unreadable, errored, or partial, 1 for invocation problems. `audit` runs the inverse contract: 0 clean, 1 when any book was flagged (a flag is the audit's trouble), 2 for its own usage errors, 3 when findings coexist with a failed `--json`/`--tag` write. `audit --tag` and `library --apply --install-to-calibre` are lock-class doors: they are REFUSED with exit 1 while Calibre is running. Argparse-level misuse exits 2 everywhere before any validation runs.
|
|
342
353
|
- 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.
|
|
343
354
|
- `repair` refuses to overwrite an existing output file unless `--force` is given.
|
|
344
355
|
|
|
@@ -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
|
|
@@ -263,11 +268,15 @@ next startup: no manual resync needed.
|
|
|
263
268
|
|
|
264
269
|
## Repairing a book
|
|
265
270
|
|
|
266
|
-
Repair one book to a new file (gated; writes only if it is an improvement)
|
|
271
|
+
Repair one book to a new file (gated; writes only if it is an improvement). Without an
|
|
272
|
+
output path the verb is a probe: the gate runs and the report says what would change,
|
|
273
|
+
and nothing is written anywhere, so probing a file can never litter the directory
|
|
274
|
+
under vetting:
|
|
267
275
|
|
|
268
276
|
```sh
|
|
269
|
-
bindery repair broken.epub #
|
|
270
|
-
bindery repair broken.epub
|
|
277
|
+
bindery repair broken.epub # probe: report only, nothing written
|
|
278
|
+
bindery repair broken.epub --all --json report.json # probe with a machine-readable record
|
|
279
|
+
bindery repair broken.epub fixed.epub # rescue form: write the repaired file
|
|
271
280
|
bindery repair scanned.epub --strip-pagination # also remove baked-in page numbers
|
|
272
281
|
bindery repair stubs.epub --strip-stub-docs # drop repeated placeholder chapters
|
|
273
282
|
bindery repair broken.epub fixed.epub --json report.json # machine-readable record
|
|
@@ -276,7 +285,9 @@ bindery repair broken.epub fixed.epub --json report.json # machine-readable rec
|
|
|
276
285
|
`repair --json FILE` writes one record in the `library --json` per-book
|
|
277
286
|
vocabulary (`status`, `applied`, `before`/`after` counts, the fix `summary`),
|
|
278
287
|
on every processing outcome including nochange and reject: a calling agent
|
|
279
|
-
gets the same facts the console lines carry.
|
|
288
|
+
gets the same facts the console lines carry. On a probe the record's
|
|
289
|
+
`output` is null and `applied` stays false; the counts and the fix
|
|
290
|
+
breakdown still describe the gate-accepted projection.
|
|
280
291
|
|
|
281
292
|
## Sweeping a library
|
|
282
293
|
|
|
@@ -311,7 +322,7 @@ bindery library ~/docs/Calibre\ Library --only all --apply --all --install-to-ca
|
|
|
311
322
|
- `--all` automatically turns on all opt-in non-fatal fixes and lossy strips (pagination, watermarks, bad attributes, unknown entities, image alt tags, etc.) in a single run.
|
|
312
323
|
- Only the `.epub` is replaced. `metadata.opf`, `cover.jpg`, and `metadata.db` are left for Calibre's Quality Check sync to reconcile.
|
|
313
324
|
- 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.
|
|
314
|
-
- Exit codes, per verb (for scripts and cron; `--help-json`'s `exit_contract` carries this machine-side): `repair` exits 0 when clean OR written (a PARTIAL repair writes its best output and exits 0; the `--json` record's status says so) and 1 on trouble (gate-rejected, epubcheck failure, unreadable input) or invocation problems (bad args, an existing output without `--force`)
|
|
325
|
+
- Exit codes, per verb (for scripts and cron; `--help-json`'s `exit_contract` carries this machine-side): `repair` exits 0 when clean, probed, OR written (a probe writes nothing and exits 0; a PARTIAL repair writes its best output and exits 0; the `--json` record's status says so) and 1 on trouble (gate-rejected, epubcheck failure, unreadable input) or invocation problems (bad args, an existing output without `--force`); it never exits 2. `library`, `run phase1`, and `run phase3` use the unified contract: 0 clean, 2 when any book was flagged, rejected, unreadable, errored, or partial, 1 for invocation problems. `audit` runs the inverse contract: 0 clean, 1 when any book was flagged (a flag is the audit's trouble), 2 for its own usage errors, 3 when findings coexist with a failed `--json`/`--tag` write. `audit --tag` and `library --apply --install-to-calibre` are lock-class doors: they are REFUSED with exit 1 while Calibre is running. Argparse-level misuse exits 2 everywhere before any validation runs.
|
|
315
326
|
- 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.
|
|
316
327
|
- `repair` refuses to overwrite an existing output file unless `--force` is given.
|
|
317
328
|
|
|
@@ -1,5 +1,101 @@
|
|
|
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
|
+
|
|
62
|
+
## v0.49.0 (2026-10-13)
|
|
63
|
+
|
|
64
|
+
### The single-file repair probe writes nothing (issue #7)
|
|
65
|
+
|
|
66
|
+
Found live in a phase-1 vetting run: `bindery repair <file> --all --json
|
|
67
|
+
report.json`, the acquisition pipeline's documented single-candidate
|
|
68
|
+
probe, wrote its full repaired output as an 11MB `...(repaired).epub`
|
|
69
|
+
sibling inside the vetted tree, where anything beside the files gets
|
|
70
|
+
inventoried: import litter by the pipeline's own rule, carrying a name
|
|
71
|
+
only the `(repaired)` suffix apart from the original, so a later driver
|
|
72
|
+
pointed at the pre-probe filename list either missed it or
|
|
73
|
+
double-handled it.
|
|
74
|
+
|
|
75
|
+
- **No output path is now a probe**: the epubcheck gate runs, the
|
|
76
|
+
console lines and the `--json` record carry the full projection
|
|
77
|
+
(status, before/after counts, the structured fix breakdown), and
|
|
78
|
+
nothing is written anywhere; the repaired bytes stay in the scratch
|
|
79
|
+
workdir and vanish. On a probe the record's `output` is null and
|
|
80
|
+
`applied` stays false.
|
|
81
|
+
- **Writing needs an explicit destination** (`bindery repair book.epub
|
|
82
|
+
out.epub`), and that rescue form is byte-for-byte what it was: same
|
|
83
|
+
gate, same copy-the-accepted-bytes rule, same `--force` and
|
|
84
|
+
refuse-to-overwrite-the-input guards.
|
|
85
|
+
- The probe's console verdict is the record's own status vocabulary
|
|
86
|
+
(`accept:`/`equal:`/`unvalidated:`; partial keeps its established
|
|
87
|
+
PARTIAL phrasing), followed by an explicit "probe: nothing written"
|
|
88
|
+
line; a probe that would be partial still exits 0 with the record's
|
|
89
|
+
status carrying the verdict.
|
|
90
|
+
- The repair `--help` text (new probe example, the output positional's
|
|
91
|
+
no-write meaning) and `--help-json`'s exit contract tell the new
|
|
92
|
+
truth; README and spec.md document both forms. The exit contract
|
|
93
|
+
itself is unchanged: 0 clean/probed/written including partial, 1
|
|
94
|
+
trouble, never 2.
|
|
95
|
+
- Three pins added (643 total): the probe writes nothing on the accept
|
|
96
|
+
and partial paths, and the `equal` noop verdict is carried honestly
|
|
97
|
+
rather than dressed up as repairable.
|
|
98
|
+
|
|
3
99
|
## v0.48.2 (2026-10-06)
|
|
4
100
|
|
|
5
101
|
### The 12-agent docs audit: README/spec/CLAUDE tell the v0.48 truth
|
|
@@ -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 = [
|
|
@@ -42,6 +42,8 @@ The plugin vendors the core and never gates (the epubcheck latency ruling).
|
|
|
42
42
|
| 0.46 | RSC-016 `--fix-comment-double-hyphen` (the last open repair class) and the RepairFlags dataclass | v0.46.0 |
|
|
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
|
+
| 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 |
|
|
45
47
|
|
|
46
48
|
## Open work
|
|
47
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
|
|
@@ -559,6 +581,16 @@ half of the same information: consumers narrow lossy consent by fix class
|
|
|
559
581
|
as data, never by substring-matching the rendered summary (v0.45.0; the
|
|
560
582
|
phase1 payload inherits them through its per-book repair records).
|
|
561
583
|
|
|
584
|
+
DST is optional, and the two forms have different write contracts. With a
|
|
585
|
+
DST the verb is the rescue form: the gate-accepted bytes are written there
|
|
586
|
+
(the input is never overwritten; an existing DST needs `--force`). Without
|
|
587
|
+
a DST the verb is a probe: the gate runs and the record carries the
|
|
588
|
+
projection (`output` null, `applied` false), and nothing is written
|
|
589
|
+
anywhere, so probing a file in a vetted tree cannot litter it with a
|
|
590
|
+
`(repaired).epub` sibling (the v0.48.2-and-before default, retired in
|
|
591
|
+
v0.49.0; a probe that would be partial still exits 0 with `status`
|
|
592
|
+
carrying the verdict).
|
|
593
|
+
|
|
562
594
|
## The Calibre plugin (Bindery Repair)
|
|
563
595
|
|
|
564
596
|
Each release vendors the repair core into a Calibre plugin zip,
|
|
@@ -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
|