--- name: find-data description: "Find a dataset for a Data2Story blog. Accepts a topic, a URL, or a DIP-style category. Downloads + validates against 4 completeness gates before handing off to /data2story-pro. Local-first: searches Economist/Pudding/TidyTuesday clones before going online. Supports --validate-only to audit a folder you already have." argument-hint: " [--mode single|theme] [--source Economist|Pudding|tidytuesday] [--out ./datasets/] [--validate-only]" allowed-tools: Bash(python:*), Read, Write, Glob, Grep, WebSearch, WebFetch, AskUserQuestion --- # find-data Turn an idea, URL, or category into a `phase2/datasets//` folder that the `/data2story-pro` pipeline can run on without crashing. You are the **gatekeeper before the 7-agent newsroom**. Detective, Analyst, Editor, Designer, Programmer, Auditor, Inspector all assume the data is already there, parseable, and provenanced. Your job is to make sure that's true before they start. Refuse to mark a folder ready until it passes 4 gates. See `references/completeness_gates.md` for the criteria. The gates are codified in `tools/audit.py`, not in this prose. ## Prerequisites Python deps for the `tools/`: - **`pandas`** — required (`audit.py` reads/inspects CSV/JSON). - **`openpyxl`** — required **only** when a source is `.xlsx` (`audit.py`'s `pd.ExcelFile`); a mid-run missing-`openpyxl` is the usual cause of an XLSX audit error. - **`urllib`** — stdlib, no install (`fetch.py` downloads, `audit.py` HEAD-checks). One install line: ```bash pip install pandas openpyxl ``` After editing anything under `tools/`, run the no-network regression suite: `py tools/selftest.py` (exit 0 = pass). ## Resolve paths first - `SKILL_DIR` = directory containing this `SKILL.md` - `WORKSPACE` = ancestor that contains `phase2/datasets/` (the parent repo root), if one exists - `DATASETS_ROOT` = `WORKSPACE/phase2/datasets` when `WORKSPACE` exists; otherwise `./datasets` (clone-relative, under the current working dir) — the open-source clone has no `phase2/datasets` - `BLOGS_ROOT` = `WORKSPACE/phase2/blogs` when `WORKSPACE` exists - `OUT_DIR` default = `DATASETS_ROOT/` (so `./datasets/` on an OSS clone). Callers may override with an explicit `--out`, which always wins. - `INPUT` = first positional argument from `$ARGUMENTS` - Parse flags from `$ARGUMENTS`: `--mode`, `--source`, `--out`, `--validate-only` Never hard-code machine paths. Resolve at runtime by walking up from `SKILL_DIR`. ## Step 0 — Classify the input Decision tree (the FIRST condition that matches wins): 1. **`--validate-only` flag present** → MODE = `validate-only`. `INPUT` must be an existing folder path. 2. **`INPUT` is an existing folder path** (and `--validate-only` absent) → still ask the user before re-fetching; default to `validate-only` behaviour with a confirmation. 3. **`INPUT` starts with `http://` or `https://`** → MODE = `url`. 4. **`INPUT` (case-insensitive) exactly matches a DIP category** from `phase2/datasets/data_is_plural/dip_category_summary.md` → MODE = `category`. 5. **Otherwise** → MODE = `topic`. State your classification out loud in one line before doing anything else. **No local corpora (open-source clone):** on a machine without the `Economist/`, `Pudding/`, `tidytuesday/` clones and without the Data-is-Plural CSV under `phase2/datasets/`, `browse_local.py` and `dip_query.py` both return empty (no crash). In that case there is nothing to match locally, so `topic`/`category` discovery degrades straight to WebSearch (Step 1). No flag is needed — detect it from the empty `browse_local.py` + `dip_query.py` results. Keep the local-first path intact for machines that DO have the corpora. ## Step 1 — Branch on mode ### validate-only mode Just audit. Skip discovery, skip fetch. ```bash python "SKILL_DIR/tools/audit.py" "" ``` This writes `/validate.json` and prints the gate summary. Read it back with `Read`, surface the verdict, and stop. If `overall.ready_for_data2story` is true → print the `/data2story-pro ` command for the user to copy. If it's false → list the specific gate failures and recommend remediations. ### url mode Determine OUT_DIR (use `--out` if given, else derive a slug from the URL path's last meaningful segment, store under `DATASETS_ROOT//` — i.e. `./datasets//` on an open-source clone with no `phase2/datasets`). Branch on URL shape: | URL pattern | Tool | |---|---| | `github.com/{owner}/{repo}/tree/{branch}/{path}` | `python fetch.py github-folder ` | | `github.com/{owner}/{repo}/blob/{branch}/{path}` | Same (fetch.py auto-detects blob) | | `github.com/{owner}/{repo}` with no path | Refuse — tell user to narrow to a folder | | Anything else | `python fetch.py url ` | `fetch.py` is source-aware: if the URL points to `TheEconomist/graphic-detail-data`, `the-pudding/data`, or `rfordatascience/tidytuesday` AND the local clone already contains that path, it copies from the local clone (no network). Otherwise it fetches via the GitHub API. This happens automatically — do not handle it yourself. #### URL normalization (share-page vs direct-download) Several hosts serve a human-facing **page** at the obvious URL, not the raw bytes, so a naive download returns HTML instead of a `.csv`/`.json`/`.xlsx`. `fetch.py` auto-rewrites GitHub `/blob/` URLs to `raw.githubusercontent.com` and, as a backstop, **warns on stderr** if a data-extension download comes back as an HTML page (`` / `//blob//` | auto-handled → `raw.githubusercontent.com////` | | Google Drive | `drive.google.com/file/d//view` | `drive.google.com/uc?export=download&id=` | | Dropbox | `...?dl=0` | swap to `...?dl=1` (or `dl.dropboxusercontent.com`) | | OneDrive | share link (`1drv.ms/...`) | append `&download=1` to the direct link | | Kaggle | dataset page (`kaggle.com/datasets/...`) | the file API / CLI download — the page is not a file | If a download trips the HTML warning, you almost certainly handed `fetch.py` a share/page URL — fix the URL to its direct-download form and re-fetch. After fetch: check whether the folder has a README.md. - If yes → leave it alone, jump to Step 2. - If no → generate one from `tools/README_template.md`. Auto-fill what you can (title from folder name, column headers from the first CSV, source URL from the manifest). Leave the codebook `definition` cells as `{TODO}` for the user to fill in. Tell the user explicitly that README is a stub. ### category mode `INPUT` is one of the 14 DIP categories. Run both browse and DIP query in parallel: ```bash python tools/browse_local.py "" --top 10 python tools/dip_query.py --category "" --top 10 ``` Both emit JSON. Merge: - Local candidates (already-cloned folders) score higher than DIP leads (which still need fetching). - For each local candidate, run a cheap pre-score (does Gate 1 trivially pass — is there ≥ 1 CSV in the folder?). Present top 5 to the user as a numbered list via `AskUserQuestion`: - For each candidate, show: source · title · date · top-3 files · 1-line readme excerpt - Options: pick 1–5, or "all" (theme mode), or "skip" (abort) Then route the pick: - Local candidate → copy folder into OUT_DIR (or in-place audit if user accepts). - DIP lead → extract URL from the candidate's `links` array. If multiple URLs, pick the one with a data-file extension (csv/xlsx/json) or the GitHub one. Route to url mode for that URL. ### topic mode Same as category mode but use full-text query: ```bash python tools/browse_local.py "" python tools/dip_query.py "" --top 10 ``` If BOTH return empty (no local hits, no DIP hits), only THEN do WebSearch: ` open dataset CSV site:ourworldindata.org OR site:github.com OR site:data.gov`. Cap web results at 5. Surface them with a "no local matches found, querying web" disclaimer. From the WebSearch results, pick the one whose URL ends in a data-file extension (`.csv` / `.xlsx` / `.json` / `.tsv`) or is a `github.com/{owner}/{repo}/tree/{branch}/{path}` folder. Route that URL through url mode: `python tools/fetch.py url ` for a direct data file, or `python tools/fetch.py github-folder ` for a GitHub folder. Then continue to the README step (Step 2) and the audit step (Step 3) exactly as in url mode — this closes the topic → web → fetch → audit loop. If no result has a usable data URL, report that honestly and do not fabricate one. ## Step 2 — Optional: generate / repair the README After fetch, the OUT_DIR may or may not have a README. Decide: - README present + has codebook table + has source mention → leave alone - README missing → generate from `tools/README_template.md` - README present but no codebook table → ADD a codebook section, do not overwrite the existing prose When generating, fill in: - `{TITLE}`: human-readable from folder slug (snake_case → "snake case", title-cased) - `{PRIMARY SOURCE URL}`: from `manifest.json` items' urls - `{filename.csv}` rows: scan each CSV's columns - `{Codebook}`: list each column with `{TODO: definition}` for unknown ones; you may guess from column names (`launch_year` → "year of launch (integer)") but flag guesses with a `{?}` prefix so the user can correct. ## Step 3 — Audit (the 4 gates) ```bash python "SKILL_DIR/tools/audit.py" "" ``` This writes `/validate.json`. Read it back. For Gate 4 (multimodal — advisory only), `audit.py` cannot judge by itself. Do your own quick check after reading `validate.json`: - From the CSV columns and entity samples, classify subjects: - **Concrete** (people / places / species / events / specific objects) — e.g., "launches.csv has agency names like SpaceX, NASA, Roscosmos → concrete" - **Abstract** (rates, indices, generic counts) — e.g., "GDP per capita time series → abstract" - For concrete subjects, do ONE WebSearch with `site:commons.wikimedia.org` to verify at least 1 reference photo exists for a representative subject. Do NOT download photos — just verify they exist. That's Detective's job. - Write your Gate 4 finding back into `validate.json` under `gates.multimodal.info`. ## Step 4 — Verdict Read the final `validate.json`. Print a concise summary: ``` === find-data verdict === folder: files: [OK ] Technical [WARN] Story material - : only 47 rows (< 50) [OK ] Provenance [INFO] Multimodal: concrete subjects (agencies, rocket types) overall: READY next: /data2story-pro ``` If overall is BLOCKED, do NOT print the `/data2story-pro` command. Instead print: ``` overall: BLOCKED to fix: ``` Suggest concrete fixes: - "README missing → run /find-data --validate-only after adding README" - "Primary URL dead → find an alternate source via /find-data --source-finder" - (or open-ended: "drop a corrected file into and re-run") ## Step 5 — Save a digest Append a one-line entry to `DATASETS_ROOT/_find-data-log.md` (create if not present) so the user has a running history: ``` - 2026-05-25T14:00Z · single · datasets/ · READY · invoked via URL ``` ## Constraints - **Never overwrite an existing README.md without asking.** Generate-from-template only fires when README is absent. If README exists but is thin, ADD sections, don't replace. - **Never modify the skill repo itself.** Your only writes are inside the dataset output directory (`DATASETS_ROOT//`) and the digest log. - **Never fabricate a source URL or license.** If you can't find a real source for a generated README, leave the `{PRIMARY SOURCE URL}` placeholder and warn the user. - **Cap WebSearch at 5 results, cap WebFetch at 3 calls per invocation.** This is a gating skill, not a research tool — depth is Detective's job. - **Do not run /data2story-pro yourself.** Your last word is the verdict + the command for the user to copy. Hand-off is manual. ## Reference files - `references/completeness_gates.md` — exact pass/warn/fail criteria for each gate - `references/input_mode_dispatch.md` — full URL classification table - `references/examples/good_economist.md` — what `single` mode output should look like - `references/examples/good_theme.md` — what `theme` mode output should look like - `tools/audit.py` — deterministic gate evaluator - `tools/fetch.py` — source-aware downloader (manifest / github-folder / single-url modes) - `tools/browse_local.py` — index Economist/Pudding/TidyTuesday for candidate matching - `tools/dip_query.py` — filter dip_categorized.csv by category or topic - `tools/README_template.md` — graphic-detail-style skeleton for auto-generation