Show CP at resolution time on resolved questions - #5055
Conversation
The default CP preview on resolved questions used the last aggregate forecast rather than the CP at the point of resolution. When a question is resolved as of a past date, it appears open until the resolution is entered, so aggregate forecasts keep accruing after actual_close_time and the preview showed a value from after the question effectively closed. Cap the "latest" aggregate at actual_close_time (min of actual_resolve_time and scheduled_close_time) in both the query used for feed-card previews and the aggregations serializer used on the detail page, so the default CP reflects the value at resolution/close time. Open questions keep showing the most recent CP. Scores are unaffected: scoring already clamps forecast windows to actual_close_time, so this was a display-only issue. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XgUzpLZNHiVXK3t7UGoSsU
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XgUzpLZNHiVXK3t7UGoSsU
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (1)
📝 WalkthroughWalkthroughResolved questions now select aggregate forecasts at their actual close time, while open questions retain the most recent forecast. Serialization marks resolved-question previews as active, and tests cover capped selection, history, no-preview behavior, and end-time handling. ChangesAggregate forecast selection
Estimated code review effort: 3 (Moderate) | ~20 minutes Possibly related PRs
Suggested reviewers: Poem
🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
🚀 Preview EnvironmentYour preview environment is ready!
Details
ℹ️ Preview Environment InfoIsolation:
Limitations:
Cleanup:
|
The capped 'latest' aggregate (CP at resolution time) can carry a past end_time, which the frontend treats as inactive and hides the CP preview/PDF when not hovering. Present the capped preview as the active final CP by nulling its serialized end_time for closed/resolved questions, matching how the last live aggregation was shown before capping. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XgUzpLZNHiVXK3t7UGoSsU
|
lgtm. |
There was a problem hiding this comment.
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 `@questions/serializers/aggregate_forecasts.py`:
- Around line 79-82: Update the forecast selection logic around the eligible
forecast computation to return None when no forecast starts at or before the
cutoff, instead of falling back to forecasts[0]. Add a regression test covering
the case where every forecast begins after actual_close_time, ensuring the
history path does not expose a post-close forecast and remains consistent with
get_last_aggregated_forecasts_for_questions.
🪄 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: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 58966cd9-aaf4-4994-8d59-6c016a38655c
📒 Files selected for processing (3)
questions/serializers/aggregate_forecasts.pyquestions/services/forecasts.pytests/unit/test_questions/test_services/test_forecasts.py
If every aggregate forecast starts after actual_close_time (a closed/resolved question whose only aggregations landed after it closed), the history path previously fell back to the earliest (post-close) forecast as the latest preview, inconsistent with the feed path where such forecasts are excluded. Return None instead so no post-close CP is surfaced. Adds a regression test. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XgUzpLZNHiVXK3t7UGoSsU
hlbmtc
left a comment
There was a problem hiding this comment.
@claude Review.
Also, why don’t we filter out AggregateForecast records for closed or resolved questions directly in get_aggregated_forecasts_for_questions instead of the current approach where we filter them in get_last_aggregated_forecasts_for_questions and then apply the same filter again in _get_latest_aggregate_forecast?
Don’t implement this yet. First, investigate how get_aggregated_forecasts_for_questions is used by other functions and make sure this change wouldn’t break anything or negatively affect database query performance.
| .filter( | ||
| Q(question__actual_close_time__isnull=True) | ||
| | Q(start_time__lte=F("question__actual_close_time")) | ||
| ) |
There was a problem hiding this comment.
Is this covered by indexes, won't we get performance drains? Double-check resulted query and psql plan
There was a problem hiding this comment.
Checked the generated SQL and EXPLAIN plan against the read replica (real data). The new predicate adds an INNER JOIN questions_question (for actual_close_time); here's the before/after for questions 45074–45079:
Before (no join):
Unique
-> Incremental Sort (Key: question_id, method, start_time DESC; Presorted: question_id)
-> Index Scan using questions_aggregateforecast_question_id_...
(Cond: question_id = ANY(...); Filter: start_time <= now())
After (with join):
Unique
-> Incremental Sort (Presorted: question_id)
-> Nested Loop
-> Index Scan using questions_aggregateforecast_question_id_...
(Cond: question_id = ANY(...); Filter: start_time <= now())
-> Index Scan using questions_question_pkey
(Cond: id = af.question_id; Filter: actual_close_time IS NULL OR af.start_time <= actual_close_time)
Both plans are fully index-driven — no sequential scans. The driving scan still uses the FK index on aggregateforecast.question_id, and the added join is a per-row PK lookup on questions_question_pkey (rows=1). The start_time <= actual_close_time comparison is applied as a filter on those already-narrowed rows, not a scan. Every caller passes a bounded question__in set (a post's questions, a batch), so the join stays small. So it's index-covered and no new index is needed.
Generated by Claude Code
|
Claude finished @hlbmtc's task in 2m 26s —— View job ReviewThe PR is well-scoped, well-tested, and the intent is clearly documented in the module-level comments. Behaviour looks correct against A few observations (nothing blocking):
Answer: why not filter directly in
|
| Caller | include_cp_history |
What it does with history |
|---|---|---|
posts/serializers.py:532 (feed + question detail) |
driven by request param, default true on FE detail pages |
drives the CP history chart on the question page |
notifications/services.py:322 ("similar posts" notif) |
False |
latest CP only, binary questions |
questions/services/movement.py:82 (calculate_period_movement_for_questions) |
True |
walks history to pick first_agg (at period start) and last_agg (most recent in past) |
What actually happens today:
get_last_aggregated_forecasts_for_questions is the single source of truth for the feed path — it already applies the actual_close_time filter at the DB layer. The filter in _get_latest_aggregate_forecast isn't a duplicate of the feed path; it exists purely for the history path: when include_cp_history=True, the outer function unions in the full history without the cap, so the serializer has to pick the "latest" from a list that can contain post-close rows. That per-list filter is what keeps the serializer safe against being handed unfiltered history.
If we pushed the cap into the outer get_aggregated_forecasts_for_questions history branch too:
- ✅ The
_get_latest_aggregate_forecastin-memory filter could go away (it'd just takeforecasts[-1]). - ✅ Perf is neutral to slightly better — same filter shape, same join to
question, potentially fewer rows on resolved questions with many post-close aggregations. ⚠️ CP history chart change: on retroactively-resolved questions, post-close aggregations would disappear from thehistoryarray. Arguably desirable (post-resolution CP is meaningless), but this PR's docstring explicitly states "Full history is still serialized; only the 'latest' preview is capped." That's a product/UX call, not a refactor.⚠️ Movement calc:calculate_period_movement_for_questionsonly runs on questions closed within the last 7 days. Itslast_agg= most-recent-in-past. For a retroactively-resolved question, capping history changeslast_aggfrom the post-close aggregation to the at-close one — probably more correct, but a behaviour change to a cached calc.- ➖ Notifications ("similar posts"): unaffected (already
include_cp_history=False, so it goes through the already-filteredget_last_aggregated_forecasts_for_questions).
Recommendation: ship this PR as-is. If y'all decide post-resolution CP points shouldn't render in the history chart either, a small follow-up can:
- Move the
actual_close_timecap intoget_aggregated_forecasts_for_questions's history branch (lines 458–465 inforecasts.py). - Delete the in-memory filter from
_get_latest_aggregate_forecast(it becomesforecasts[-1]). - Update the retroactive-resolution test to assert 1 history entry instead of 3.
- Re-verify the movement path in
movement.py:82— should still be correct, but worth an explicit test.
- Review changed files in the PR
- Investigate how
get_aggregated_forecasts_for_questionsis used - Assess feasibility and DB performance implications
- Post review + answer to the question
| Branch:claude/issue-5053-sb5kii
Address review feedback: collapse the if/else that set serialized_data[method]['latest'] in both branches into one assignment. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XgUzpLZNHiVXK3t7UGoSsU
Summary
Fixes #5053. The default aggregate forecast ("latest" CP) preview on resolved questions showed the last CP value rather than the CP at the point of resolution. These differ when a question is resolved as of a past date (it kept accumulating forecasts until the resolution was actually entered). The preview now reflects the forecast value at the question's effective close/resolution time.
Key Changes
questions/services/forecasts.py:get_last_aggregated_forecasts_for_questions()now caps the "last" aggregate atactual_close_time. Open questions (nullactual_close_time) still use the most recent forecast; closed/resolved questions only consider forecasts that started at or beforeactual_close_time.questions/serializers/aggregate_forecasts.py: Added_get_latest_aggregate_forecast(), which selects the forecast live atactual_close_timefor the "latest" CP preview instead of blindly taking the most recent one. Full history is still serialized; only the "latest" preview is capped.tests/unit/test_questions/test_services/test_forecasts.py: NewTestCPAtResolutionTimesuite covering retroactively-resolved questions (preview uses CP at resolution time, not the later ones) and a regression guard that open questions still show the most recent CP.Notes
actual_close_time=min(actual_resolve_time, scheduled_close_time), matching the horizon that scoring already uses.scoring/score_math.py) already clamps every forecast window toactual_close_time, so it computes on the correct range independently of this display value — the bug was display-only.🤖 Generated with Claude Code
https://claude.ai/code/session_01XgUzpLZNHiVXK3t7UGoSsU
Generated by Claude Code
Summary by CodeRabbit