--- name: review-data-pr description: Review an OWID ETL data update PR end-to-end — runs the pipeline, compares snapshot fields against the previous version, verifies links, audits indicator metadata coverage, and cross-checks workflow items from /update-dataset. Trigger when the user asks to "review this PR", "review the data PR", or invokes this on an open dataset-update branch. metadata: internal: true owner: paarriagadap --- # Review Data PR End-to-end review of a dataset-update PR. Goes deeper than `/review`: actually runs the steps, compares to the previous version, audits metadata coverage against a fixed checklist, and reports on `/update-dataset` workflow status (Slack draft, Codex review, indicator upgrade, downstream deps). > **Paired skill — keep in sync.** [`/update-dataset`](../update-dataset/SKILL.md) is the author-side counterpart of this skill: the steps it defines are the outcomes verified here. Whenever you add, remove, or change a check in this file, check whether `update-dataset/SKILL.md` needs a matching author-side step (and add it in the same commit if so). The reverse also holds — see the mirror note there. The creation-side skills [`/create-dataset`](../create-dataset/SKILL.md) and [`/create-snapshot`](../create-snapshot/SKILL.md) belong to the same family: the checks here (§5 snapshot fields, §6 links, §7 code clarity, §9 metadata coverage, §10 quality) also gate PRs produced by `/create-dataset`, so when one of them changes, check whether the create skills need a matching edit in the same commit too. ## Inputs - Optional PR number. If omitted, derive it from the current branch via `gh pr list --head `. ## Workflow ### 1. PR metadata ```bash gh pr view --json title,body,isDraft,mergeable,statusCheckRollup,comments,reviews ``` Flag if **PR description is empty** (per user's standing rule: keep PR body in sync with substantial changes). Flag 🟡 if the Summary doesn't open with a **tracking-issue link** (`Tracks: owid/owid-issues#NNNN`) — `/update-dataset` requires it as the first line; most data updates have a corresponding `owid-issues` ticket. ### 2. Diff and changed files ```bash gh pr view --json files --jq '.files[] | "\(.additions)+ \(.deletions)- \(.path)"' ``` For very large diffs (>1MB) skip `gh pr diff` and read the changed files directly with `Read`. ### 3. Locate the new dataset From the changed files, identify: - New snapshot path: `snapshots///..dvc` (a `.py` upload script is **optional** — `.dvc` + `url_download` or a local file path is enough) - New step files: `etl/steps/data/{meadow,garden,grapher}///.{py,meta.yml}` - Old version (from `dag/archive/*.yml` or by grepping for the same ``) ### 3b. Update shape — version bump vs restructure **Open with a short pipeline brief.** The reviewer is often seeing this dataset for the first time. Run the same overview that `/update-dataset` step 0b uses on the **new** version. Run it on the old version too while it's still active in the PR's DAG (the script only reads active steps), and compare the two chains: ```bash .venv/bin/python .claude/skills/update-dataset/scripts/pipeline_overview.py // .venv/bin/python .claude/skills/update-dataset/scripts/pipeline_overview.py // # if still active ``` Give the user about 5–8 lines: the chain, what each non-trivial step does, external inputs, consumers, and who did the previous update (`git log --diff-filter=A` on the old garden script, if the script can't reach it). Then compare against the author's `Pipeline structure: …` line in the PR Summary. If the line is missing, flag 🟡. If it says "unchanged" but any trigger below holds, flag 🔴. Before running the pipeline, classify the PR. If any of the following are true, you're reviewing a **restructure**, not a version bump, and several downstream checks apply differently: - The `short_name` changed (old version uses one name, new version uses another). - The schema changed (wide ↔ long, different file format with a different column set, new dimensions). - The set of policies/indicators changed substantially (splits, dropped composites, newly added areas). - Score semantics changed (e.g. binary → continuous 0–1, units/scale changed). When it's a restructure: - **Don't expect the auto-Indicator-Upgrader to have remapped charts.** When short_names differ entirely, the upgrader has nothing to match on. Look for a hand-curated v1 title → v2 title mapping table in the PR description (or a follow-up PR thread). 🟡 if charts on the old chain are still published but no mapping plan exists. - **Don't expect a `.py` step copy from the old version.** Step files should be authored from scratch, not produced by `etl update` rename. If the new step files look mechanically renamed (same logic, just version-bumped strings), flag 🟡 — the author may have skipped restructure-specific decisions. - **A chart remapped onto a successor indicator needs a config-vs-shape check.** Verify its pinned `selectedEntityNames` exist in the successor's data (v1 regional aggregates often don't — expect the garden step to rebuild them, mirroring the retired step's method), that pinned `yAxis` bounds don't clip the new range, and that the subtitle doesn't still describe the old construction. Any of the three broken: 🔴 (the default view renders empty, clipped, or mislabeled). - **Slack + `/latest` drafts are not expected in the PR body at all.** `/update-dataset` keeps them in the author's `workbench/` (steps 9 / 9b, owned by `/draft-data-update-slack-post` and `/owid-staff:draft-data-update-post`), so their absence from the PR is correct — don't flag it. ### 4. Run the full pipeline end-to-end ```bash .venv/bin/etlr data://grapher/// .venv/bin/etlr grapher://grapher/// --grapher --force --only ``` The `--grapher` upload is required to verify MySQL ingestion and to enable later checks (chart count, indicator upgrade verification). Confirm: - All four steps run cleanly (snapshot pulled from S3 if `.dvc` is committed, otherwise re-fetched) - MySQL upload returns a `dataset id` and shows variable upserts - No errors / no empty tables **Shortcut: read DB checks off the populated staging server.** OWID provisions a `staging-site-` server (via Buildkite) that runs the ETL chain and uploads to its MySQL. Once it's built, you can read the DB-dependent checks (chart count, `attributionShort`, rendered titles/Jinja coverage, indicator-upgrade, ghost variables) straight off staging instead of re-running `--grapher` locally — which also avoids re-triggering step side-effects (e.g. a grapher step that exports to Google Sheets). **Confirm the staging ETL build actually ran and finished** before trusting it: query `staging-site-` for the new dataset's variables (they exist) **and** check the `owidbot` PR comment shows a chart-diff block (✅) — that comment is produced *after* the staging build. ⚠️ Do **not** use the GitHub **`build-and-deploy`** check as that signal — it's the *docs* Cloudflare Pages deploy (`.github/workflows/deploy-docs-cf.yml`: `make docs.build` → deploys `site/`), with **no** ETL chain or Grapher upload, so a green `build-and-deploy` says nothing about pipeline correctness or the data DB. Reserve a local build for what the staging DB can't answer — chiefly **entity-level canonicalization (§8c #2)** (data lives outside MySQL). If you can't confirm staging is populated, run the pipeline locally per the steps above, and say in the report whether correctness rests on the staging build or a local run. **Review the actual PR head, not a stale local checkout.** The local branch can lag `origin` (or carry an in-progress merge). Before reading step files locally, `git fetch` and confirm your tree matches the PR head — `git diff HEAD origin/ --stat` should be empty, and `gh pr view --json headRefOid` should match `git rev-parse HEAD`. `gh pr view --files` / `gh pr diff` and the staging DB always reflect origin; local `Read`s do not. If they diverge, sync (or review via `gh pr diff`) before trusting local files. ### 5. Snapshot field comparison Read both `.dvc` files (old and new) and produce a side-by-side table for these fields: | Field | Check | |---|---| | `title` | Reasonable update if scope changed | | `description` | Updated to reflect new source / scope | | `date_published` | **Should normally differ from `date_accessed`** — source from `url_main` or the file. Equality is legitimate only as the documented fallback when no producer release date is discoverable (e.g. a scraped page carries fresh rows but no updated stamp — see `/update-dataset` Guardrails, "Scraped chart embeds"); expect a `.dvc` comment explaining it, and flag 🟡 for the author to confirm rather than 🔴. Bare equality with no rationale: ask. | | `date_accessed` | Updated to today (or run-date) | | `producer` / `attribution_short` | Same source, same values (unless changed deliberately) | | `citation_full` / `attribution` | **Year bumped to the new release year** — `etl update` copies both verbatim from the old `.dvc`, so a stale year ships silently. 🔴 if still the old version's year. | | `citation_full` year vs `date_published` year | **Warn (🟡) if they differ.** The year inside `citation_full` (and `attribution`) should normally match `date_published`'s year. A mismatch is sometimes legitimate — the producer labels the release by *edition* rather than publish date (e.g. UN IGME's "2025 report" published `2026-03-17`, so `citation_full` `(2025)` ≠ `date_published` `2026`) — but it's just as often a stale citation the author forgot to bump. Surface it for the author to confirm; don't silently pass it. | | `url_main` | Status check — see step 6 | | `url_download` | Status check; OK to remove if data is now fetched via API | | `license.url` | Status check | | `version_producer` | **Unchanged label + changed payload = in-place revision.** If the producer's version label is the same as the old `.dvc` but the data changed, confirm the author verified the revision against the source's file-modification dates/hashes (not the label) and documented the behavior in a `.dvc` NOTE; `date_published` should be the replacement date. Missing NOTE on a known in-place reviser: 🟡. | - **Freshness check for scraped snapshots.** When the snapshot `.py` scrapes the producer's page or a chart platform's endpoint, re-fetch the *producer's page* and compare against the committed snapshot — the endpoint the script reads can lag the page (e.g. a Datawrapper chart CDN trailing the page's own `