From 1d5537742f4cd881ba65e9ca2c1483954a283ff2 Mon Sep 17 00:00:00 2001 From: afiqzudinhadi Date: Mon, 3 Aug 2026 22:08:09 +0800 Subject: [PATCH] fix(library): first-pass enrich gate skips post-resolve skeletons 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. --- packages/obsidian/src/library/book.ts | 9 ++++++--- packages/obsidian/src/library/comic.ts | 3 +++ packages/obsidian/src/library/game.ts | 9 ++++++--- tests/library-book.test.ts | 6 ++++++ tests/library-comic.test.ts | 6 ++++++ tests/library-game.test.ts | 19 ++++++++++++++----- 6 files changed, 41 insertions(+), 11 deletions(-) diff --git a/packages/obsidian/src/library/book.ts b/packages/obsidian/src/library/book.ts index 8e53743..0ece284 100644 --- a/packages/obsidian/src/library/book.ts +++ b/packages/obsidian/src/library/book.ts @@ -126,9 +126,12 @@ export const bookSpec: MediaTypeSpec = { }, isActive(fm: Record): boolean { - // static once enriched; no chapter/issue sources, no flips -- only a missing - // olid ever makes a book note active again (a full sync bypasses this check) - return !stripQuotes(fm['olid']); + // active until the first real enrich has actually run: missing olid (never + // resolved) OR missing read_status (post-resolve skeleton -- canonical render + // always writes read_status, so its absence means sync() hasn't produced + // canonical output yet). Static once both are present; no chapter/issue sources, + // no flips -- a full sync bypasses this check regardless. + return !stripQuotes(fm['olid']) || !stripQuotes(fm['read_status']); }, async resolve(ctx: LibraryNoteCtx, deps: SpecDeps): Promise | null> { diff --git a/packages/obsidian/src/library/comic.ts b/packages/obsidian/src/library/comic.ts index 6695792..279cef6 100644 --- a/packages/obsidian/src/library/comic.ts +++ b/packages/obsidian/src/library/comic.ts @@ -158,6 +158,9 @@ export const comicSpec: MediaTypeSpec = { isActive(fm: Record): boolean { if (!stripQuotes(fm['comicvine_id'])) return true; // never enriched -> needs first pass + // post-resolve skeleton -- canonical render always writes status, so its absence + // means sync() hasn't produced canonical output yet + if (!stripQuotes(fm['status'])) return true; if (stripQuotes(fm['status']) === 'Ongoing') return true; if (stripQuotes(fm['read_status']) === 'Reading') return true; return false; diff --git a/packages/obsidian/src/library/game.ts b/packages/obsidian/src/library/game.ts index f492b63..6c8944f 100644 --- a/packages/obsidian/src/library/game.ts +++ b/packages/obsidian/src/library/game.ts @@ -192,9 +192,12 @@ export const gameSpec: MediaTypeSpec = { }, isActive(fm: Record): boolean { - // static once either id is set; no chapter/issue-style automation exists for games -- - // only missing both ids ever makes a game note active again (full sync bypasses this) - return !stripQuotes(fm['steam_appid']) && !stripQuotes(fm['rawg_id']); + // active until the first real enrich has actually run: both ids empty (never + // resolved) OR missing play_status (post-resolve skeleton -- canonical render + // always writes play_status, so its absence means sync() hasn't produced canonical + // output yet). Static once an id + play_status are present; no chapter/issue-style + // automation exists for games -- a full sync bypasses this check regardless. + return (!stripQuotes(fm['steam_appid']) && !stripQuotes(fm['rawg_id'])) || !stripQuotes(fm['play_status']); }, async resolve(ctx: LibraryNoteCtx, deps: SpecDeps): Promise | null> { diff --git a/tests/library-book.test.ts b/tests/library-book.test.ts index aea1715..b8d348c 100644 --- a/tests/library-book.test.ts +++ b/tests/library-book.test.ts @@ -194,6 +194,12 @@ describe('bookSpec.isActive', () => { test('olid set + Unread -> static', () => { expect(bookSpec.isActive({ olid: 'OL1168083W', read_status: 'Unread' })).toBe(false); }); + test('olid set, read_status missing (post-resolve skeleton) -> active (C1)', () => { + expect(bookSpec.isActive({ olid: 'OL1168083W' })).toBe(true); + }); + test('canonical enriched note (olid + read_status both present) -> static', () => { + expect(bookSpec.isActive({ olid: 'OL1168083W', read_status: 'Unread' })).toBe(false); + }); }); function ctxFor(fm: Record, body = '## My Notes\n\n'): LibraryNoteCtx { diff --git a/tests/library-comic.test.ts b/tests/library-comic.test.ts index 29dc7d2..f0850a6 100644 --- a/tests/library-comic.test.ts +++ b/tests/library-comic.test.ts @@ -235,6 +235,12 @@ describe('comicSpec.isActive', () => { test('status Ended + read_status Unread -> static', () => { expect(comicSpec.isActive({ comicvine_id: '195824', status: 'Ended', read_status: 'Unread' })).toBe(false); }); + test('comicvine_id set, status missing (post-resolve skeleton) -> active (C1)', () => { + expect(comicSpec.isActive({ comicvine_id: '195824' })).toBe(true); + }); + test('canonical enriched note (status Ended, not Ongoing) -> static', () => { + expect(comicSpec.isActive({ comicvine_id: '195824', status: 'Ended', read_status: 'Read' })).toBe(false); + }); }); describe('comicSpec.resolve', () => { diff --git a/tests/library-game.test.ts b/tests/library-game.test.ts index f9a994c..8b5d593 100644 --- a/tests/library-game.test.ts +++ b/tests/library-game.test.ts @@ -266,13 +266,22 @@ describe('gameSpec.isActive', () => { test('both ids empty -> active (needs first pass)', () => { expect(gameSpec.isActive({})).toBe(true); }); - test('steam_appid set -> static', () => { - expect(gameSpec.isActive({ steam_appid: '792100' })).toBe(false); + test('steam_appid set + play_status set -> static', () => { + expect(gameSpec.isActive({ steam_appid: '792100', play_status: 'Unplayed' })).toBe(false); }); - test('rawg_id set -> static', () => { - expect(gameSpec.isActive({ rawg_id: '4200' })).toBe(false); + test('rawg_id set + play_status set -> static', () => { + expect(gameSpec.isActive({ rawg_id: '4200', play_status: 'Unplayed' })).toBe(false); }); - test('both set -> static, regardless of play_status', () => { + test('both ids set -> static, regardless of play_status', () => { + expect(gameSpec.isActive({ steam_appid: '792100', rawg_id: '4200', play_status: 'Played' })).toBe(false); + }); + test('id set, play_status missing (post-resolve skeleton) -> active (C1)', () => { + expect(gameSpec.isActive({ steam_appid: '792100' })).toBe(true); + }); + test('both ids set, play_status missing (post-resolve skeleton) -> active (C1)', () => { + expect(gameSpec.isActive({ steam_appid: '792100', rawg_id: '4200' })).toBe(true); + }); + test('canonical enriched note -> static', () => { expect(gameSpec.isActive({ steam_appid: '792100', rawg_id: '4200', play_status: 'Played' })).toBe(false); }); });