Repository navigation
Releases: VirInvictus/cquarry
Release list
## v1.28.1 (2026-10-23)
v1.28.1 (2026-10-23)
The Calibre 9.17 watch audit: NBSP preservation and an atomic extra-file write
The Friday parity-watch automation's second schema-search trip (v9.17.0,
tagged 2026-10-23) turned out to be upstream fixing a regression in the
exact adapter cquarry mirrored in 1.26.2. The full read-only audit against
this tree found two fixes to make and verified the rest of the release is
either out of cquarry's scope or already safe by construction; the per-file
verdicts live in the roadmap's Calibre 9.17 watch audit section.
- The whitespace collapse now preserves the three non-breaking spaces
(NO-BREAK SPACE, FIGURE SPACE, NARROW NO-BREAK SPACE), exactly like
upstream 9.17'snormalize_whitespace. 9.16's plain' '.join(x.split())
-- mirrored by 1.26.2 -- silently turned them into regular spaces because
Python'sstr.split()treats them as whitespace, and upstream reverted
that as a regression in 9.17 after users hit it in text and comments
columns. All four_write_pattern_abranches and
add_custom_column_valuesnormalize through the same helper; ends still
strip, breaking runs still collapse, and a trailing NBSP still strips away
with the ends (upstream's composite behavior). add_data_filewrites through a temp file + atomic replace (upstream
9.17'sadd_extra_filefix): opening the destination directly truncated
an existing file whenever the source read or the write failed part way.
A failed copy now leaves the old file intact and no temp orphan behind.- Audited, no action:
restore_original_formatcannot destroy the
current format on failure (cquarry reads the ORIGINAL bytes into memory
first and places them atomically after the rows commit -- upstream's
empty-placeholder bug never existed here); the trash move is a
same-filesystem atomic rename (.caltrashlives inside the library), so
the partial-copy data-loss class upstream fixed does not apply;
set_format's file swap stays the caller's atomic-replace job by
contract; the remaining 9.17 work is notes-sidecar, in-memory cache, and
GUI-facing robustness that cquarry does not mirror. - 707 tests pass (four new: NBSP preservation for single-valued text, the
enum adapt-then-validate order with NBSP spellings, NBSP preservation in
multi-valued items and appends, and the failed-copy intact-destination
pin).
## v1.28.0 (2026-10-11)
v1.28.0 (2026-10-11)
The entity case-change wave (issue #6)
Third live occurrence of the same trap in the library (ABRAMS/Abrams on
2026-10-04, then Daw Books/DAW Books and ROC/Roc on 2026-10-11, the ROC
row carrying 40 accumulated books): a case-variant target spelling
resolved NOCASE to the existing row and the setter reported already-so,
silently no-oping the consolidation. Issue #6 offered two shapes; the fix
takes the parity one, because the spec's documented rationale turned out
to rest on a misreading of upstream.
- The spec premise was wrong, and said so plainly. §5 deviation 12
(2026-09-12) claimed upstream's write default reuses a case-different
entity row as-is (allow_case_change=False), citingget_db_id. The
flag flows frommany_one/many_many, whose callers default it True
(Writer.set_booksdb/write.py:558,set_fieldcache.py:1970 -- the
path Calibre's GUI metadata edits ride); only the retired legacy
db.pyAPI wrapped False, andchange_casethen rewrites the row's
name and dirties every linked book. The rewritten deviation records
the correction in place, in the same class as the two upstream-factual
fixes the v1.25.0 audit landed. set_publisherconsolidates case variants (the issue's headline).
set_publisher(id, "DAW Books")on aDaw Booksrow now rewrites the
row's name to the requested spelling and touches + queues every book
linked to it, returning True.set_seriesis the same shape, indexes
untouched by an in-place re-case.- Authors get the full upstream treatment.
set_authors(and every
path that funnels through it:add_book, the trash restore) compares
and stores the display form and updates each linked book's
author_sortonly when it matched the string recomputed from the
stored sorts modulo case -- upstream's strcmp rule, so a hand-tuned
sort on an unrelated book survives a spelling fix made from another
book. The row's ownsortis deliberately NOT rewritten: upstream's
change_casewrites only the name column (its asort_map recompute is
session memory that reverts on reopen), so the stored sort -- and any
hand-tuning in it -- survives, withset_author_sort_nameremaining
the re-keying verb. Paths re-lay through the usual deferred machinery. - The tag surface stops fragmenting.
add_tagused to resolve the
exact spelling only and INSERT a duplicate row for a case variant --
the only cquarry path that could create case-duplicate rows. It now
resolves NOCASE and re-cases the shared row (every linked book
touched), andremove_taggained the NOCASE fallback so removing
rocreaches aROCrow instead of honestly failing against the
case. - Custom-column text family included, exclusions named. Pattern-A
text/series/multi-text value rows re-case like the builtin entities
(upstream writes them through the samemany_one/many_many), and
multi-valued items dedupe case-insensitively keeping the first
spelling (upstream'suniq(vals, kmap)), so["ya", "YA"]stores one
row. Deliberately excluded: custom enumerations (upstream's enum
writer hardcodes case changes off), languages (upstream keys them
exactly; odd-case rows reuse as-is), ratings (numeric), and the
rename/removal verbs, whose exact-spelling-first resolution is the
ratified 1.19 contract --rename_entityremains the consolidation
tool, including for pre-existing case-duplicate rows cquarry
deliberately does not heal (fix_case_duplicatesstays Calibre's). - Eleven new regression tests in
TestCaseChangeParity(the DAW
scenario, series indexes, the author strcmp rule, tag
re-case/already-linked/removal, custom-column set/append/dedupe,
language reuse, batch rollback), plus the three tests that pinned the
old behavior re-pinned to the new contract.
## v1.27.0 (2026-10-11)
v1.27.0 (2026-10-11)
The author display-form parity wave (the three issue #5 follow-ups)
The 1.26.3 collision fix (issue #5, earlier today) made set_authors
reuse an existing piped author row; its review verified three adjacent
surfaces still resolving stored spellings only, each logged as a
deliberate non-goal. Brandon chose to ship all three as one wave the
same day. Together they complete the author display-form parity story:
upstream keys EVERY author lookup by the display form (AuthorsTable
unserializes stored pipes back to commas when it loads the id map,
tables.py -- an authors-only flip), and its authors write adapter flips
incoming pipes to commas before anything else.
- Lane 1, resolution (write surface).
set_author_sort_name,
rename_entity(old-name lookup AND merge detection),
set_link_map(kind="authors"), andremove_entity_everywherenow
resolve author names through_resolve_author_id: exact raw
spelling first, then that spelling NOCASE (lowest id, the ratified
1.19 contract untouched), then the piped display form NOCASE. A
comma name reaches a piped row instead of raising, and renaming to a
comma spelling whose piped form already exists MERGES into it, the
survivor keeping its stored name -- exactly upstream's replace path. - Lane 2, layout.
_first_author_name(the single input every
path builder shares) now shows the display form, so directories and
format stems readLeckie, Ann/...for piped-stored authors where
1.26 laid outLeckie_ Ann/(the pipe hit_ascii_filename's
illegal-character map). This is convergence, not a new divergence:
Calibre's own layout inputs are unserialized display forms.
Pre-1.27 underscore directories linger untouched until each book is
next written, then move -- the same shape as any layout correction;
no bulk migration, andfind_book_by_path/format_path_indexmatch
stored paths, so read-side matching is unaffected. - Lane 3, inputs.
set_authorsandadd_bookdisplay-normalize
incoming author names before any resolution, dedupe, or path
construction (pipes become commas, upstream's authors-only write
adapter), so a pipe-bearing input resolves and stores exactly like
its comma form instead of matching only its own spelling and
INSERTing a duplicate. The other name-taking verbs deliberately do
NOT normalize: their raw-first resolution is what lets a caller
target a piped row by its stored spelling when both spellings exist. - Nine new regression tests across the three lanes (resolution
reaching piped rows through every verb, the display-form merge,
layout assertions for directories and format stems, the
underscore-to-display re-lay, and the piped-input round trip: input
pipes -> normalized -> stored piped -> layout display). Suite 683 to
692; spec.md, CLAUDE.md, API.md, and the roadmap's issue #5 section
document the completed surface.
## v1.26.3 (2026-10-11)
v1.26.3 (2026-10-11)
set_authors resolves piped author rows instead of colliding with them (issue #5)
Live failure on 2026-10-11 (big-wave library phase 3, book 10083): the
author row existed as the pipe-import artifact John William Strutt| Baron Rayleigh and the caller called set_authors(10083, ['John William Strutt, Baron Rayleigh']). The lookup tried the raw comma
spelling only, missed, and the create path's INSERT
(name.replace(",", "|")) died on UNIQUE(authors.name) against the
piped row it was about to duplicate. The batch machinery rolled the
143-book pass back cleanly, so no data was hurt, but the crash fires
exactly when the comma form is the repair for an existing piped form,
which is the author-byline-repair case the setter exists for.
- Author resolution now tries both spellings.
set_authorsand
add_bookresolve incoming display names through
_author_row_for_name: the raw spelling first (sorename_entity's
verbatim comma rows keep winning), then the piped form the create
path would store, and only a double miss reaches the INSERT. This is
upstream's observable behavior: Calibre keys its author lookups by
the display form (AuthorsTable unserializes stored pipes back to
commas on load), so a comma-name lookup there already reuses piped
rows; cquarry's direct SQL needed both spellings tried explicitly. - Reuse semantics are unchanged: an existing row keeps its
hand-tunedsort, the stored spelling is never rewritten, a raw
comma match outranks a piped variant, and the orphaned piped
duplicate then falls to the setter's standard prune. To fix a reused
piped row's sort afterward, passset_author_sort_namethe STORED
piped spelling (the display comma name raises there; that verb still
resolves stored spellings only -- see the logged follow-ups). - The add_book dry-run plan stays truthful for the reuse case:
_author_sort_keysshares the same resolution, so a plan over a
comma name with an existing piped row reportsnew: Falseand the
row's stored sort instead of predicting a phantom new author. - Logged follow-ups (adjacent surfaces this fix deliberately does not
touch, each recorded in the roadmap's issue #5 section with
upstream citations): the on-disk layout for piped-named authors lays
out with the pipe mapped to an underscore (Leckie_ Ann/) where
upstream uses the display form -- pre-existing since the 1.26
creation shape, identical before and after this fix, and now on
record; the reverse resolution direction (a pipe-bearing input
against comma-spelled rows) still creates a second row where
upstream's write adapter would reuse; andset_author_sort_name/
rename_entity/set_link_mapstill resolve stored spellings only,
so display names raise where reuse now silently works. - No schema, read-path, or search changes; nothing else in the write
module resolved-and-inserted different spellings of the same name.
Six regression tests cover set_authors reuse, hand-tuned sorts,
NOCASE, comma-row precedence, add_book over a piped row, and the
dry-run plan.
## v1.26.2 (2026-10-09)
v1.26.2 (2026-10-09)
Mirroring Calibre 9.16: the first parity-watch audit lane
Calibre 9.16 shipped on 2026-10-09 and tripped the new parity-watch
automation's schema-search tier (seven files under src/calibre/db/
moved, the most since 9.12). The full read-only audit against this tree
found one real divergence and ratified everything else; the per-file
verdicts live in the roadmap's Calibre 9.16 watch audit section.
- Custom-column writes now collapse whitespace exactly like upstream's
text adapters: single-valued text, series, and enumeration values go
through thesingle_textmirror (' '.join(x.split())over the
stripped string; a whitespace-only value clears via its empty-to-None
rule), and each item of a multi-valued text write collapses like
upstream'smultiple_textadapter has always done -- in both write
verbs,set_custom_columnandadd_custom_column_values. Enumeration
values are adapted BEFORE validation, so a double-spaced spelling of a
listed value now normalizes and passes instead of raising. cquarry
used to strip ends only and store the interior verbatim, so rows
written here and rows written by the 9.16 GUI disagreed for values
carrying double spaces or line breaks. Comments columns are
unaffected: upstream deliberately moved them to the verbatim
long_textadapter, so their behavior is unchanged. - Schema v27 ratified. The
book_storagetable cquarry mirrored
from master in 1.25 shipped upstream in 9.16 with the same DDL and
write semantics; the audit confirmed the support gate (`user_version27
, fresh libraries born at 28, upgrades land there too) and cquarry's_require_table` ValueError on pre-v27 schemas. No code
change was needed; CLAUDE.md's blob-writer bullet now records the
shipped fact. - The roadmap records the audit: the 9.16 verdict per file (the
search fast path and the categories cache need no action; the
read-path reworks are internal mechanics), the stale gap row for
get_book_storage(shipped in af41e52) removed, and the
snapshot-retry hardening candidate updated to 9.16's 60-second
busy-time budget +can_use_sqlite_inprobe + /tmp fallback
reference shape. - Nothing is owed downstream: no caller in CalibreQuarry, bindery-cli,
Hermitage, or Carrel-calibre-web writes custom columns through
cquarry yet.
## v1.26.1 (2026-10-02)
v1.26.1 (2026-10-02)
The closing sweep: documentation truth after the audit wave
A verification sweep over the 1.26 wave's documentation found the prose
lagging the shipped code and the recorded facts. No behavior changes; the
only Python delta is one docstring. Every fix was re-verified against the
current tree and upstream's reference clone before landing.
- CLAUDE.md's blob-writer bullets teach the 1.26 storage shapes. The
bullets still described the 1.25 passthrough (str verbatim / raw
json.dumps / wrapped entry) and instructed "do not 'complete' that" --
which, after 1.26 shipped the conversion-options frame, read as a
mandate to revert a shipped fix. They now match spec §3.6 and API.md:
json.dumps on every plugin-data payload, upstream's deterministic
protocol-2 BINSTRING frame, the bare str->str map in the data column. - The author-sort bullet carries the 1.26 refinements (the honest
no-op on an equal stored sort andupdate_books=False's row-only
write), and the four consumer-wave spots in CLAUDE.md and API.md speak
in the recorded past: Carrel 0.6.43 and Hermitage 1.8.6 retired their
helper copies on 2026-10-02. set_plugin_data's docstring matches its body: every payload goes
throughjson.dumps(val, default=str)since 1.26; the docstring's
first half still claimed astrstores verbatim.- Spec §5 carries the search-audit divergences it promised. New
entries 14-17 record the enum-multi count-operator scope, the
relative-date vocabulary pair (negativeNdaysagoupstream, singular
1dayagohere),loc:="X"'s contains-vs-exact result-set difference,
and the search-behavior preferences -- each verified against upstream's
reference clone, each dependency- or GUI-bound. The roadmap audit block
had claimed "§5 carries them" while §5 carried none of them. - The audit's seventh search divergence is retracted. Unregistered
@name:does not sweep as anall:text search upstream:
get_user_category_matchesreturns the empty set for an unknown name,
exactly like cquarry (db/search.py:601-602, :719-721, :907-929 in the
reference clone). There is no divergence to record; CLAUDE.md's
match-nothing claim was right all along, and the roadmap now says so. - Erratum, tag v1.25.0 vs patchnotes.md. Commit c03d7dc backticked
the@Nametoken in the v1.25.0 entry after the tag was cut (GitHub
autolinked the bare token as a mention), so the tag message and this
file differ by that one formatting pair. Tags are immutable and stay
untouched; the delta is recorded here rather than rewritten.
## v1.26.0 (2026-10-02)
v1.26.0 (2026-10-02)
The parity audit's fix wave: every confirmed bug and divergence lands
The 2026-10-02 eight-agent parity audit (four reviewers, two bug hunters,
one compat-merge review, one claim verification -- 8/8 top claims confirmed)
found three shipped-code bugs, eleven confirmed divergences, and a gaps
list. This release fixes the bugs and the divergences users plausibly hit,
and records the rest in the roadmap's audit block.
set_book_storagestores upstream's bare data map (was the wrapped
{'timestamp', 'data'}entry, which Calibre's validating reader dropped
on every cquarry-written row), with upstream's guards reproduced
(str-only keys/values, the 1 MiB UTF-16 cap, newest-timestamp-wins) and
the missing read half shipping asget_book_storage.remove_bookcleansbooks_pages_linkandbook_storage-- the two
FK-cascade tables neitherbooks_delete_trgnor cquarry's
foreign-keys-off connection covered; the stranded pages row (its book
column is the PRIMARY KEY) brokemove_book_from_trashwith
IntegrityError on any library with page counts.get_categoriesfollows upstream'sis_category = normalizedrule
(direct-storage customs out -- their raw values made engine-unparseable
expressions), gains the identifiers category (one node per identifier
key), and escapes expression values for quotes/backslashes.- Search parity, claim-verified: bare-term sweeps match identifier
KEYS like upstream (the 1.18-1.25 spec sentence said otherwise and was
wrong about upstream);all:falseis upstream's sequential loop
(missing AT LEAST ONE swept field, not missing every);cover:is
numeric with exactly true/false special-cased (cover:1works,
cover:yesraises). - New author rows follow upstream's creation path: the name stores
with commas as the legacy pipe and the sort is
helpers.author_to_author_sort's flip under the default tweaks -- the
old verbatim defaults rested on a spec sentence that was factually
wrong about upstream. - Writer fidelity:
remove_formatqueues the pages rescan;
set_plugin_datajson.dumps's every payload (the str-verbatim
shortcut stored strings upstream's reader cannot decode);
set_conversion_optionswraps payloads in upstream's deterministic
protocol-2 frame (fresh rows were unreadable by Calibre's unpickling
reader);set_custom_series_indexwrites the link row's extra float
and fresh series assignments seed it at 1.0;
set_author_sort_namegainsupdate_books=Falseand its long-promised
honest no-op;copy_book_from_librarycarries a 0.0 series index
verbatim and its author-sort postprocess no longer rewrites existing
destination books. - Recorded, not adopted (roadmap audit block): the enum count-operator
gap, negativeNdaysago,loc:="X"'s contains-vs-exact degradation,
translated date vocabulary, the search-behavior prefs, upstream's
localized language node names, ICU sort keys, and the popularity/
first-letter sort parameters -- each dependency-bound or GUI-state-
bound, now named in spec §5 or the roadmap. - Housekeeping: the compat branch carries the merge-review erratum
(restored its own 1.18.0+py313.1 entry; suite count corrected to 653). - Suite: 672 tests (flat -- every fix reshaped existing pins rather than adding volume; the new pins replaced the wrong-shape ones one-for-one).
## v1.25.0 (2026-10-02)
v1.25.0 (2026-10-02)
The parity program's second release: Phases 16, 17, and 18 land
The 2026-09-29 reopen's remaining three phases ship in one release: the
read-surface wave with the four shared-helper promotions (Phase 16), the
write-side extras (Phase 17), and the restricted tag browser (Phase 18). The
Cross-Repo Rule consumers affected this round are Carrel-calibre-web and
Hermitage; the four promotions carry the standing 2026-10-01 waiver (the
helpers land here, the consumers' private copies retire in a future consumer
wave), and both consumer roadmaps carry the adoption rows.
- Phase 16 reads.
get_cover_bytes/get_cover_last_modified(the
web-frontend pair: raw bytes for serving, an aware-UTC mtime for
conditional GET, ridingget_cover_path's resolution);
virtual_libraries_for_booksanduser_categories_for_books(the
inverse maps: every book -> the wings /@Namemembers it holds, resolved
through the same engine paths as the search locations so the answers
agree withvl:/@Name:queries; wings that fail to evaluate are
skipped with a warning where upstream splices an error string into the
name tuples);format_hash/format_metadata(the SHA-256 file-changed
detector the FTS sidecar's hash columns compare against, plus the
on-disk{path, size, mtime}facts);books_by_year/books_by_month
(upstream's date bucketing generalized to any date field, builtin or
date-typed custom column, restriction-shaped; the 0101/0100 sentinels
bucket nowhere, exactly as the search engine treats them);
get_next_series_num_for(the preference-aware next number, builtin or
series-typed custom column, withcurrent_indices=True; one boundary
named rather than faked: upstream readsseries_index_auto_increment
from its tweaks files, which metadata.db never carries, so the setting
comes from the library's preferences table with upstream's shipped
default "next");get_annotations_filteredplus
get_annotation_users/get_annotation_types/get_annotation_styles
(the annotation conveniences: the decoded view plus user/kind/style
filters, a limit, and Calibre's removed-skeleton tombstones hidden by
default -- upstream'signore_removedinverted to the renderer-facing
default; styles cover only what the library holds, since the viewer's
builtin catalog is GUI constants, not data);read_backup(Calibre's
stored sidecar OPF as bytes, for diffing its last write against the
rows; reads what Calibre wrote, the OPF-generation decline untouched);
and the XS tail:get_size_stats(notes always 0 per the recorded
decline),is_fts_enabled(the preference; the in-process pool state
is not database-visible),get_all_link_maps_for_book(the four
builtin fields plus link-carrying custom columns), and
get_last_read_positionsfmt/user/order_by/limit filters. The tail
rode one real fix:load_custom_columnbuilt every normalized column's
link map but stashed it series-only, socustom_column_linksanswered
empty for text/enumeration/rating despite documenting all four. - Phase 16 promotions (the 2026-10-01 waiver applies to all four):
ordered_virtual_library_names(Calibre's sidebar order: stored tab
position first, unknown names alphabetical, unparseable positions
ranking with the unknowns),helpers.unpipe_author(the
pipe-flattening display helper, render-identical with the consumer
copies),CalibreDB.tag_rollup_ids(the id-set sibling of the
counts-only rollup, the engine's anchored-subtree rule), and
helpers.IDENTIFIER_LINKS+identifier_link()(Hermitage's canonical
identifier-to-link table; ISBN resolves to openlibrary.org per
Brandon's 2026-09-29 call, Carrel's WorldCat mapping switches in its
consumer wave). - Phase 17 writes.
set_author_sort_name(the row-level author sort
writer; book-levelauthor_sortrecomputed " & "-joined in link
order) andset_link_map(the generic per-value link writer over
authors/series/publishers/tags plus normalized custom columns by
#label; one named deviation: unknown values raise where upstream
skips silently, because a link aimed at a misspelling should fail
loudly);set_pages(the page-count row writer clearingneeds_scan:
a real value is what the pending rescan waited for); the data/
directory verbs (list_data_files/get_data_filereads,
add_data_file/rename_data_file/remove_data_fileswrites -- pure
filesystem like the trash verbs, one shared traversal guard, upstream's
merge-conflict auto-rename layout);copy_book_from_library(the
blessed cross-library copy composed from sanctioned reads and writes,
onebatch()on the destination, format files throughadd_book's
seed path, per-author sorts then the book-levelauthor_sortverbatim;
annotations, data/ extras, custom columns, and the uuid each stay out
for a recorded reason, and duplicate policy stays with the frontend);
and the blob passthrough writersset_plugin_data/
set_conversion_options/set_book_storage(opaque payloads, none of
them OPF-queued, matching upstream; upstream's in-process recipe
pickling is deliberately the caller's boundary). The copy primitive
carries conversion options; plugin data it does not, exactly like
upstream's copy. - Phase 18:
get_categories(book_ids=None), the restricted tag
browser. Nodes over the builtin browse fields plus storage-backed
custom columns carry{id, name, sort, count, avg_rating, id_set, search_expression}, and searching a node's expression returns that
node's id set (swept per-node in the tests). Restriction narrows to
values the restricted books actually hold; counts and entity-view
average ratings agree withget_tag_browser_countswhere they
overlap. Named in the ship notes: the views' ratingsavg_rating
column is an upstream cross-join artifact (every row identical,
verified against the live library), rating nodes surface stars per the
cquarry convention rather than the views' internal 0-10 text, and
composite columns stay gated by the section 7 GPM boundary.
User categories,search, andnewsare upstream GUI synthesizations
outside the portable subset. Verified against the live library: 12
categories over ~8k books with per-node roundtrips clean. - Housekeeping: the pre-tag slop pass returned zero kill findings and
three fix-grade notes, all applied (a repeated guarantee clause,
a recurring triple negation, and a stale__version__example). - Suite: 556 -> 653 tests (pytest and CI's unittest discover agree).
## v1.24.0 (2026-10-02)
v1.24.0 (2026-10-02)
The parity program's first release: Phases 14 and 15 land
The 2026-09-29 reopen (roadmap.md) committed five parity phases; this release ships the first two, the completion wave and the maintenance ring, plus the Dependabot bookkeeping. The Cross-Repo Rule consumers affected this round are CalibreQuarry (adopts get_dirtied_formats() in its Phase 20 lane) and Carrel-calibre-web (the residue trio; adoption waived this round per the decision record).
- Restore-from-trash verbs (
cquarry.write): the trash stops being a dead end.copy_format_from_trash/copy_book_from_trashrescue entries out of.caltrash(pure filesystem),move_format_from_trashundeletes one format into its live book,delete_trash_entryremoves one entry, andmove_book_from_trashrebuilds the row from the entry's sidecarmetadata.opf(Calibre's own backup, read with stdlib ElementTree and never generated, keeping the OPF-generation decline intact): core metadata restored, every extension-bearing file re-registered indata, the cover flag from the file, OPF resync and FTS extraction queued, and the directory moved back only after the rows commit. Custom columns, annotations, and plugin data are not restored (the OPF carries none of them). get_dirtied_formats()(read side): the FTS sidecar's extraction queue as sorted(book_id, format)pairs, the formats sibling ofget_dirtied_books(). Retires CalibreQuarry's raw sidecar read when its lane adopts it.set_custom_column_metadata(write side): name/editable/display edits on an existing column, upstreambackend.py:1407's shape. The headline use:display={"enum_values": [...]}populates an enumeration without Calibre open. Honest no-op on equal values; a change setsupdate_all_last_mod_dates_on_startlike upstream's wrapper; datatype and label changes are refused by omission (type is baked into the storage layout; the label is the#labelsearch token).- The Carrel residue trio (read side):
get_book_by_uuid(the Calibre-Companion lookup; case-insensitive, None on miss),get_entity_book_ids(kind, name)(the id-set half ofget_entities; tags resolve the engine's anchored-subtree rule so a browse node's set covers its children), andget_all_formats()(a cached{book_id: [FMT]}map). facet_counts/facet_counts_for_ids(read side): per-value counts over a restricted result set (the builtin browse fields plus custom columns by#label; ratings surfaced as stars exactly asfield()yields them). The_for_idshalf is the restriction plumbing Phase 18's restricted tag browser reuses.WritableCalibreDB.maintain(): vacuum, analyze, and integrity_check over the library DB and the attached FTS sidecar in one standalone pass (upstreambackend.pyvacuumextended with the two statements calibredb users run by hand; the notes DB stays out of scope). Refusesbatch()and mid-transaction states; reports the check rows (["ok"]on a healthy file).check_library_disk()(cquarry.integrity): the extra-side disk checks of upstream'scheck_libraryin one tree walk: extra titles/authors, malformed paths and formats, extra formats, unknown files, extra covers, failed folders. The missing side is deliberately not re-answered (find_missing_format_files/find_missing_cover_filesown it). The format-vs-unknown-file classifier is structural (a token extension minus the image/OPF/junk set), not upstream's curatedBOOK_EXTENSIONS; POSIX case sensitivity assumed.- FTS queue verbs (
cquarry.write):fts_reindex_book,fts_reindex_all, andfts_queue_clearare the queue half of upstream'sfts_unindexfamily as pure sidecar SQL. One boundary named rather than faked: upstream's index-row deletion runsbooks_textdelete triggers that tokenize through Calibre's custom FTS5 tokenizer, which does not exist outside Calibre, so removing indexed text stays Calibre's job (an O-lanecalibredb fts_unindexwrap if a consumer asks). - Typed preference writes (
cquarry.write):set_preference(key, value)over a five-key whitelist (saved_searches,virtual_libraries,user_categories,grouped_search_terms,fts_enabled), each payload validated by key before anything lands and stored as one JSON row in Calibre's shape;saved_search_add/saved_search_delete/saved_search_renameare the calibredbsaved_searchesparity item (rename resolves case-insensitively to the stored spelling and refuses to overwrite an existing name where upstream silently overwrites). - Housekeeping: the Dependabot gate ticked (both major-bump PRs merged 2026-09-30), the import skills swept per the skill-sync rule, and an API.md table row the
maintain()insert had split was repaired in the same window. - Suite: 500 -> 556 tests (pytest; 550 collected by CI's unittest discover).
## v1.23.2 (2026-09-16)
v1.23.2 (2026-09-16)
The version guard runs again; every carrier agrees
The v1.23.1 hotfix shipped with four stale version carriers (src/cquarry/__init__.py, src/cquarry/config.py, spec.md, API.md all still reading 1.23.0), so the published wheel reported cquarry.__version__ == "1.23.0" and every consumer reading the constant saw the wrong release. The guard that should have caught it, tests/test_version_sync.py, was the one file in the suite written as plain pytest-style functions -- and CI runs unittest discover, which silently collected zero tests from it. The file is now a unittest.TestCase (469 -> 471 collected tests), every carrier agrees at 1.23.2, and the discover run that gates each push executes the guard.
Found by the post-blitz verification day's deep pass on the foundation repos.