Skip to content

Release v1.2.0#21

Merged
elfensky merged 16 commits into
mainfrom
develop
Jul 10, 2026
Merged

Release v1.2.0#21
elfensky merged 16 commits into
mainfrom
develop

Conversation

@elfensky

Copy link
Copy Markdown
Owner

Release v1.2.0 — the desloppify code-health roadmap (strict score 82.3 → 83.9) plus classify usability.

Highlights since v1.1.0

Safety

  • run_writer now refuses a change-set that would catastrophically empty a populated column (coarse last-line wipe guard covering every writer; --force overrides).
  • The data-loss guard actually fires now — a book that would lose its last fandom/character aborts the run (it was structurally unreachable before).
  • calibre_open() fails closed when Calibre is undetectable; backups hardened; run_writer subprocess has a timeout.

classify

  • New scourgify classify --bakeoff — compare sample books across every usable engine, then exit.
  • The controlled vocab is seeded from the AO3 high-frequency freeforms (build_classify_seed.pydefaults/classify_vocab_ao3.txt), so it isn't under-tagging out of the box; mistral engine documented.
  • backfill preview shows titles, not raw ids.

Engine

  • char/trope fold maps are now case/spacing-insensitive.
  • The --step reject→overrides subsystem is split into its own overrides.py (wrangle.py 817→618 LOC).

Quality

  • Type annotations across the public surface (blind type_safety re-review 62→82).
  • New tests: test_cli, test_wizard; wired into CI. Reviewed bandit config.
  • README documents the AO3 taxonomy vintage (frozen at the 2021-02-26 dump) + an add-your-fandom recipe.

Merge commit (not squash) per the release flow; auto-publishes to TestPyPI on landing.

🤖 Generated with Claude Code

elfensky and others added 16 commits July 9, 2026 19:14
… + add rollback

calibre_open() returned False (opening the write gate) on any host without pgrep,
silently disabling the "refuse while Calibre is open" guard. It now tries pgrep,
falls back to ps, and fails CLOSED if neither exists; GUI matching moved to
_is_calibre_gui() and tightened to also exclude calibredb/calibre-server/parallel.

Backups move from /tmp to data/backups/ (survive reboot, gitignored, pruned to the
newest 20). _backup_path() adds an _N suffix so two writes in the same second can no
longer overwrite one snapshot — the exact scenario a guided wizard run creates — and
run_writer verifies the copy size before writing. New `scourgify rollback [--list]`
restores the newest (or a chosen) snapshot with Calibre-closed and valid-Calibre-DB
checks, snapshotting the current db first so the rollback is itself reversible.

tests/test_backup.py pins the GUI matcher, collision-proof paths, and pruning (added
to CI). README/CLAUDE.md updated for the new backup location, rollback command, and
fail-closed detection.

Closes #4, closes #5.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…type hints)

Resolves the actionable findings from the desloppify holistic review:
- contract/high_level/convention: refresh stale docstrings in common.py and
  wizard.py (backup path /tmp->data/backups/; landing-menu; no-main() note)
- authorization: name the tag mass-deletion thresholds (TAG_SHRINK_FRACTION/FLOOR)
- error_consistency: unify engine-error truncation behind ERR_TRUNC; document the
  select.pick ValueError as an internal programmer-error guard
- test_strategy: gate test_promote.py + test_layers.py in CI; add tests for the
  pure tag_loss_guard and the extracted data_loss_guard safety rails
- type_safety: annotate the highest-fan-in seams (norm, ascii_fold,
  read_custom_column, load_config, transform, select.pick, parse_resp)
- mid_level_elegance: compute _engines() once per wizard menu, reuse hints/usable

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A wedged calibre-debug could hang a scripted/CI apply forever. Cap the write at
a generous 1h (a real batch write finishes in seconds/minutes), raise a clear
SystemExit on TimeoutExpired (backup is already taken), and move the temp-file
cleanup into finally so it runs on the timeout path too.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
cli.py's routing table was the one untested piece of real (non-interactive)
logic. Pin every branch — version flag, classify/staleness/promote routing,
overrides/rollback cmd handlers, bare/unknown fall-through to wrangle, and the
sys.argv reframing the subcommands' argparse relies on. Stub each tool main()
with a recorder so it needs no Calibre/library/network. Gate it in CI.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…png)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The advertised "no book loses its last fandom/character" abort could never
fire: transform() gated it on `had_real_F and not nF`, but any fandom that
made had_real_F true was the same one added to nF, so the condition was a
contradiction (always False). Meanwhile a book genuinely CAN end with an empty
#fandoms via a bad fandoms.csv alias->"" or an empty decompose payload — and
the SAFETY line reassuringly printed "losing last fandom: 0".

Fix: flag a loss when a book had a fandom and ends with none, tracking blocklist
relocations (value preserved in tags) so those don't count. Give data_loss_guard
a --force escape like tag_loss_guard, with a message that points at the likely
override. audit's dry-run SAFETY line now shows real counts; apply aborts unless
--force. Tests pin that the guard now fires on alias->"" and empty-decompose,
stays quiet on relocation/normal routing, and that --force overrides.

