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
- 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.
- 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.
- 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.
- 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.rb — last_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.rb — document_type enum for minutes_pdf / transcript / packet_pdf / packet_html
app/models/committee.rb — committee_type for tier 2a vs 2b distinction
- Affected topic for verification: https://tworiversmatters.com/topics/513
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_atwas ~3 months old (Jan 5, 2026 resident complaint), but the topic was still appearing in Top Stories / Wire on the homepage because its most recentTopicAppearancewas 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
PruneHollowAppearancesJobeither 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.HomeControllerfilters:@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.agoProposed tiered ranking
Weight "last activity" by the quality of evidence backing each appearance. Strong evidence always beats weak evidence within a reasonable staleness window.
MeetingDocumentof typeminutes_pdfortranscriptwithextracted_textpresent)packet_pdforpacket_html), no minutes/transcript yetRanking rules
effective_activity_ateither stays the same (if the appearance was already within the 90-day Tier 1 window via another meeting) or jumps forward. WhenPruneHollowAppearancesJobdrops a hollow appearance, the ranking recomputes naturally.These window numbers (90 days / 30 days) are starting defaults, not sacred.
Implementation shape
Model layer:
Topic(or a dedicatedTopics::ActivityRankerservice) that returns{ tier: Integer, activity_at: Time }by walking the tiers in order.meeting.committee.committee_type(for newer records) orbody_namepattern matching as fallback.Query layer:
HomeController's@top_stories/@wire_cards/@wire_rowschange from filtering bylast_activity_atto filtering by the new effective-activity value.(tier ASC, effective_activity_at DESC, resident_impact_score DESC)or similar.Tests:
TopicRanker/Topic#effective_activity_atunit tests covering each tier + the "Tier 1 beats Tier 2 within 90d" rule + the "Tier 3 never drives Top Stories" rule.HomeControllerTestor 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
TopicSummaryrows 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
resident_impact_scoreweighting. The AI-derived impact score is a separate signal and has its own issues; this issue is purely about the recency/quality dimension.last_activity_atas a field. It's still useful for non-homepage consumers (audit trails, admin reports). Addeffective_activity_atas 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 constructionapp/models/topic.rb—last_activity_atdefinitionapp/jobs/prune_hollow_appearances_job.rb— recomputeslast_activity_atpost-prune (reference for how the recompute should work)app/models/meeting_document.rb—document_typeenum for minutes_pdf / transcript / packet_pdf / packet_htmlapp/models/committee.rb—committee_typefor tier 2a vs 2b distinction