feat(scanner): force rescan = original scan; feat(filters): the manually-edited filter (faceted plan Phase 13)
Release / build-and-push (push) Successful in 3m7s

Force rescan contract change (owner-directed): the editorial-keep skip
is retired. Force rescan now re-derives EVERY field on EVERY book from
all sources — multi-file books included (the folder/zip flows ran the
full chain on create and on manifest-flagged edits, but skipped the
sidecar apply under force). metadata_overrides is the only protection:
ApplyMetadataOverrides re-applies manual values inside the writer, so
manual edits survive; everything else re-derives.

- A force pass also STAMPS each re-derived book's manifest with the
  current parser version (success-only — same truthfulness rule as
  Phase 10) and closes its open processing issues (Phase 11 self-heal).
  Force rescan and the ev-bump trickle now converge: either leaves the
  library fully current; the deploy order is bump-first, then Rescan
  All (documented in the plan and contract_pins).
- Single-file force rescan already ran the full chain — unchanged,
  now pinned.

NEW filter field manually_edited ("Manually edited", boolean):
cardinality(mi.metadata_overrides) > 0 / = 0. Composed-engine (qf)
only — the builder's Book facts group gains it with is true/false;
no value menu, no suggestion surface; family lens inherited.

Tests: multifile force applies sidecar metadata + stamps + override
protection; single-file pin; the filter both directions scoped to the
test library; adjacent suites (Audiobook, Calibre, ExtractorVersion,
CrossLibraryMove, ProcessingIssues, FilterBuilder, SchemaIdempotency)
green. The extractor mechanism itself is untouched — the owner bumps
scan_extractor_version manually via the admin UI (plan Phase 13 §3).
This commit is contained in:
John O'Keefe committed 2026-10-20 22:05:42 -04:00
1 parent 9b5defcb31
commit a42c6d4f75
7 files changed
+239 -6

No files matched your search

+19
View File
@@ -1490,6 +1490,15 @@ func (s *MediaScanner) processExistingRow(ctx context.Context, existingItem data
// Use UPDATE instead of DELETE+INSERT to preserve created_at
if err := s.updateMediaItem(ctx, existingItem, path); err != nil {
fmt.Printf("Warning: failed to update existing media item: %v\n", err)
} else {
// Force rescan = original-scan semantics (faceted plan
// Phase 13): stamp the fresh manifest with the current
// parser version and close open ledger issues — success-
// only, same truthfulness rule as the bulk path.
dir := bookSidecarDir(path)
s.storeManifest(ctx, libraryID, &scanEntry{ID: existingItem.ID, FilePath: s.getRelativePath(path)},
hashSidecarManifest(path, dir, statSidecarManifest(path, dir)))
s.resolveIssuesForItem(ctx, existingItem.ID)
}
// Recompute hash identifiers too - a force rescan is the admin's
// backfill tool and must refresh stale or missing hashes.
@@ -1541,6 +1550,16 @@ func (s *MediaScanner) processExistingViaScanState(ctx context.Context, entry *s
}
if err := s.updateMediaItem(ctx, existingItem, path); err != nil {
fmt.Printf("Warning: failed to update existing media item: %v\n", err)
} else {
// Force rescan = original-scan semantics (faceted plan
// Phase 13): the full extraction just re-derived this row,
// so stamp the fresh manifest with the current parser
// version and close its open ledger issues. Success-only —
// a stamp over a failed update would claim parser work
// that never ran.
dir := bookSidecarDir(path)
s.storeManifest(ctx, libraryID, entry, hashSidecarManifest(path, dir, statSidecarManifest(path, dir)))
s.resolveIssuesForItem(ctx, entry.ID)
}
// Recompute hash identifiers too - a force rescan is the admin's
// backfill tool and must refresh stale or missing hashes.