Skip to content

feat(events-crawler): North Vancouver City Library adapter, staged disabled - #92

Merged
ltanafranca1004 merged 4 commits into
mainfrom
feat/events-crawler-nvcl
Aug 3, 2026
Merged

feat(events-crawler): North Vancouver City Library adapter, staged disabled#92
ltanafranca1004 merged 4 commits into
mainfrom
feat/events-crawler-nvcl

Conversation

@ltanafranca1004

@ltanafranca1004 ltanafranca1004 commented Aug 2, 2026

Copy link
Copy Markdown
Collaborator

What & why

First of the three orgs deferred from the 2026-07-29 round. The deferral rationale did not survive re-checking. It assumed the listing carried no dates and would need a fetch per event; the live feed puts date and time in the listing, so there is no N+1.

Ships enabled: false, same as every Phase-2 source did. No deploy, no enable.

🛑 Known: this source currently yields ZERO rows

Not a bug — the adapter is correct, and the same dry-run returns 25 well-formed rows with the relevance filter off. It's the filter, and it needs a decision that doesn't belong in this PR.

NVCL's entire 4-month window is 27 distinct titles, and lib/relevance.ts matches none of them:

AI for school and learning · Birding for beginners · Book Bike at Lonsdale Quay
Collage party · Crafts and free play in the Plaza · Dungeons & Dragons
Family drop in · Lunchtime Live! · MS Office learn and practice: Intro to Excel
MS Office learn and practice: Intro to Word · North Shore knitters
Open door community hub · Open gaming: Minecraft · Outdoor storytime (×3 parks)
Rattle making workshop · Recording station tour · Seniors' gathering
Silent writing group · Stop motion animation · Storytime in the Plaza
Tech for tots · Teen open gaming · Teen open space · Toddler storytime

Most of that is genuinely off-mission and should be filtered. But two look like real misses: "MS Office learn and practice: Intro to Excel / Word" is digital-skills-for-employment content, and the digital-literacy group only covers digital literacy, computer (basics|skills|help), tech (cafe|help|support), device clinic, online safety, internet basics — none of which match. relevance.ts also already names this exact shape as a known cost: "an opaquely-named but genuinely relevant event (NVDPL's 'Open Door Community Hub Drop-In') is dropped." NVCL runs its own "Open door community hub".

Three ways forward, none taken here:

  1. Add terms to relevance.ts — the path its own header sanctions ("Revisit by adding terms"). But it's shared, so it changes behaviour for all 10 now-live sources, and a naive \bword\b would catch "Crossword"/"Wordplay". Deserves its own PR and its own dry-run regression.
  2. Enable NVCL unfiltered — floods the feed with storytimes, exactly what the filter exists to prevent. No.
  3. Leave NVCL disabled indefinitely and treat this adapter as shelf-ready.

Flagging rather than deciding, since it touches live shared behaviour.

Decisions baked in

The year is derived, then checked. The listing carries none — 0 of 45 sampled rows across three pages. That matters because the paginated list runs chronologically past the year boundary: page 37 of 38 lists April–June, i.e. the following year. "Assume current year" would file those eight months in the past. So each candidate year's weekday is compared against the weekday the page printed, and a row matching neither candidate is skipped rather than guessed at — a wrong date is worse than a missing event.

Second BiblioCommons dead end. Unlike the bpl/Boston trap, nvcl really is North Vancouver City Library — but the gateway answers 403 "The Events feature is not available", exactly as Burnaby's does. Verified before writing any code and recorded in the adapter header so nobody re-tests it.

defaultLocation, and a hard refusal without one. The venue slot exists in the markup but is empty on every row sampled. Since events.location is NOT NULL, the adapter returns [] with an error rather than run — a wrong place is worse than a dropped source.

Dedupe-by-link is load-bearing, not defensive. Every page repeats the same five featured rows above its paginated section, so without it a 24-page walk returns those five 24 times. Verified safe as an identity: across 45 sampled rows no slug appeared with two different dates, so the slug identifies the occurrence and matches events_external_link_key.

No images — the listing renders an icon font, not photos, so covers always fall through to Pexels/Unsplash. No next.config.ts allowlist needed.

Both surrey.ts parse-failure guards are carried forward (page 0 with no blocks; page 0 where every title misses), since this is the same class of scraper.

