# Port: `edquests` → `extensions/quests`

2026-08-11

The eighth port, and the first aimed at the thing a young forum actually fails
at: people arrive, look around, and never do anything.

Convoro already has points, ranks, badges and a leaderboard. What it does not
have is anything that tells a new member *what to do next*. A leaderboard shows
you who is winning; it does not tell you how to play. Quests are the missing
half — a small set of named goals with visible progress, rewarded through the
points and badges that already exist rather than through a parallel currency
nobody else can see.

Chosen over `edguard` deliberately. Guardian is the bigger and arguably more
valuable application, but it needs two decisions that are not mine to make: an
AI provider and its cost policy, and whether the illegal-material reporting
half comes across at all. Both are written up at the bottom.

## 🚨 Two traps carried across, one of them expensive

**`days_member` was measured wrong in the original**, and it failed in the worst
possible direction: silently, and generously. The member's join date came back
as a date object, casting it to an integer yielded `1`, and every member read as
having joined in January 1970 — so every "be a member for 30 days" goal
completed the instant a member's progress was first evaluated. **Nobody reports
a quest they finished early.** There is a test for exactly this.

**Rewarding records first and awards second.** If the points call throws after
the award has been recorded, the member is owed points. If it throws before,
they get rewarded again on the next pass, and again, and again. The original got
this right and it is worth restating: write the completion row, then hand out
what it earned.

## 🚨 What the port changes: nothing measures on a page view

The original evaluated a member's quests on page load. That is eight counting
queries per member per page — `COUNT(*)` over posts, topics, reactions given,
reactions received, followers — and it grows with the site rather than with the
feature.

Convoro has a standing rule about work inside a visitor's request, and a live
outage that paid for it. So:

- **Nothing is measured on an ordinary page.** Events mark a member's quests
  stale; the queue re-measures a bounded batch and hands out anything earned.
  When nothing has happened there is no job at all — the same shape Signal uses.
- **The quest board is the one exception**, and it is the same narrow exception
  the update screen and "Back up now" already claim: a person deliberately
  opened this page, is looking at their own progress, and is watching the
  result. One member, bounded, and it is the page whose entire purpose is to be
  up to date.

That also fixes the shared-hosting case honestly. With no cron, quests still
work — they are simply evaluated when a member visits the board — and the
badge that follows arrives when they look, rather than never.

## Goals are a registry, not a `switch`

The original had eight goal types in a `switch` statement. Convoro's version
registers them, so another module can add one: Signal could offer "have a reply
marked as the answer", Downloads could offer "publish a file", and neither has
to be known to this extension.

🚨 **A quest whose goal type is not registered is not shown to members**, and
says so in the admin list. The alternative is a step that silently measures zero
and a quest nobody can ever finish — which looks, to a member, exactly like the
site being broken. Uninstalling the extension that provided a goal is a normal
thing to do and must not leave a dead quest on a public page.

## The seam this needed in core: point reasons are registrable

`Points::REASONS` was a constant with three entries — topic, reply, reaction —
and the profile's "where the points came from" panel labels each line from it.
Awarding under a reason not in that list rendered as the raw key: **`quest: 40`**
on a member's public profile.

So reasons are a registry now, seeded from the same three. A registered reason
carries a label and whether it is *configurable*: the built-in three each get a
field on the points settings screen, because "what is a reply worth" is a policy
question. A quest's reward is set per quest, so it registers as not
configurable and gets a label without a settings field it has no use for.

Every future extension that awards points wants this, and there was no way to
reach it from outside core. Convoro 1.3.6.

## Not built

**A quest step that depends on time inside a window** — "post 10 times this
week". Every goal here is a lifetime total, which is what makes progress
monotonic: it never goes down, so a member never watches a bar retreat. Windowed
goals are a different feature with a different failure mode and belong in their
own version if they belong at all.

## What `edguard` needs before it can be ported — ✅ ANSWERED

Both questions were put to Ernest and both came back the same day:

> *"on edguard allow choice of ai model and yes make it like the ips version."*

So:

1. **The operator chooses the model**, rather than the extension picking one.
   That means provider *and* model are settings, not constants — and a site
   that already pays for one provider must not be told to use another. Guardian
   ships its own provider settings for now; if a second extension ever wants the
   same key, that is when provider configuration becomes a core seam, not
   before.
2. **The illegal-material half comes across in full.** The locked queue that
   never renders the content, the preservation window, and the reporting
   destination. 🚨 That part is a legal workflow rather than a feature, and the
   thing it is actually for is keeping moderators away from material nobody
   should have to look at — so "like the IPS version" means the refusals travel
   too, not only the capabilities.
