Reach means the kit caused the work — never "a contributor who happens to use the kit did some work". Two cases, and the kit's own definition of activation splits them cleanly:
- Newcomer to this kind of work. A user with no work of that kind before their kit anchor, who then completes a run started from the kit, is activated: the kit made them a person who does this. From that point ALL their work of that kind is reach, whether or not each individual piece went through the kit.
- Already experienced. A user who was already doing this before the kit gets credited only for runs started from inside the kit. The kit did not make them a creator; it did cause the specific pieces it sent them into.
Case 1 is already implemented (getActivation in essential-work-scan.js, cached permanently per (wiki, user) because "did they do this before their anchor" is a historical fact). Case 2 is implemented nowhere.
The defect
The scanners gate on activation and then simply stop:
if (!act?.activated) continue;
An experienced contributor therefore contributes nothing at all to the reach tiles. Not "only their kit-started runs" — zero. The card silently under-reports exactly the users most likely to be a wiki's core contributors.
Observed on production this month, start to finish. A contributor opened the HotCat task from the kit, categorised an article, and returned to the kit to click Complete. The kit verified that edit in-flow and wrote the ledger row itself (task_verifications, source check); task_completions records the run. Tile 5 shows none of it — the activation probe found HotCat edits of theirs from about three weeks earlier and marked them activated: false, so the scanner skipped the user and every edit they have made. Each component did its job; the tile simply never asks the case-2 question.
Scale of the exclusion on production today:
| tile | users decided | activated (counted) | excluded (contribute 0) |
|---|---|---|---|
| 5 · HotCat articles | 4 | 1 | 3 |
| 1 · articles created | 85 | 58 | 27 |
Meanwhile the ledgers already hold 5 install-gadget and 2 create-essential-article verified runs — kit-initiated work that exists, is attested, and is currently counted by nothing.
The record needed to fix it already exists: task_verifications holds precisely the runs a user started from a kit CTA and that were then verified on-wiki. That IS "initiated from inside the kit"; it is what tiles 3 and 6 already read, and what T433819 established as the pattern (prefer the attested record over a scan).
Scope
| tile | today | change |
|---|---|---|
| 1 · articles created | case 1 only; experienced users → 0 | add case 2 from the create-essential-article ledger |
| 5 · articles categorised (HotCat) | case 1 only; experienced users → 0 | add case 2 from the install-gadget ledger |
| 3 · policy pages | case 2 only (one page per wiki, nothing to unlock) | none — done |
| 2 · interface messages | case 2 by construction (counts from the kit connect forward) | none |
| 4 · gadget installs · 7 · infobox articles · 8 · main pages | the kit performed the write, or the edit is a third party's | none — no user-activation question |
Design
Materialise ledger runs as item rows; do not make the tile read two sources. If the tile stayed a plain COUNT(*) over essential_work_items, everything downstream — the drill-down identity, the contributions rollup, the mobile subset — keeps working untouched. A tile whose headline reads one source while its item rows say another is the bug class already hit twice (502621c on main pages, and again when tile 3 was re-sourced): if a headline stops counting item rows, that tile must leave ITEM_CONTRIBUTION_TILES, or an orphaned mobile-tagged row can push the mobile figure past the headline. Writing items avoids the whole question.
The overlap is real and already in the data. For an activated user, a kit-started run is recorded twice — once as an install-gadget ledger row and once as a hotcat-articles item, with the same page and the same timestamp. Production currently holds at least one such pair. So the materialiser must write with the same key shape the scanner uses — UNIQUE(tile, scope, title, subject) — and let INSERT OR IGNORE absorb the duplicate. Getting this wrong double-counts activated users.
Tags / the mobile signal. Ledger rows carry rev_id but no change tags. A materialiser can fetch tags for that revision (the same read scripts/backfill-item-tags.js performs) and keep the mobile signal intact; skipping that leaves tags NULL, which the positive mobile predicate treats as "not captured" — correct, but an under-count. Prefer fetching.
Windowing and exclusions are unchanged: verified_at is the qualifying revision's own timestamp, directly comparable to item_ts, and the item's username carries the production admin/test-wiki exclusions as for every other row.
Acceptance criteria
- An experienced user's kit-started run appears in the tile, dated by the qualifying revision.
- That same user's work of the same kind done outside the kit does NOT appear.
- An activated (case 1) user's counts are unchanged — no double-count where a kit-started run is already an item.
- Headline == drill-down list length, at every window.
- contributionsOnMobile <= contributions still holds; state explicitly whether ledger-materialised rows carry tags.
- Existing production exclusions apply unchanged.
- Historical ledger runs are picked up (the ledgers already carry backfilled history) — no separate ops script.