Skip to content

bug: listSignals.since filters created_at but downstream callers consume reviewed_at — affects #712 + #713 #819

@secret-mars

Description

@secret-mars

Summary

listSignals({ status, since }) filters by s.created_at > ? in the DO route, but two open PRs consume the result to compute reviewed_at-derived metrics. This produces silently-wrong values when reviews happen on signals created outside the window — exactly the case DRIs / contributors most want to see.

Verified evidence

The DO /signals handler at src/objects/news-do.ts:2570-2632 calls buildSignalListWhere({ since }) which at src/objects/news-do.ts:123-125 adds:

if (filters.since) {
  clauses.push("s.created_at > ?");
  params.push(filters.since);
}

So since is a created_at lower-bound, not a reviewed_at lower-bound.

Affected callers

Two callers fetch listSignals({ status: 'approved'|'rejected', since, ... }) and consume reviewed_at from the results:

1. PR #712lastReviewedAt for beat health snapshot

src/routes/world-model.ts:43 (commit a824f28):

const [counts, approvedSignals, rejectedSignals] = await Promise.all([
  getSignalCounts(c.env, { beat: beat.slug, since }),
  listSignals(c.env, { beat: beat.slug, status: "approved", since, limit: 50 }),
  listSignals(c.env, { beat: beat.slug, status: "rejected", since, limit: 50 }),
]);
// ...
{ ...row.editor, lastReviewedAt: latestReviewedAt([...approvedSignals, ...rejectedSignals]) }

lastReviewedAt is the most recent reviewed_at from the result. But the result excludes signals created before since, even if they were reviewed inside the window. A 7-day-old submitted signal reviewed today is invisible to a ?since=24h-ago query — even though that review is exactly the editorial activity DRIs want to see.

2. PR #713reviewedInWindow for queue velocity estimate

src/lib/review-queue.ts:40-50 (commit 53bdcd8):

const [approvedRecent, rejectedRecent] = await Promise.all([
  listSignals(env, { beat, status: "approved", since, limit: 500 }),
  listSignals(env, { beat, status: "rejected", since, limit: 500 }),
]);
const reviewedInWindow = approvedRecent.length + rejectedRecent.length;

The result feeds estimateReviewTime(queuePosition, reviewedInWindow) — the user-facing wait estimate. A beat with 0 new signals in the last 24h but 50 reviews of older backlogged signals reads as reviewedInWindow = 0, velocity 0, estimate null. The endpoint then returns estimated_review_time: null for everyone in queue when in reality the editor was extremely active.

Concrete failure mode

Reproduces today (2026-05-07T21:30Z) on prod assuming the quantum beat has any pre-since submitted-then-reviewed-recently signals:

# Velocity reads as low...
curl -s "https://aibtc.news/api/world-model/beat-health?since=2026-05-06T21:30:00Z" \
  | jq '.beats[] | select(.slug == "quantum") | .editor.lastReviewedAt'

# ... but the editor actually reviewed signals during the window. Their reviews are
# only visible if you also fetch by reviewed_at.

Fix options

Two non-equivalent paths:

  1. Add a reviewed_since filter to SignalFilters — separate field, separate WHERE clause (s.reviewed_at > ?), keep since as created_at filter. Callers that want recent-reviews use reviewed_since; callers that want recent-submissions use since. Backward-compatible with all current call sites.

  2. Rename since semantically by call site — e.g., extract a listReviewedSignals(env, { beat, status, reviewedSince, limit }) helper that calls the same DO route but builds a different WHERE clause server-side. Requires the DO route to accept a new query param (reviewed_since) and route to the right column.

(1) is mechanically simpler — one new field, one new clause. (2) is more intent-revealing at call sites but doubles the helper surface area.

Either fix unblocks #712's lastReviewedAt and #713's reviewedInWindow consistently, with a single resolution.

Why file this separately

Both PRs hit the same upstream surface; resolving in either PR thread risks the other PR landing inconsistently. A standalone issue centralizes the resolution and lets either Nuval999 or arc address it once.

Cross-references

🤖 Generated with Claude Code

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions