feat(scanner): extractor-version (ev) mechanism — parser fixes reach old rows without a force rescan (faceted plan Phase 10)

Parser fixes (7e4c97f6, genre model v2) never reached rows ingested
under an older parser: the sidecar never changed, so every fingerprint
said 'unchanged'. The manifest JSONB now carries the reserved 'ev'
stamp — the parser version that produced the item's row.

Semantics (all pinned by tests):
- ABSENT ev decodes as 1 ('v1-or-unknown'); stamping is truthful-only:
  create paths + post-extraction stores stamp the setting's current
  value; deploy-day backfill and the extract-failure guard stay UNSET
  (a row never claims a version its data can't prove — a mid-backfill
  bump self-heals next poll); repoint reconciliation preserves the
  stored ev (stat-only refresh re-extracted nothing).
- NEW setting scan_extractor_version (seeded + registry + getter,
  int default 1, category scanner): a manifest whose ev != the setting
  re-extracts through the SAME change path (full extraction →
  updateMediaItemWithMetadata → fresh stamped manifest). Sidecar-less
  books get an entries-less manifest as their ev home (create: stamped;
  first-sighting: unset).
- Catch-up bounded: evReextractBudgetPerScan=100, atomic (watcher pass
  can overlap a poll scan), reset per ScanFolders — amortized +
  resumable across polls; no force rescan, no flood.
- Multifile books included: the ev gate rides multifileSidecarChanged's
  shared ladder; ev-driven manifestChanged drives the editorial apply
  + stamped re-store.

Settings API: the unified GET/PUT /api/system/settings now lists the
key (registry-driven, auto-surfaces in the admin UI). Test harness:
the registry is now Loaded + wired into the SystemSettingsHandler
(main.go parity) — settings writes through the API were 503s before.

Verified: unit (ev normalization, budget CAS, GetExtractorVersion
cache paths, registry clamping); integration state machine —
create-stamp ev=1 → equality skips (corruption probe persists) →
wiped scan-state backfills UNSET without re-extract → bump re-extracts
ONCE (sidecar value restored, ev=2) → stable (second corruption
persists); multifile twin green; adjacent suites (Audiobook, Calibre,
CrossLibraryMove, SchemaIdempotency) green; live app restart re-scans
1198 files with 0 changed (deploy-day rule), setting listed at
/api/system/settings.
This commit is contained in:
John O'Keefe committed 2026-10-20 18:24:08 -04:00
1 parent 43987262a6
commit 91b0c45dd4
10 files changed
+486 -10

No files matched your search

+25
View File
@@ -342,3 +342,28 @@ cover_image_path, has_conflict, format_group, badge`.
- The Phase-7 facet-panel partial stays removed; the all-facets JSON
endpoint and its two shapes are unchanged (they serve flat params —
the builder's UI does not use them).
## 2026-10-20 — Scanner Phase 10: extractor version (ev) mechanism
- `media_scan_state.manifest` JSONB gains the reserved `ev` key:
the parser version that produced the item's CURRENT row. ABSENT
means "v1-or-unknown" — decode normalizes it to 1.
- NEW setting `scan_extractor_version` (int, default 1, min 1,
category scanner): the unified `GET/PUT /api/system/settings` list
now includes this key (additive; the list was not a pinned closed
set). Writes stay settings-API-only — direct SQL leaves the registry
cache stale (known gotcha).
- Scanner behavior: a stored manifest whose ev ≠ the setting is
treated as CHANGED → full re-extract + the fresh manifest stamps
the new ev. Stamping is TRUTHFUL-ONLY: create paths and
post-extraction stores stamp; deploy-day BACKFILL and the
extract-failure guard store UNSET (the row never claims a version
its data can't prove — a mid-backfill bump self-heals next poll).
Repoint reconciliation preserves the stored ev (stat-only refresh,
nothing re-extracted).
- Catch-up is BOUNDED per scan pass (`evReextractBudgetPerScan = 100`,
atomic, reset at each ScanFolders): resumable across polls, no
force rescan, no flood. Budget exhaustion leaves ev stale → later
polls resume.
- Deploy-day rule (unchanged, re-verified live): manifests without ev
ride v1; at default setting 1 the first post-release scan re-extracts
NOTHING (0 changed on 1198 files).