Supersedes 4249952's half-fix: try now wraps pickCandidate too; restores
the 19 controller tests that commit deleted; replaces its source-grep
pseudo-tests with a behavioral two-entry isolation test.
syncAll() ran each enabled type's sync in sequence with no per-type
error handling, so one type throwing (e.g. its vault folder missing)
aborted the whole run and skipped libraryLastSync -- every other
enabled type silently never synced. Each type's runSync is now wrapped
individually: failures are logged (and surfaced via Notice unless
quiet) and the loop continues; libraryLastSync still updates after the
loop regardless.
Also: dry-run sync previews now route spec-level flip notifications
(the "chapter/issue out" Notices) through the log instead of a real
Notice, since a dry run never actually writes the change it's
describing.
- comic/manga sync(): a note's first-ever observed issue/chapter number
(no prior latest_issue/latest_chapter) now seeds the baseline without
flipping read_status or notifying. Previously `prev === null` counted
as "newer", so a Read note with no stored baseline flipped to Unread
on its very first real sync. Flips now require a prior value AND a
strictly greater candidate AND read_status Read, mirroring the
watchlist build.ts `prevLast &&` guard.
- manga rss/last_chapter_date: quotedOrNull renders an unset value as
the literal `null` token; on the next parse that string came back
truthy, so an unset rss field was fetched as the URL "null" and
isActive treated it as "has rss -> active" forever. Sentinel is now
stripped back to '' everywhere it's read from frontmatter.
- manga resolve(): best-effort MangaDex id lookup by title (unique-exact
match against attributes.title + altTitles) runs alongside the
existing MAL lookup when the note has no mangadex_id yet. Any failure
(no match, ambiguous, network error) is logged and skipped, leaving
the mal_id patch intact.
isActive() for book/game/comic only checked for a resolved id, so a
note that had run resolve() but never a real sync() (no read_status/
play_status/status field yet) was treated as static and never
enriched on subsequent runs. Now also requires the status-analog
field canonical render always writes; missing it means sync() hasn't
actually produced output yet.
- render **Rating:** line in manga/book/game/comic body (after read/play
status, gated on rating != '0' + stars non-empty), matching watchlist's
render.ts convention
- manga finish-flip now notifies user (final chapters out), matching
chapter-flip's existing notify path
- game renderGame emits ## Links (Steam by appid, RAWG by url) — fixes
latent owned-heading strip where user-added Links sections silently
vanished since games never re-rendered the heading
- move duplicated deriveReadStatus/deriveRating/parseNumOrNull into
library/convert.ts, import across specs; zero behavior change
TMDB's origin_country returns ISO codes (KR), not full names, causing
a vault regression from South Korea to KR. buildRecord now prefers
production_countries[0].name, falling back to the raw origin_country
code only when absent.
TMDB also now returns vote_average at full precision (6.537), causing
mass diff noise against the vault's 1-decimal convention and spurious
rewrites on precision drift alone. tmdb_rating is now rounded to 1
decimal place.
Loki fixture updated to realistic TMDB shapes (ISO country code +
production_countries) to catch this regression going forward.
renderNote wiped any ## heading not owned by the renderer (Synopsis,
Cast, Links, My Notes) on every sync, destroying user content like
## Collection graph-link sections. extractCustomSections now pulls
non-owned sections out of the existing body before rewrite, and
renderNote re-emits them verbatim, in original order, between Links
and My Notes.
package.json author/description now match manifest.json, release
workflow plugin name and repo-automation.config.json repo ref no
longer point at the upstream project.
- SyncDeps.listNotes now returns paths only; new readNote(path) reads
content immediately before parse/diff/write per note, closing the
window where an edit made between the initial scan and a later write
got silently overwritten. resolveMissingIds updated to match.
- WatchlistController gains a syncing flag shared by syncNow and
resolveMissingIds so overlapping invocations short-circuit instead
of racing on the same vault notes.
- main.ts registers cleanup for the 30s startup catch-up setTimeout so
it doesn't fire after unload.
- syncNow takes a quiet flag (used by the scheduled catch-up path) so
zero-change scheduled syncs don't spam a Notice; manual commands are
unaffected.
quotedOrNull renders an empty notion_url as the literal string `null`.
buildRecord read that back verbatim on the next sync pass and re-quoted
it as "null", breaking diff-on-write (every re-sync of a note with no
notion_url appeared changed). Treat the `null` sentinel as empty when
reading prev frontmatter.