Fixes #10

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Ran bandit 1.9 over src/scourgify: 19 findings, all verified safe for this
tool's threat model (single-user local CLI, no network/web input surface).
Documented the triage in a [tool.bandit] skips block — subprocess argv are
fixed lists to trusted Calibre tools (never shell=True), urlopen hits hardcoded
https API literals, SQL uses ? placeholders / schema-derived integer ids, and
the calibre_open probe intentionally fails closed. `bandit -c pyproject.toml -r
src/scourgify` is now clean, so future scans surface only new issues.

Addresses #14

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The tag-mass-loss / last-fandom guards lived only in wrangle.apply_changes, so
classify --apply / promote --backfill / staleness / setup inherited the auto
backup but not the safety checks — "every write is guarded" was per-caller
discipline, not structural.

run_writer now predicts each set_field's effect (pure _predict_populated) and
refuses, BEFORE writing, a change-set that would empty a populated column
(_is_wipe: >90% of a >=100-book column). Coarse on purpose — no legitimate write
approaches it, so it never false-positives on wrangle's own 25%-capped drops; it
exists to catch a future writer silently regressing. --force overrides, and
wrangle threads its force through so a deliberate forced deletion isn't
double-blocked. Add-only writers (classify/promote/staleness) get it for free.

Pure helpers are unit-tested (no DB needed); docs updated.

Closes #6

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The char/trope fold maps were raw-keyed, but is_junk, known_chars, and
redundancy-strip all match on norm(). A custom-map entry that differed from
the book's value only in casing or punctuation folded silently — the user had
to guess the exact spelling. load_maps now adds norm-keyed aliases (raw keys
still win) and every char/trope lookup goes through _lookup (exact, then norm).

Refs #9.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…to overrides.py

Pure move, no logic changes. wrangle.py owned two concerns — the core
normalize/audit/apply engine and the whole reject→overrides subsystem behind
`apply --step` / `scourgify overrides`. The seam was already clean (cli.py
dispatches `overrides` separately; _step_walk takes the classified edits as a
param), so the cluster (_edit_label, reconstruct, synth_reject, _reject_row,
_step_walk, _append_override, build_overrides, _archive_consumed_rejects,
overrides_cmd) moves to a sibling module.

The wrangle<->overrides cycle is broken by a lazy import inside apply_changes;
overrides imports read_csv/read_lines/transform from wrangle at module level.
cli/wizard/tests repointed. wrangle.py 817 → 618 LOC. Full suite green.

Closes #11.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Extend the type hints from the five high-fan-in seams to the rest of the
public functions in common, select, staleness, promote, classify — plus the
two largest engine modules (wrangle, overrides) so type_safety improves
holistically, not just in the modules the issue named. Plain honest hints
(dict / list / tuple / X | None / argparse.Namespace), no typing gymnastics;
this also clears the 'loose type annotation' smells. No behavior change; full
suite green.

Refs #12.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…tion helpers

wizard.py and ui.py were the last untested production files. Test only the pure
helpers where a regression can actually ship: _task_hint (the pending-work menu
markers), _engines (cloud-engine-usable-iff-key-in-env + hints), and the
proposal pending/to_stamp split — extracted from snapshot() into a pure
_proposal_counts() so it's testable without a live library. The rich-interactive
shells (menus, prompts, live dashboard) are left untested by design.

Closes #13.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…fill + docs

Three verified polish items from the #9 batch:
- --bakeoff: the same sample-books-across-engines comparison the wizard offers is
  now available as `scourgify classify --bakeoff` (display-only, writes nothing).
  Engine metadata (ENGINE_ENV + usable_engines) now lives in classify as the single
  source of truth; wizard.ENGINE_KEYS aliases it so the two can never disagree.
- backfill preview now shows each book's title, not just its raw id.
- README engine list includes mistral (it was already in PRICING/ENGINE_ENV/classify).

Closes #9.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… freeforms

The curated vocab was ~120 terms while wrangle ships the full ~93k-fold AO3
taxonomy, so the classifier under-tagged out of the box (issue #8). The highest-use
AO3 freeforms ARE the generic reusable tropes (Fluff, Angst, Slow Burn, Hurt/Comfort…)
— a use-count threshold cleanly separates them from fandom-specific noise, which never
accumulates many uses.

build_classify_seed.py (maintainer tool) reads data/ao3_vocab.csv (name,uses; from the
OTW dump via build_ao3_layer.py), gates at >=20k uses, drops fandom-specific '(...)' and
format/meta tags, and writes the regenerable defaults/classify_vocab_ao3.txt (~120 terms).
load_vocab() now merges curated core ∪ AO3 seed (dedup-on-merge, curated spelling wins),
still fully overridable. Also removes a pre-existing 'Found Family' dup the merge-dedup
test surfaced.

Closes #8.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ecipe

The bundled defaults/ao3/ layer is generated from the only public OTW dump
(2021-02-26), so franchises that blew up after it (Baldur's Gate 3, Elden Ring,
Honkai: Star Rail) aren't in the master taxonomy and won't auto-fold on a fresh
install. The full fix is a regen against a newer OTW dump — external, none released
yet — so this documents the staleness prominently and gives a concrete overrides/
recipe (fandoms.csv + characters.csv) plus the promote-loop alternative.

Addresses the user-facing half of #7; full AO3 regen stays blocked on a newer source dump.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@elfensky
elfensky merged commit 36bbc9c into main Jul 10, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant