Skip to content

Tiered last-activity ranking: weight homepage by evidence quality, not just calendar recency #95

@AndreRobitaille

Description

@AndreRobitaille

Background

Surfaced during the #93 + #94 verification session for topic 513 ("garbage and recycling service changes"). After both shipped, topic 513's last_substantive_activity_at was ~3 months old (Jan 5, 2026 resident complaint), but the topic was still appearing in Top Stories / Wire on the homepage because its most recent TopicAppearance was on m178 (April 6, 2026 — a PUC meeting with a packet but no published minutes yet).

The homepage ranking treats "topic on the agenda of a past meeting" the same as "topic had substantive activity recently", regardless of whether minutes exist to confirm anything substantive happened. So topic 513 rides the homepage for 2-4 weeks after every committee meeting where it was on the agenda, until minutes land and PruneHollowAppearancesJob either keeps the appearance (decision/discussion) or drops it (status_update).

Current state

Topic#last_activity_at = max(topic_appearances.appeared_at). No filtering by evidence quality.

HomeController filters:

  • @top_stories: resident_impact_score >= 4 AND last_activity_at >= 30.days.ago
  • @wire_cards / @wire_rows: resident_impact_score >= 2 AND last_activity_at >= 30.days.ago

Proposed tiered ranking

Weight "last activity" by the quality of evidence backing each appearance. Strong evidence always beats weak evidence within a reasonable staleness window.

Tier Definition
1 Past meeting with minutes OR transcript (MeetingDocument of type minutes_pdf or transcript with extracted_text present)
2a Past City Council or Work Session with packet (packet_pdf or packet_html), no minutes/transcript yet
2b Past committee (anything not council/work session) with packet, no minutes/transcript yet
3a Upcoming City Council or Work Session with packet or agenda
3b Upcoming committee with packet or agenda

Ranking rules

  1. Tier 1 always wins within 90 days. If a topic has any Tier 1 appearance within 90 days, its effective recency = max(Tier 1 appearance dates). Tier 2 appearances, even if more recent, do not override.
  2. Beyond 90 days, Tier 2 can take over if it's within its own window (say 30 days). Between Tier 2a and 2b, council/work session beats committee regardless of date.
  3. Tier 3 (upcoming) never drives Top Stories. It belongs in the "Next Up" zone on the homepage, which already exists as a separate section. Upcoming-only topics shouldn't compete with topics that have actual past events.
  4. Self-correction: once minutes/transcript land on a Tier 2 appearance, it promotes to Tier 1 and effective_activity_at either stays the same (if the appearance was already within the 90-day Tier 1 window via another meeting) or jumps forward. When PruneHollowAppearancesJob drops a hollow appearance, the ranking recomputes naturally.

These window numbers (90 days / 30 days) are starting defaults, not sacred.

Implementation shape

Model layer:

  • Add a method on Topic (or a dedicated Topics::ActivityRanker service) that returns { tier: Integer, activity_at: Time } by walking the tiers in order.
  • Query-time computation to start; can materialize into a column later if the homepage query gets slow.
  • Committee type lookup via meeting.committee.committee_type (for newer records) or body_name pattern matching as fallback.

Query layer:

  • HomeController's @top_stories / @wire_cards / @wire_rows change from filtering by last_activity_at to filtering by the new effective-activity value.
  • Sort key becomes (tier ASC, effective_activity_at DESC, resident_impact_score DESC) or similar.

Tests:

  • TopicRanker / Topic#effective_activity_at unit tests covering each tier + the "Tier 1 beats Tier 2 within 90d" rule + the "Tier 3 never drives Top Stories" rule.
  • HomeControllerTest or an integration test asserting that a topic with a Tier 2b April appearance and a Tier 1 January appearance does NOT show up in Top Stories when the window is 30d past Jan.

Secondary benefit

This fix would also clean up the 116 topics we found with orphan TopicSummary rows during the #93 diagnostic cleanup. Most of those topics are probably in the same situation as topic 513 — showing up as "recent" on the homepage because of agenda-only-no-minutes appearances, even though their last substantive event was months ago. Fixing the ranking is higher-leverage than the orphan cleanup alone, and sidesteps the need for a 190-row mass destruction.

Out of scope

  • Don't rewrite the homepage layout. The four zones (Top Stories, Wire, Next Up, Escape Hatches) are correct. Only the topic-selection logic for Top Stories and Wire changes.
  • Don't change resident_impact_score weighting. The AI-derived impact score is a separate signal and has its own issues; this issue is purely about the recency/quality dimension.
  • Don't remove last_activity_at as a field. It's still useful for non-homepage consumers (audit trails, admin reports). Add effective_activity_at as a parallel concept.

Discovered while working on

Refs #93, #94. Surfaced when verifying topic 513 on the live homepage after both shipped. Topic 513's content is now good (specific headline + 3 substantive timeline entries), but the topic shouldn't be on the front page at all given the most recent substantive event is January 2026.

References

  • app/controllers/home_controller.rb — Top Stories / Wire / Next Up construction
  • app/models/topic.rblast_activity_at definition
  • app/jobs/prune_hollow_appearances_job.rb — recomputes last_activity_at post-prune (reference for how the recompute should work)
  • app/models/meeting_document.rbdocument_type enum for minutes_pdf / transcript / packet_pdf / packet_html
  • app/models/committee.rbcommittee_type for tier 2a vs 2b distinction
  • Affected topic for verification: https://tworiversmatters.com/topics/513

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions