Background
/impact currently shows two cards: Key Highlights (wikis using the kit, contributors onboarded, tasks completed) and Users onboarded. "Tasks completed" is a single aggregate — it says how many task runs finished, but not what got made. The per-task outputs the kit actually produces (articles created, policy pages translated, infoboxes imported, main pages customised…) are not surfaced anywhere.
Proposal
Add a third card at the bottom of /impact:
Essential work done through the kit
The detailed outputs behind the highlights, across all wikis.
(The subtitle originally promised a per-wiki filter; that is split into its own task — see the per-wiki filter section.)
Ten stat tiles in two rows of five, each with a coloured left accent, a large value and a caption — visually the same tile idiom as KeyHighlights.vue.
Kit-side sources (seed data + kit-action tiles)
Each tile mapped to what the kit itself records. How to read this table under the decided reach scope:
- For the kit-action tiles (4, 6, 8, 9) — tiles that count something the kit itself did — this is the displayed metric; reach and direct coincide there.
- For the reach tiles (1, 2, 3, 5, 7) these are the seed sets and anchors their reach aggregation is computed from (twn_links, install events, ledger anchors, imported-template titles) — the displayed numbers are defined in the metric-scope section.
- These same sources, read directly as the tile values, are the rejected direct-completions alternative (see that subsection) — so this table doubles as the record of what that alternative would have shown, and its Status column tracks which MR/task unblocks each source.
The values in the mockup are placeholders, not targets — see the metric-scope section.
| # | Tile | Proposed source | Status |
|---|---|---|---|
| 1 | essential articles added | task_verifications rows for create-essential-article (distinct created_title) | Available after !152 |
| 2 | interface messages translated | T433428's twn_links (last_count − baseline) and/or its milestone ledger rows | Unblocked by T433428 — see below |
| 3 | policy pages added | on-wiki: the user STARTED the translation from the kit (a translate_policy_page cta_click of either open-translation or open-cx — both, see its section) and the policy page's langlink target was later created by that same user — NOT .complete clicks | T433617 implemented (MR !156) — once merged this is a plain task_verifications read like tiles 1 and 5, no aggregation needed; that task's verifier IS this predicate |
| 4 | HotCat gadget installs | install_gadget.cta_click, cta=install-success (fires only after the common.js write succeeds) | Available — an install need not lead to an edit, so this will exceed tile 5 |
| 5 | articles categorized with HotCat | task_verifications rows for install-gadget | Available — but = 3 in prod today |
| 6 | infoboxes imported | import_infobox.cta_click, cta=import-template-success | Available |
| 7 | articles given missing infoboxes | MW API transclusion counts of kit-imported infobox templates | Defined — needs the aggregation subsystem, see its section |
| 8 | main page customizations | wizard.publish with outcome=success | Available |
| 9 | recruit shares initiated | community.recruit_share | Available |
| 10 | accounts created via the kit | recruit-link account creations — no accessible attribution channel | Blocked — see its section |
Metric scope — the card shows kit-seeded reach (DECIDED)
Decided (see discussion below): the card reports kit-seeded reach. The /impact page answers "what difference has the kit made on wiki", and reach — work causally downstream of the kit — is that difference. Direct task completions remain valuable as *operational* metrics (completion rates, drop-off, task improvement) and belong to the task-level analytics of T429291: Improve the visualization of the Sankey user journeys analytics graph, not to this card. Every caption must still say what its number is ("seeded by the kit", "created by users the kit activated") — the scope is decided, honest labeling is not optional.
Kit-seeded reach, defined: work causally downstream of the kit even when the kit did not observe it happen, in two flavors —infrastructure reach (activity using something the kit installed: categories added with a kit-installed HotCat, articles transcluding kit-imported infobox templates) and user reach (activity by people the kit activated: categories added by users whose first HotCat use came via the kit, articles created by first-time creators the kit onboarded). Note the boundary: reach still counts only kit-connected people and kit-installed infrastructure — never a wiki's unrelated activity.
Per-tile reach definitions and their feasibility (the kit-side-sources table above keeps the direct sources: they are the seed sets and anchors the reach metrics are computed FROM, and tiles that count kit actions themselves — installs, imports, publishes — are identical under either scope):
| # | Tile | Reach metric | Feasibility |
|---|---|---|---|
| 1 | essential articles | mainspace articles created by kit users who were first-time creators (no creation before their kit anchor), plus CX-published articles by first-time translators (no contenttranslation-tagged contribution before the anchor) | good — the was-first-time predicate is one bounded usercontribs call per user (ucshow=new / uctag=contenttranslation, ucend=anchor, limit 1) and is a historical fact, cacheable forever; the ongoing count reuses getUserNewPages / the same tag query |
| 2 | messages translated | core-group translation edits made on translatewiki.net by kit-linked accounts after linking (usercontribs bounded at linked_at, intersected with the cached core message-key set — see the tile 2 section) | good — one paged query per linked account plus one shared, cached group enumeration. The direct metric (last_count − baseline) is already activation-anchored, so "same as direct" is a fine fallback; this variant additionally escapes the direct metric's 500-result API cap and last-translator churn (edit counts are monotonic — someone re-editing a message later doesn't erase the work) while staying core-only by design |
| 3 | policy pages | policy page created by any kit user, even without starting it from the kit (no open-event anchor) | good — same langlink + creator lookup as the direct metric, minus the anchor; see the tile 3 section for both definitions |
| 5 | HotCat categorizations | (a) all HotCat edits on wikis where the kit installed it, or (b) HotCat edits by users who first used HotCat via the kit | (b) good — per-user contrib scans for the distinctive HotCat edit summary (the kai audit used exactly this), cacheable; (a) heavy — per-wiki RC scans, no edit tag to filter by |
| 7 | infobox articles | articles transcluding kit-imported templates | already defined this way — tile 7 IS the reach pattern's template |
| 8 | main page | e.g. pageviews of kit-published main pages | possible (public pageviews API) but a different unit — needs discussion |
Common properties of every reach metric: external-API aggregation on a schedule with caching (never per dashboard load — same subsystem tile 7 already needs), and a seed set the kit records anyway (install events, twn_links, ledger users, imported-template titles). Design rule for the aggregation cache: store item rows (wiki, title, timestamp), not counts — every tile count is derived as COUNT(*) over its items, which is what makes the per-tile outputs drill-down (see the discussion) free and guarantees the headline always equals its list.
Activation-anchored reach (the unifying rule): the strong versions of these metrics don't count all work by kit users — they count work by users the kit *activated*: no prior work of that kind before their kit anchor. TWN's baseline already implements this at the account level (pre-link translations never credit); HotCat's user-reach variant is "first used HotCat via the kit"; tile 1's is "first article created / first translation published after meeting the kit". Suggested defaults (each a judgment call to confirm before implementation):
- Anchor = the user's first contact with the relevant task (first .open event / metadata / ledger row for that task on that wiki), falling back to contributors.first_seen_at. Task-contact is the stronger causal claim than mere login.
- Per wiki, consistent with contributors, baselines and everything else — a prolific creator on eswiki can still be an activated first-time creator on gnwiki.
- The predicate is immutable once computed (a historical fact), so it's evaluated once per (user, wiki) and cached forever; only the ongoing count refreshes on the aggregation schedule.
- Known blind spot: deleted contributions are invisible to usercontribs, so a user whose only pre-kit creation was deleted reads as first-time. Rare, biases slightly upward, accepted.
The rejected alternative — direct task completions
For the record, the dilemma this section used to pose: every tile can also be measured at the direct scope — only work the kit itself observed (verification-ledger runs, TWN baseline deltas, kit events). Its appeal was real: every source is a local query (no external-API aggregation subsystem, the card could ship immediately), and the numbers grow with genuine usage from a small base — tile 5 stood at 3 in production (the ledger's verified runs as of 2026-07-29) against a mockup value of 9,956; the mockup's three largest values (34,917 / 9,956 / 5,370) are only reachable at the reach scope.
Rejected for this card because it answers the wrong question here: "how many task runs did the kit observe" is an operational measure of the tasks, not of the kit's on-wiki impact — a wiki whose community adopted a kit-installed HotCat, or whose articles transclude kit-imported infoboxes, delivers value the direct scope is structurally blind to. The direct metrics are not discarded: they move to the task analytics of T429291: Improve the visualization of the Sankey user journeys analytics graph (completion rates, drop-off, per-task improvement), and inside this card they survive as the machinery — the ledger predicates, baselines and events are the seed sets and activation anchors every reach metric is computed from.
Per-wiki filter — not part of this card (T433618: Add a wiki filter to the /impact dashboard)
This card does not implement per-wiki filtering. The mockup subtitle's "filter by language code in the header" is delivered by T433618: Add a wiki filter to the /impact dashboard, which adds a page-wide wiki filter to the whole /impact dashboard; when it lands, this card inherits the filter like every other card on the page (its endpoint accepts the same wiki parameter as the rest — worth keeping in mind when writing this card's queries, but nothing here blocks on it).
The card therefore ships with the subtitle:
The detailed outputs behind the highlights, across all wikis.
Tile 2 — core-message translations, per-edit (windowed and exact) — DECIDED
Decided source — settled in this task's comments (the windowed preference and the closing tile-2 summary, {T433621#12175151}): a per-edit scan of the linked accounts' translatewiki.net contributions, filtered to the MediaWiki core message group. This satisfies both constraints from that thread at once — the tile follows the card's days selector like every other tile (windowed; the ≈10-step granularity caveat that originally made windowing a design question no longer exists on this source), and it counts exactly the kind of work the task asks for (core messages only) — while staying message-exact: each item is an individual core-message translation with its own timestamp, so the window selects single translations. Remaining design surface is presentational only (tile caption, drill-down layout).
Two API ingredients, composed by the aggregation:
- The per-account edit scan: list=usercontribs on translatewiki.net per linked account, ucnamespace=8 (every translation lives at MediaWiki:<message-key>/<language-code>), bounded at the account's linked_at — pre-link work is excluded by the bound itself. Incremental per scheduled run (continue from the last scanned timestamp).
- The core-group membership set: list=messagecollection&mcgroup=core, paginated via mcoffset — the complete core key list (a few thousand messages, a handful of requests), shared across all users, cached and refreshed on the aggregation schedule. Note this is the same API the task's own counter uses, minus its single-request mclimit=500 simplification — so the tile escapes the 500-message cap the in-flow read accepts.
An edit counts when its title parses as MediaWiki:<key>/<lang> with <key> in the cached core set. Items are stored as rows (the card-wide design rule), so the tile count is COUNT(*) in the window, the outputs drill-down lists the messages (each linking to its page/diff on translatewiki.net), and the window filters on the edit's own timestamp — when the work happened, the same convention the verification ledger uses.
Definition pins:
- Language scope — decided: any language. Every core-message translation by a linked account counts, as long as it happened after linking — the kit activated the translator, and all their core work is reach. Attribution follows the translation, not the link: a hausa translation by a kcg-linked user belongs to hausa. Concretely, each item row stores the target language code parsed from the title (MediaWiki:<key>/<lang> → <lang>), and the per-wiki filter (T433618, when it lands) matches items by the selected wiki's language code — not by the twn_links row's wiki_url, which remains only the seed that says whose contributions to scan.
- Unit — decided: distinct messages. One item per (message key, language) per account — the user's later edits to their own translation don't create new items. The item's timestamp is the first post-link edit of that message, so a message counts in exactly one window, windows are additive, and the all-time total equals the sum of the periods.
The seed and anchor come from T433428's twn_links (linked account + linked_at); its two direct derivations remain as supporting reads, not the tile:
- Fallback while an account's scan hasn't run yet (or translatewiki is unreachable): SELECT COUNT(*) * 10 FROM task_verifications WHERE task_key='translate-interface-messages' AND verified_at >= datetime('now', ?) AND <EXCLUSIONS_SQL> — windowed but decade-granular (≈10 per credited milestone); label with ≈ if ever shown.
- Cross-check (all-time only): SELECT SUM(MAX(0, last_count - baseline)) FROM twn_links WHERE baseline IS NOT NULL AND last_count IS NOT NULL (+ exclusions) — message-exact for the core group but a current-state snapshot with no time dimension.
Do NOT use the mark-done analytics event's translations property — it is frozen at the click and superseded by all of the above.
Scope caveats by source: the scan is core-only by design and monotonic (an edit is a historical fact — someone re-editing the message later doesn't erase it); the last-translator semantics and the 500-message cap apply only to the fallback/cross-check derivations above.
Tile 3 — count published policy pages on-wiki, not Done clicks
.complete events and task_completions rows only exist when the user clicks "Mark as done" — a translator who publishes the policy page via Content Translation and never returns is invisible to them. That is the same false negative the verified tasks eliminated, so the tile must read the wiki, not the click. Both scopes are defined on-wiki:
- Direct: the user started the translation from the kit — an open-cx cta_click for translate_policy_page anchors the run — and the policy page's langlink target was created by that same user, at or after the open (resolveLanglinkTarget('Wikipedia:Five pillars', <code>) then getPageCreator; same-user and created-at-or-after-open, i.e. exactly the create-essential-article verifier predicate pointed at a fixed title). No return visit needed; the Done click is irrelevant to the count.
- Reach (weaker): the policy page was created by any kit user, whether or not they started it from the kit — creator matched against the wiki's contributors, no open-event anchor.
- The task currently covers a single policy page (Wikipedia:Five pillars, tasks.js:79), so both counts are per-wiki ("wikis whose Five pillars a kit user created…"). If the task grows more policy pages, the same lookups apply per page.
- One langlink + one creator lookup per kit wiki — external API, so it rides the same scheduled cached aggregation as tile 7's transclusion counts. Tiny volume; the open-event anchor is a local query.
- Systematic fix, filed as T433617: register translate-policy-page in VERIFIABLE_TASKS — the direct predicate above IS the verifier that task specifies, so completion itself becomes on-wiki verified and ledger rows feed this tile for free. The tile does not depend on it — the aggregation works against today's schema — but if T433617 lands first, this tile simplifies to a ledger read like tiles 1 and 5.
Tile 7 — articles transcluding kit-imported infoboxes (DEFINED)
Definition (decided 2026-07-30): distinct mainspace articles, across all wikis, that transclude an infobox template the kit imported. This is what "articles given missing infoboxes" honestly means — not template imports (tile 6), but the articles that ended up using them.
Derivation:
- Which templates count: the local template pages the import-infobox tool created, recoverable per wiki from the import-template-success cta_click events ({ type, title } — title is the local template page). De-duplicate per (wiki, title).
- Count per template: MediaWiki API list=embeddedin (einamespace=0) on each wiki, counting distinct pages; union the page sets per wiki before summing so an article with two kit templates counts once.
- This is the card's only external-API tile, and transclusion counts change as wikis edit — so the number must be computed on a schedule and cached (a table or in-memory snapshot refreshed by the existing in-process sweep cadence), never per dashboard load. Politeness-delay the API calls.
Because of the aggregation+caching subsystem this can ship as a fast-follow to the card rather than blocking it: the card renders the tile once the aggregate exists (the acceptance criteria's omit-until-real rule covers the interim).
Scope note: this counts transclusions of kit-imported templates regardless of who added the infobox to the article — it is an infrastructure-reach metric (see the metric-scope section), and the template for how the card's other reach metrics should be built.
Refinement — resolve WHEN the infobox was added (decided 2026-08-01)
As first built, this tile stamps each item with its discovery time: list=embeddedin reports which pages transclude a template, never when the transclusion appeared. Two costs. The days window moves when a scan runs rather than when work happens, so everything found on the first pass lands in that pass's window. And the row carries no change tags, so unlike every other item it cannot say whether the work was done on mobile.
Both are fixable, because the revision history holds the answer:
- Bound the search. A transclusion cannot predate the template's import, whose timestamp the import-template-success event already gives. Fetch the page's revisions from that point: prop=revisions&rvprop=ids|timestamp|tags&rvdir=newer&rvstart=<import>.
- Find the revision that introduced it. action=parse&oldid=<revid>&prop=templates reports the templates of that revision. Scan the candidates linearly while the list is short (exact); binary-search above a threshold, documented as an approximation because add → remove → re-add is not monotonic and a binary search would find a later addition.
- Store what it found. That revision's timestamp becomes item_ts and its change tags become the row's tags. Both are immutable, so a page is resolved once and never revisited.
Insert only once resolved, which keeps the items table insert-only — no discovery-time row to update later. A page that cannot be resolved (RevDeleted content, a failed request) simply does not count yet and is retried on the next pass.
Cost: heavy once, then negligible. The first pass works through the existing backlog at one revisions call plus a handful of parse calls per page. Afterwards each pass sees only pages new to embeddedin, whose transclusion necessarily happened within the pass interval — so the candidate list is one or two revisions. A template imported after this ships never accumulates a backlog at all. Keep a per-(wiki, template) cursor and a per-pass page budget so one widely-adopted template cannot monopolise a pass.
Two details: match the template by page id, not title, so a later rename or redirect does not orphan the search; and a page that gains, loses and regains the infobox keeps its first origin — the metric is "when this page first got an infobox".
Sequencing: land this before the first production scan pass. Otherwise tile 7's rows exist with discovery timestamps and have to be deleted and re-derived — recoverable, since nothing about these rows is unique to the moment they were written, but avoidable.
What it buys beyond this card: tile 7's rows become tag-bearing and truthfully dated like every other item, which is what lets them join the /impact Key Highlights contributions tile and its mobile subset on the same terms as the rest.
Tile 10 — recruit-link account creations are not attributable (BLOCKED)
Goal: count accounts created on the wiki through the link the RecruitDialog shares.
Why it is blocked (analysis 2026-07-30): the shared link is <wiki>/wiki/Special:CreateAccount?utm_source=starterkit&utm_medium=<platform> (RecruitDialog.vue:130-135). MediaWiki ignores utm_* parameters — they are recorded nowhere a Toolforge tool can read. The mechanism that DOES attribute account creations to a link is the campaign= parameter on Special:CreateAccount, which lands in the ServerSideAccountCreation EventLogging stream — WMF-internal analytics (Data Lake), not exposed by any public API. The public list=logevents&letype=newusers shows account creations with no referrer/campaign data. So there is no automatable way for the tool to know which creations came from the kit's link.
Two paths, recorded for whenever this is picked up:
- Internal-analytics route (accurate, not automatable): add campaign=starterkit-recruit to the recruit URL (one-line change, worth doing regardless — it starts accumulating attributable data from the moment it ships), and query ServerSideAccountCreation internally for periodic numbers. Those numbers cannot feed a live tile (the card's no-hardcoded-values rule), but they answer the question for reports.
- Kit-redirect proxy (automatable, weaker): route the shared link through a kit endpoint that logs an analytics event and 302s to Special:CreateAccount. That yields recruit-link arrivals (clicks by recruits, a real funnel stage beyond tile 9's shares-initiated) but still not completed account creations.
Until one of these is chosen, the tile is omitted (per the acceptance criteria).
Acceptance criteria
- New card renders at the bottom of /impact, titled "Essential work done through the kit" with the agreed subtitle, matching the mockup's two-row five-tile layout and per-tile accent colours
- Each shipped tile is backed by a real query — no hardcoded or mocked values
- Every tile honours the existing days window and binds EXCLUSIONS_SQL + ...ADMIN_USERS, so test wikis and analytics admins are excluded in production exactly as the other cards do
- Tiles whose definition is unresolved are omitted, not shipped with placeholder numbers — the precedent is already in KeyHighlights.vue, which ships three tiles and documents the deferred two in a comment
- Values use the dashboard's existing number formatting — the mockup's 34.917 dot separators are a locale-formatting question, not something to reproduce literally from the image
- Card is visible to all /impact viewers; no tile exposes usernames (no maintainer-only gating needed unless drill-downs are added)
- i18n: en.json + qqq.json only (translated files are bot-synced)
- Responsive: tiles wrap on narrow viewports rather than overflowing
Implementation notes
- Tile 2 additionally depends on T433428 (feat/T433428-twn-repeatable, stacked on !153) merging — its twn_links table and milestone ledger rows are the tile's data source.
- Tile 3 is best sequenced after T433617 (policy-page verification): its ledger rows make the tile a local read and the bespoke langlink/creator lookup unnecessary. Not a hard dependency — see the tile 3 section.
- Branch off feat/T433369-phase5-dashboard (!152), not main. !152 already edits server/db/impact.js (getTaskCompletionsDetail) and server/db/journey.js, and tiles 1 and 5 read the task_verifications ledger that !152 teaches the dashboards to use. Branching off main means resolving that conflict twice.
- Server: one new aggregate in server/db/impact.js beside getTaskCompletionsDetail, returning all tiles in a single object; one new route in server/routes/impact.js following the existing pattern (clampDays, sinceParam).
- Client: new client/src/components/impact/EssentialWorkCard.vue mirroring KeyHighlights.vue (accent span + value + caption, formatValue, days prop, fetch on mount and on days change); mounted last in ImpactDashboard.vue.
- One endpoint for all ten tiles rather than ten calls.
- Event-derived tiles should reuse the shared definitions in server/db/journey.js (completedTaskForEvent, CTA_COMPLETION) where they apply, so the card cannot drift from what the rest of the dashboard calls "done".
Open questions (blocking for the affected tiles only)
Metric scope— RESOLVED: kit-seeded reach (see the metric-scope section, which also records the rejected direct-completions alternative and why).- Tile 10 unblocking — pick a path from its section (internal-analytics via campaign=, or the kit-redirect arrivals proxy), or drop the tile. Adding campaign=starterkit-recruit to the recruit URL is worth doing immediately either way, so attributable data starts accumulating.
Update (2026-08-06): this ledger now also defines the /impact "Policy pages added" tile.
As of !173, the verification ledger introduced here is the source for tile 3 of the essential-work card on /impact (T433621: Add an "Essential work done through the kit" stats card to the /impact page). That tile previously counted the wiki's Five-pillars langlink target whenever its creator was any contributor of that wiki, with no anchor of any kind — so it credited pages created before the kit existed (production carried a 2023 creation on a wiki whose author only became a kit user later) and disagreed with this task's own ledger, reading 4 against 2.
Both the tile and its drill-down now read task_verifications for translate-policy-page, so "the kit caused this policy page" has a single definition across /impact, /analytics and the task's own completion state: the user opened the translation from a kit CTA and the creation was then verified as theirs. The scanner that used to produce the tile's rows is retired, and the drill-down links the localized page the run actually produced rather than the constant English source.
One consequence worth recording at the source: the card's other reach tiles count downstream work by kit-activated users without requiring each individual artifact to originate from a kit click (essential articles, HotCat-categorized articles, infobox transclusions). Tile 3 is now the only one that requires per-item provenance from the kit. That is stricter than the card's general reach rule, and it is precisely what makes the tile agree with this task.