Changes

  • adapters/nvcl.ts (new) — the adapter.
  • lib/types.tsnvcl-drupal added to SourceKind (the ADAPTERS record is exhaustive, so a missing implementation is a compile error).
  • lib/sources.ts — import, ADAPTERS entry, and a Phase-3 registry block. Existing ten entries untouched.

⚠️ Coordination

enabled: false; no migration, cron, Vault secret or DB object touched; not deployed; no writes to shared prod. Enabling needs Savar's sign-off, exactly as the Phase-2 flip did.

Verification

Run with the real harness (npx deno@2, matching CI's --node-modules-dir=auto).

Check Result
deno check (CI's exact invocation) clean
npx tsc --noEmit clean
npm run lint clean — 0 errors (1 warning pre-existing in opengraph-image.tsx)
dryrun --source nvcl, filter off 25 rows — correct dates, end times, dedupe, covers, genres
dryrun --source nvcl, filter on (shipped) 0 rows — see above
dryrun --list nvcl nvcl-drupal DISABLED, relevance-filtered; other 10 unchanged

Sample of the 25 rows produced with the filter off:

Open door community hub       2026-08-12T16:00Z → 19:00Z   Socials
Outdoor storytime at Semisch  2026-08-12T17:30Z → 18:00Z   Uncategorized
Book Bike at Lonsdale Quay    2026-08-12T22:00Z → 13T02:00Z Socials

Reviewer notes

  • MAX_PAGES = 24 (vs Surrey's 20): the pager advertises 38 pages of 15, and the 4-month window is reached well before that, so the cap is a backstop for a listing that grows or stops being chronological — not the expected exit.
  • The weekday check also catches malformed dates for free: zonedWallClockToUtc already returns null for dates that don't exist (Feb 30), and a wrong-year candidate fails the weekday comparison.
  • End time is dropped rather than stored inverted when it precedes the start — the listing gives no end date, so a past-midnight event can't be confirmed.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added support for collecting events from the North Vancouver City Library.
    • Events are filtered by relevance, date range, and location, then deduplicated and presented in a consistent order.
    • Improved handling of paginated listings, incomplete event details, unavailable pages, and changing website content.
    • Added fallback handling for event images and locations when source details are missing.
    • Configured the new source for staged rollout while keeping it disabled by default.

…sabled

First of the orgs deferred from the 2026-07-29 round. The deferral assumed the
listing carried no dates and would need a fetch per event; re-checking the live
feed showed otherwise — date and time are both in the listing, so there is no N+1.

NVCL is the second library whose BiblioCommons tenant is a dead end. Unlike the
bpl/Boston trap, `nvcl` really is North Vancouver City Library — but the gateway
answers 403 "The Events feature is not available", exactly as Burnaby does.
Verified before writing a line, and recorded in the adapter header so nobody
re-tests it. The Drupal listing is the only readable surface.

The listing carries no year. Every row reads "Tuesday, August 4, 10:30 am to
11:00 am" — 0 of 45 sampled rows across three pages carried one. That matters
because the paginated list runs chronologically past the year boundary: page 37
of 38 lists April-June, i.e. the following year, which "assume current year"
would file eight months in the past. So the year is derived and then *checked*:
each candidate year's weekday is compared against the weekday the page printed,
and a row matching neither is skipped rather than guessed at.

Two more things the feed does not have. There is no location — the venue slot
exists in the markup but is empty on every row sampled — so the source supplies
defaultLocation, and the adapter refuses to run without one rather than inserting
a wrong place into a NOT NULL column. And there are no images, only an icon font,
so covers always fall through to the Pexels/Unsplash tiers.

Dedupe-by-link is load-bearing here rather than defensive: every page repeats the
same five featured rows above its paginated section, so without it a 24-page walk
returns those five 24 times. Safe as an identity — across 45 sampled rows no slug
appeared with two different dates, so the slug identifies the occurrence and
matches events_external_link_key.

Carries forward both parse-failure guards from surrey.ts (page 0 with no blocks,
page 0 where every title misses), since this is the same class of scraper.

KNOWN: with relevanceFilter on, this source currently yields ZERO rows — see the
PR for the evidence and the decision it needs. Landing disabled either way.

Verified with the real harness: deno check clean; dryrun --source nvcl parses 25
well-formed rows with the filter off (correct dates, end times, dedupe, covers)
and 0 with it on. tsc and lint clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented Aug 2, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
web-app Ready Ready Preview Aug 3, 2026 1:31am

@coderabbitai

coderabbitai Bot commented Aug 2, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 3fbee600-8114-4d64-877e-56c4d8b37037

📥 Commits

Reviewing files that changed from the base of the PR and between 989d4fc and ada14d1.

📒 Files selected for processing (3)
  • supabase/functions/events-crawler/adapters/nvcl.ts
  • supabase/functions/events-crawler/lib/sources.ts
  • supabase/functions/events-crawler/lib/types.ts

Walkthrough

Adds a staged North Vancouver City Library Drupal source. The adapter fetches and parses paginated event pages, resolves yearless dates, filters and deduplicates events, resolves covers, and returns event rows with per-row failure isolation.

Changes

NVCL event ingestion

Layer / File(s) Summary
Source kind and adapter registration
supabase/functions/events-crawler/lib/types.ts, supabase/functions/events-crawler/lib/sources.ts
Adds nvcl-drupal to SourceKind, registers the adapter, and adds a disabled NVCL source with relevance filtering and a default location.
Concurrent page fetching and event parsing
supabase/functions/events-crawler/adapters/nvcl.ts
Fetches paginated Drupal HTML with timeouts, parses event details and yearless dates, applies relevance and date-window filters, and detects malformed markup.
Event selection and row construction
supabase/functions/events-crawler/adapters/nvcl.ts
Stops traversal at exhaustion or configured limits, deduplicates and sorts candidates, resolves fallback covers, and builds rows while isolating row failures.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the new North Vancouver City Library adapter and its staged, disabled status.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/events-crawler-nvcl

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@supabase/functions/events-crawler/adapters/nvcl.ts`:
- Around line 264-271: Update parsePage so maxStartMs is calculated only from
the paginated chronological listings, excluding the five leading featured rows
repeated on every page. Preserve the existing pastWindow stop logic in the
crawler loop, and verify the featured-row dates remain within ctx.windowEndMs
before relying on this source.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 0543e4b8-7be2-4082-9c02-41732caac88f

📥 Commits

Reviewing files that changed from the base of the PR and between 989d4fc and 9b3ea7f.

📒 Files selected for processing (3)
  • supabase/functions/events-crawler/adapters/nvcl.ts
  • supabase/functions/events-crawler/lib/sources.ts
  • supabase/functions/events-crawler/lib/types.ts

Comment thread supabase/functions/events-crawler/adapters/nvcl.ts Outdated
…aximum

Addresses the CodeRabbit finding, which is a real latent truncation bug.

parsePage took the maximum start across every block on the page, but each page is
"five featured rows, then the chronological section". The featured rows are
curated, so one can sit arbitrarily far out — a save-the-date. That maximum would
then exceed the window on page 0, set pastWindow on the very first batch, and stop
the walk after 4 pages while in-window listings remained. Not reachable today
(the featured rows are currently Aug 4-5, well inside the window) but entirely
reachable by an editorial choice on the library's side, with no signal that it
had happened.

Took a different fix from the one sketched. Requiring a whole batch to agree
before stopping does not work here: the featured block repeats on EVERY page, so
a distant featured row makes every page in the batch report the same
past-the-window maximum and the walk still truncates. Excluding "the leading five"
would work but hardcodes a count the markup never promises.

Using the LAST block in document order sidesteps both. The chronological section
always trails the featured block, so the final block is the furthest-out entry the
paginated list has actually reached — which is the thing the walk wants to compare
against the window — and it needs no guess about how many featured rows exist.

Verified against the live feed: with the relevance filter off the walk still ends
early and correctly, taking 4 pages, 58 candidates, 38 unique, soonest 25. The 20
dropped duplicates are exactly the 5 featured rows repeated across those 4 pages,
which is the repetition this finding is about. deno check clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@ltanafranca1004

Copy link
Copy Markdown
Collaborator Author

Applied in 96aa322 — valid finding, and a real latent truncation bug. Took a different fix from the one sketched, for a reason worth spelling out.

Why the batch-level condition doesn't work here. The sketch stops only when a whole batch is past the window. But the featured block repeats on every page — that's the premise of the finding — so a distant featured row makes every page in the batch report the same past-the-window maximum. The batch condition is then satisfied just as readily as the per-page one, and the walk truncates anyway. It changes how many pages it takes to go wrong, not whether it does.

Why not exclude the leading five. That works, but hardcodes a count the markup never promises. If the library features four one week and six the next, the guard silently starts trimming real rows off the chronological section.

What I did instead: use the LAST block in document order. The chronological section always trails the featured block, so the final block is the furthest-out entry the paginated list has actually reached — precisely the value the walk wants to compare against the window — and it needs no assumption about how many featured rows there are. maxStartMs became lastStartMs.

Verified against the live feed. With the relevance filter off (so the walk actually produces rows), it still ends early and correctly:

nvcl walked 4 page(s), 58 in-window + relevant (38 unique), taking soonest 25

That run also happens to confirm the finding's premise: 58 candidates → 38 unique is exactly 20 duplicates, i.e. the 5 featured rows repeated across 4 pages.

One thing worth confirming explicitly, since you asked: the featured-row dates are currently well inside the window — Aug 4–5 against a Dec 2 edge — so this was latent, not active. It would have become reachable the first time NVCL featured a save-the-date.

deno check clean. The source remains enabled: false.

@ltanafranca1004

Copy link
Copy Markdown
Collaborator Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Aug 2, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.


Your included review limit is currently reached under our Fair Usage Limits Policy. This review may still proceed through usage-based billing if eligible. Your next included review will be available in 52 minutes.

@ltanafranca1004

Copy link
Copy Markdown
Collaborator Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@supabase/functions/events-crawler/adapters/nvcl.ts`:
- Around line 229-286: Update fetchEvents to track candidate uniqueness by link
while pages are processed, using an incremental set or equivalent keyed by each
candidate’s link. Add candidates only when their links have not been seen, and
use the unique count for the MAX_PER_ORG early-stop check instead of
candidates.length; preserve the existing later processing and other stop
conditions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: c96db07b-c7fb-4d0e-9408-4471a50985e5

📥 Commits

Reviewing files that changed from the base of the PR and between 989d4fc and 96aa322.

📒 Files selected for processing (3)
  • supabase/functions/events-crawler/adapters/nvcl.ts
  • supabase/functions/events-crawler/lib/sources.ts
  • supabase/functions/events-crawler/lib/types.ts

Comment thread supabase/functions/events-crawler/adapters/nvcl.ts
…t raw hits

Second CodeRabbit finding on this adapter, and valid for the same underlying
reason as the first: the featured block repeats on every page, so any running
total that ignores it means something other than what it says.

The MAX_PER_ORG early-stop compared candidates.length, which counted the featured
rows once per page. With the relevance filter on — the shipped configuration —
a source whose relevant rows are mostly featured ones would reach 25 in raw count
while holding a handful of distinct events, stop there, and return that handful
with in-window listings still unread. The observed run hid this (58 raw / 38
unique, both above the cap) but the shape of the bug does not depend on that mix.

Deduping during the walk rather than after it fixes the count at its source and
removes the separate post-loop pass: byLink.size is now what the stop condition
tests, what the page-cap warning reports, and what gets sorted and sliced. A raw
counter is kept purely for the log line, where "58 in-window + relevant (38
unique)" is genuinely more informative than either number alone.

Verified against the live feed. Filter off: 4 pages, 58 raw, 38 unique, soonest
25 — identical to before, so the happy path is unchanged. Filter on: still 0, now
reported as "0 unique candidate(s)". deno check clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@ltanafranca1004

Copy link
Copy Markdown
Collaborator Author

Applied in eed0f64 — valid, and it's the same underlying issue as the first finding wearing a different hat: the featured block repeats on every page, so any running total that ignores that means something other than what it says.

candidates.length counted the featured rows once per page. With the relevance filter on — which is the shipped configuration — a source whose relevant rows are mostly featured ones would hit 25 in raw count while holding only a handful of distinct events, stop, and return that handful with in-window listings still unread.

Worth being precise that the observed run masks this rather than demonstrating it: 58 raw / 38 unique are both above the cap, so the correct 25 came out either way. The bug doesn't depend on that mix being favourable, which is why it's worth fixing rather than filing.

Deduped during the walk instead of after it, which fixes the count at its source rather than patching the comparison. byLink.size is now the single thing the stop condition tests, the page-cap warning reports, and the sort/slice consumes — and the separate post-loop dedupe pass is gone. A raw counter survives only for the log line, where 58 in-window + relevant (38 unique) says more than either number alone.

Verified against the live feed:

filter off:  walked 4 page(s), 58 in-window + relevant (38 unique), taking soonest 25
filter on:   stopped at the 24-page cap with 0 unique candidate(s)

Filter-off output is byte-identical to before the change, so the happy path is untouched. deno check clean. Source remains enabled: false.

@ltanafranca1004

Copy link
Copy Markdown
Collaborator Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@supabase/functions/events-crawler/adapters/nvcl.ts`:
- Around line 157-227: Update parsePage to track when-line and date-parsing
failures alongside title-link failures, using a dedicated counter or equivalent
aggregate that identifies blocks rejected before candidate creation. Update the
page-0 diagnostic in fetchEvents to report this failure state when all parsed
blocks fail title or date extraction, while preserving the existing zero-block
and relevance-filter behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 3cde6e77-a61a-498a-9f15-77970b2cad0d

📥 Commits

Reviewing files that changed from the base of the PR and between 989d4fc and eed0f64.

📒 Files selected for processing (3)
  • supabase/functions/events-crawler/adapters/nvcl.ts
  • supabase/functions/events-crawler/lib/sources.ts
  • supabase/functions/events-crawler/lib/types.ts

Comment thread supabase/functions/events-crawler/adapters/nvcl.ts
Third CodeRabbit finding, and valid: the page-0 diagnostic only counted title
misses, so a block whose anchor still matched but whose when-line or date format
had moved was rejected silently. The source would return zero rows and look
exactly like a library with nothing on.

Closed the whole family rather than the flagged layer, because these fail
independently and a per-layer fix invites the same finding again next round. The
three structural rejections — no anchor or empty title, no bold when-line
paragraph, when-line present but unparseable — are now counted separately, and
page 0 reports the breakdown when none of its blocks survive extraction. The
per-layer split is not decoration: it says which of the three moved, and the fix
differs for each.

The relevance and window filters are deliberately excluded from that count. They
reject on merit, and rejecting most of a library calendar is precisely their job,
so folding them in would make the diagnostic fire on every healthy run — this
source legitimately returns zero under the filter today.

Verified by breaking each layer in turn against the live feed:
  when-line regex broken → "title 0, when-line 15, date 0"
  date parse broken      → "title 0, when-line 0, date 15"
  restored               → silent, and still 0 rows from the relevance filter
That last line is the one that matters: a legitimate zero stays quiet while a
structural zero is loud.

Happy path unchanged — filter off still gives 4 pages, 58 raw, 38 unique,
soonest 25. deno check clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@ltanafranca1004

Copy link
Copy Markdown
Collaborator Author

Applied in ada14d1 — valid, and the same silent-zero hazard the other two guards exist to prevent, one layer further down.

Closed the whole family rather than the flagged layer. The three structural rejections fail independently — the anchor can move, the bold when-line paragraph can be renamed, or the date string can change shape (dropping the weekday would defeat the parse on its own) — so fixing only the when-line invites this finding again next round. All three are now counted separately, and page 0 reports the breakdown when nothing survives extraction. The split isn't decoration: it names which layer moved, and the fix differs for each.

The relevance and window filters are deliberately excluded from that count. They reject on merit, and rejecting most of a library calendar is their job — this source legitimately returns zero under the filter today. Folding them in would make the diagnostic fire on every healthy run, which is how a warning becomes noise and then gets ignored.

Verified by breaking each layer in turn against the live feed:

when-line regex broken → matched 15 block(s) … (title 0, when-line 15, date 0)
date parse broken      → matched 15 block(s) … (title 0, when-line 0, date 15)
restored               → silent, and still 0 rows from the relevance filter

That last line is the one that matters — a legitimate zero stays quiet while a structural zero is loud. Under the previous code the first two cases printed nothing at all, because titleMisses was 0.

Happy path unchanged: filter off still gives 4 page(s), 58 in-window + relevant (38 unique), taking soonest 25. deno check clean, source still enabled: false.


Worth noting across this review round, since all three findings share a root: NVCL pages are featured block + chronological section, and every bug found here came from logic that treated a page as homogeneous — the page maximum (#1), the raw candidate count (#2), and now single-layer failure detection (#3). I've swept the adapter for the remaining instances of that assumption and don't see a fourth. Carrying the same lens into the Capilano adapter rather than rediscovering it in review.

@ltanafranca1004

Copy link
Copy Markdown
Collaborator Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.

@ltanafranca1004
ltanafranca1004 merged commit 40ca9e3 into main Aug 3, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant