Skip to content

Commit 5699814

Browse files
committed
Initial release of Raincloud (v0.1.0)
Raincloud is a client-reproducible pipeline for building a curated catalog of 249 public datasets as analytics-ready Parquet + Vortex files. See README.md for the user-facing overview, AGENTS.md for the architecture, SKILLS.md for procedural playbooks, and CHANGELOG.md for the v0.1.0 release contents. License: Apache-2.0.
0 parents  commit 5699814

100 files changed

Lines changed: 143393 additions & 0 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.agents/README.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# .agents
2+
3+
Everything an AI coding agent needs to operate inside this repo. The `.claude → .agents` symlink at the repo root means tooling that follows either naming convention (Claude Code, Codex, Cursor, …) sees the same files.
4+
5+
## What's here
6+
7+
| Path | Role |
8+
|---|---|
9+
| [`settings.json`](settings.json) | **Tracked** allow-list of safe, read-only Bash / git / WebFetch / pipeline commands. A fresh-clone agent gets these pre-approved so it doesn't burn turns on permission prompts. Side-effecting stages (build / fetch / extract / convert / tighten_variant) are intentionally *not* pre-approved here. |
10+
| `settings.local.json` | **Gitignored** per-machine override — additional permissions specific to the local agent's session. Don't commit. |
11+
| [`skills/`](skills/) | 16 invokable skills following the [Agent Skills](https://agentskills.io) standard — wrappers around pipeline entrypoints (`/raincloud-build`, `/raincloud-fetch`, `/raincloud-status`, `/raincloud-validate-manifest`, `/raincloud-list-datasets`, …) and procedural playbooks (`/raincloud-add-dataset`, `/raincloud-add-handler`, `/raincloud-debug-build`, …). See [`skills/README.md`](skills/README.md). |
12+
| [`context/`](context/) | Symlinks back to the repo-root canonical docs (`AGENTS.md`, `SKILLS.md`, `README.md`, `sources.schema.md`) so each `SKILL.md` can pull authoritative guidance via a stable relative path without copying. |
13+
| `scheduled_tasks.lock` | Gitignored — agent-runtime state. |
14+
15+
## Where to start (fresh-clone agent)
16+
17+
1. Read [`../AGENTS.md`](../AGENTS.md) (auto-loaded via `../CLAUDE.md → AGENTS.md` symlink) for the invariants.
18+
2. Run `python -m scripts.pipeline.status --fast --missing-only` to verify the env.
19+
3. Run `python -m scripts.pipeline.validate_manifest` to confirm the manifest is well-formed.
20+
4. Browse [`skills/README.md`](skills/README.md) for the catalog of invokable commands.
21+
22+
## Adding or editing a skill
23+
24+
See [`skills/README.md`](skills/README.md#adding-or-editing-a-skill) — frontmatter schema, the `disable-model-invocation` flag for destructive skills, and the `allowed-tools` pre-approval pattern.

.agents/context/AGENTS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../../AGENTS.md

.agents/context/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../../README.md

.agents/context/SKILLS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../../SKILLS.md

.agents/context/sources.schema.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../../sources.schema.md

.agents/settings.json

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
{
2+
"$comment": "Tracked agent settings — safe, read-only defaults shared across fresh clones. settings.local.json sits next to this file (gitignored) and overrides per-machine. Side-effecting pipeline stages (build, fetch, extract, convert, tighten_variant) are intentionally NOT pre-approved here — those skills carry disable-model-invocation: true and confirmation lives in AGENTS.md.",
3+
"permissions": {
4+
"allow": [
5+
"Bash(python -m scripts.pipeline.status *)",
6+
"Bash(python -m scripts.pipeline.status)",
7+
"Bash(python -m scripts.pipeline.validate_manifest *)",
8+
"Bash(python -m scripts.pipeline.validate_manifest)",
9+
"Bash(python -m scripts.pipeline.list_datasets *)",
10+
"Bash(python -m scripts.pipeline.list_datasets)",
11+
"Bash(python -m scripts.pipeline.docs *)",
12+
"Bash(python -m scripts.pipeline.docs)",
13+
"Bash(python -c *)",
14+
"Bash(python3 -c *)",
15+
"Bash(.venv/bin/python -c *)",
16+
"Bash(.venv/bin/python -m scripts.pipeline.status *)",
17+
"Bash(.venv/bin/python -m scripts.pipeline.validate_manifest *)",
18+
"Bash(.venv/bin/python -m scripts.pipeline.docs *)",
19+
"Bash(uv sync*)",
20+
"Bash(uv run python -m scripts.pipeline.status *)",
21+
"Bash(uv run python -m scripts.pipeline.validate_manifest *)",
22+
"Bash(uv run python -m scripts.pipeline.docs *)",
23+
"Bash(git status*)",
24+
"Bash(git diff*)",
25+
"Bash(git log*)",
26+
"Bash(git show*)",
27+
"Bash(git branch*)",
28+
"Bash(git remote*)",
29+
"Bash(git ls-files*)",
30+
"Bash(git rev-parse*)",
31+
"Bash(git blame*)",
32+
"Bash(ls *)",
33+
"Bash(wc *)",
34+
"Bash(du -sh*)",
35+
"Bash(df -h*)",
36+
"Bash(file *)",
37+
"Bash(head -*)",
38+
"Bash(tail -*)",
39+
"Bash(jq *)",
40+
"WebFetch(domain:github.com)",
41+
"WebFetch(domain:api.github.com)",
42+
"WebFetch(domain:raw.githubusercontent.com)",
43+
"WebFetch(domain:agentskills.io)",
44+
"WebFetch(domain:code.claude.com)",
45+
"WebFetch(domain:docs.astral.sh)",
46+
"WebFetch(domain:duckdb.org)",
47+
"WebFetch(domain:arrow.apache.org)",
48+
"WebFetch(domain:parquet.apache.org)",
49+
"WebFetch(domain:vortex.dev)",
50+
"WebSearch"
51+
]
52+
}
53+
}

.agents/skills/README.md

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
# skills
2+
3+
Project-local skills for AI coding agents (Claude Code, plus other tools that follow the [Agent Skills](https://agentskills.io) open standard, e.g. Codex). Each `<skill-name>/SKILL.md` is invokable as `/<skill-name>` and Claude can also load it automatically when its `description` matches the user's intent (unless `disable-model-invocation: true` is set).
4+
5+
`.agents/` is the canonical directory; `.claude → .agents` is a symlink at the repo root, so Claude Code (which reads `.claude/skills/`) and tooling that reads `.agents/skills/` see the same files.
6+
7+
## Script wrappers
8+
9+
Wrappers around `python -m scripts.pipeline.<module>`. Side-effecting ones set `disable-model-invocation: true` so Claude won't auto-trigger destructive work; read-only ones are model-invocable.
10+
11+
| Skill | Wraps | Purpose |
12+
|---|---|---|
13+
| `/raincloud-build` | `scripts.pipeline.build` | Full pipeline (fetch → … → convert) for one or more slugs. |
14+
| `/raincloud-fetch` | `scripts.pipeline.fetch` | Download raw bytes only. |
15+
| `/raincloud-extract` | `scripts.pipeline.extract` | Unpack archives into `_workdir/`. |
16+
| `/raincloud-convert` | `scripts.pipeline.convert` | Stage 7 — emit sibling `.vortex` per spec opt-in. |
17+
| `/raincloud-hydrate` | `scripts.pipeline.hydrate` | Stage 8 (optional, opt-in) — dereference a slug's URL column into a sibling parquet under `parquet-hydrated/`. Side-effecting (outbound HTTP); safety-filter-gated; `disable-model-invocation: true`. |
18+
| `/raincloud-docs` | `scripts.pipeline.docs` | Regenerate derived docs. *(model-invocable — regen is mostly idempotent.)* |
19+
| `/raincloud-tighten-variant` | `scripts.pipeline.tighten_variant` | In-place JSON → VARIANT promotion. |
20+
| `/raincloud-status` | `scripts.pipeline.status` | Per-slug filesystem state (raw / workdir / parquet / vortex / variant-pending). *(read-only, model-invocable.)* |
21+
| `/raincloud-validate-manifest` | `scripts.pipeline.validate_manifest` | Static checks for `sources.json` — JSON Schema + handler-registry / slug-uniqueness / fetch-auth cross-checks. *(read-only, model-invocable.)* |
22+
| `/raincloud-list-datasets` | `scripts.pipeline.list_datasets` | Filter/list slugs by family / handler / license / fetch-type / reader / vortex / regex. *(read-only, model-invocable.)* |
23+
24+
## Procedural playbooks (model-invocable)
25+
26+
These guide multi-step procedures from [`SKILLS.md`](../context/SKILLS.md). Default frontmatter — Claude can pull them up automatically when the user's request matches.
27+
28+
| Skill | When to use |
29+
|---|---|
30+
| `/raincloud-add-dataset` | Adding a new dataset to `sources.json` and producing its first build. |
31+
| `/raincloud-add-handler` | Writing a new transform handler under `scripts/pipeline/handlers/`. |
32+
| `/raincloud-add-kaggle-tos` | Adding a Kaggle dataset gated behind a one-time ToS click-through. |
33+
| `/raincloud-promote-variant` | Picks the right path (in-place vs new-build) for JSON → VARIANT. |
34+
| `/raincloud-debug-build` | Diagnostic checklist for a failing build — isolate which stage broke. |
35+
| `/raincloud-large-build` | Run a memory- or runtime-heavy build safely (caps, nohup, logging). *(side-effecting — `disable-model-invocation: true`.)* |
36+
| `/raincloud-remove-dataset` | Remove a dataset from the manifest and clean up its outputs. *(destructive — `disable-model-invocation: true`.)* |
37+
38+
## Supporting context
39+
40+
`../context/` holds symlinks back to the repo-root canonical docs:
41+
42+
- [`AGENTS.md`](../context/AGENTS.md) — invariants and architecture for AI agents.
43+
- [`SKILLS.md`](../context/SKILLS.md) — playbooks (the source for the procedural skills above).
44+
- [`README.md`](../context/README.md) — user-facing project overview.
45+
- [`sources.schema.md`](../context/sources.schema.md)`sources.json` schema reference.
46+
47+
Each `SKILL.md` references these via relative paths (`../../context/X.md`) so the agent pulls authoritative guidance without copying.
48+
49+
## Adding or editing a skill
50+
51+
```text
52+
my-skill/
53+
├── SKILL.md # required — frontmatter + instructions
54+
├── reference.md # optional — detailed reference loaded only when needed
55+
└── scripts/ # optional — bundled scripts the skill can execute
56+
└── helper.py
57+
```
58+
59+
Reference: <https://code.claude.com/docs/en/skills>. Frontmatter fields used here:
60+
61+
- `name` — slug; matches the directory name.
62+
- `description` — front-load the key use case (truncated at 1,536 chars in the listing).
63+
- `argument-hint` — autocomplete hint for `/<skill> <args>`.
64+
- `disable-model-invocation``true` for side-effecting skills so Claude won't auto-trigger them.
65+
- `allowed-tools` — pre-approve specific tool patterns when the skill is active (e.g. `Bash(python -m scripts.pipeline.build *)`).
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
---
2+
name: raincloud-add-dataset
3+
description: Walk through adding a new dataset to sources.json and producing its first build. Use when the user wants to onboard a new upstream source, add a slug to the manifest, or extend the catalog.
4+
argument-hint: [<proposed-slug>] [<upstream-url>]
5+
---
6+
7+
Guide the user through adding a new dataset entry. Reference: [SKILLS.md "Adding a new dataset"](../../context/SKILLS.md#adding-a-new-dataset), [sources.schema.md](../../context/sources.schema.md).
8+
9+
Steps:
10+
11+
1. **Identify the upstream.** Confirm with the user:
12+
- A stable public URL (prefer the publisher's canonical endpoint over a mirror).
13+
- The license — must permit redistribution-of-derivatives. Check SPDX ID and `source_url`.
14+
- Approximate row count (used for `expect.rows`; can be `null` on first build).
15+
16+
2. **Append a `DatasetSpec` to `sources.json`** using the Python load-edit-dump pattern from [AGENTS.md](../../context/AGENTS.md#safe-ways-to-edit-sourcesjson) — never `sed`. Start from [`examples/minimal_spec.json`](../../../examples/minimal_spec.json) (every field present with placeholder values) rather than typing one from scratch. Minimal direct-HTTP shape:
17+
18+
```jsonc
19+
{
20+
"slug": "my-dataset",
21+
"short_name": "My Dataset",
22+
"full_name": "My Dataset (publisher attribution)",
23+
"description": "One-line summary.",
24+
"family": "direct",
25+
"license": { "spdx": "CC0-1.0", "source_url": "...", "redistribution_permitted": true, "attribution_required": false },
26+
"fetch": { "type": "http", "urls": ["https://..."], "auth": null },
27+
"extract": { "type": "passthrough" },
28+
"parse": { "reader": "csv", "options": { "delimiter": "," } },
29+
"transform": { "handler": "tighten_types", "params": {} },
30+
"write": { "output": "my-dataset.parquet", "compression": "zstd", "row_group_size_rows": 1048576, "statistics": true },
31+
"expect": { "rows": 123456 }
32+
}
33+
```
34+
35+
Pick `family` from existing values (`direct`, `kaggle-upstream`, `nyc-tlc`, `public-bi`, `uci`) — do not invent new ones without discussing.
36+
37+
3. **Validate the manifest.** Invoke `/raincloud-validate-manifest` — sub-second check that the new entry has the right shape, the handler resolves, the slug is unique, and `fetch.type`/`fetch.auth` agree. Catches typos before paying for a fetch.
38+
39+
4. **Run the first build.** Invoke `/raincloud-build <slug> --loose` (the `--loose` is essential when the row count is a guess). If `expect.rows` was wrong, update the manifest with the actual count once the build succeeds.
40+
41+
5. **Regenerate docs.** Invoke `/raincloud-docs` (or just `/raincloud-docs datasets columns_parquet coverage_parquet`).
42+
43+
6. **Optionally opt into Vortex.** If the dataset's types are vortex-compatible, add `"convert": { "vortex": true }` to the spec and run `/raincloud-convert <slug>`.
44+
45+
If the upstream needs unpacking, set `extract.type` accordingly and pick a parser — see existing specs in `sources.json` for shapes. If the source has nested JSON or row-level processing, you'll likely need a custom handler — see `/raincloud-add-handler`. If it's a Kaggle dataset behind ToS acceptance, see `/raincloud-add-kaggle-tos`.
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
---
2+
name: raincloud-add-handler
3+
description: Walk through writing a new transform handler under scripts/pipeline/handlers/. Use when the default tighten_types/identity paths can't produce the right shape — row-level JSON parsing, streaming to avoid OOM, multi-output splitting, or VARIANT-from-the-start.
4+
argument-hint: [<handler-name>]
5+
---
6+
7+
Guide the user through adding a transform handler. Reference: [SKILLS.md "Adding a new transform handler"](../../context/SKILLS.md#adding-a-new-transform-handler) and ["Writing a streaming handler"](../../context/SKILLS.md#writing-a-streaming-handler).
8+
9+
When this is the right pattern: the default `tighten_types` / `identity` paths can't produce the right shape — e.g. the source needs row-level JSON parsing, streaming to avoid OOM, multi-output splitting (one upstream → many slugs), or VARIANT columns emitted from the start.
10+
11+
Steps:
12+
13+
1. **Create the handler** at `scripts/pipeline/handlers/<name>.py`. Signature:
14+
15+
```python
16+
def <name>(spec: dict, parsed: list[tuple[Path, pa.Table | None]], **params
17+
) -> list[tuple[str, pa.Table]]:
18+
...
19+
```
20+
21+
- `parsed` contains one `(path, table)` tuple per parsed file; `table` is `None` when `parse.reader = "custom"`.
22+
- Return `[(output_slug, table), ...]` — one tuple per output parquet. Multi-output handlers emit several slugs from one source (see `glove_split`, `osm_pbf_split`, `stack_exchange_split`).
23+
- **Streaming handlers** (write direct to parquet, bypass the write stage) return `[]`. Copy [`examples/streaming_handler.py.tmpl`](../../../examples/streaming_handler.py.tmpl) as the starting point — it has the `outputs_root()` / `duckdb_connect()` / cleanup wiring already shaped — and study `factbook_variant_parse`, `wikipedia_variant_parse`, `lichess_pgn_parse` for upstream-shape variations.
24+
25+
2. **Register** in `scripts/pipeline/handlers/__init__.py`:
26+
27+
```python
28+
from .<name> import <name>
29+
_REGISTRY = {
30+
...
31+
"<name>": <name>,
32+
}
33+
```
34+
35+
3. **Wire it into the manifest** — set `"transform": { "handler": "<name>", "params": { ... } }` on the relevant spec.
36+
37+
4. **Run the test suite**`pytest` checks that every handler in `_REGISTRY` imports and that there are no orphans (registered but unreferenced) or unregistered handler names in the manifest. Sub-second; catches typos before paying for a fetch.
38+
39+
5. **Build the dataset** — invoke `/raincloud-build <slug> --loose` to validate end-to-end.
40+
41+
Style: handlers stay short (most under 150 lines). Reuse `duckdb_connect`, `outputs_root`, `spec_field` from `scripts.pipeline.spec` rather than reimplementing. Don't shoehorn a new shape into `tighten_types` or `identity` — write a dedicated handler.
42+
43+
For VARIANT-from-the-start handlers, use DuckDB's `CAST(to_json(col) AS VARIANT)` inside `COPY ... TO PARQUET`. `duckdb_connect(db_path)` applies `storage_compatibility_version=v1.5.0` automatically. See `factbook_variant_parse` (1-column) and `wikipedia_variant_parse` (multi-column with typed siblings).
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
---
2+
name: raincloud-add-kaggle-tos
3+
description: Add a Kaggle dataset gated behind a one-time ToS click-through. Use when a Kaggle URL returns 403 from the API and the user needs the manifest+workflow pattern that documents the click-through gate cleanly.
4+
argument-hint: [<proposed-slug>] [<kaggle-url>]
5+
---
6+
7+
Guide the user through adding a Kaggle dataset that requires a one-time click-through ToS acceptance on the Kaggle web UI before the API will serve downloads. Reference: [SKILLS.md "Adding a Kaggle dataset gated behind ToS acceptance"](../../context/SKILLS.md#adding-a-kaggle-dataset-gated-behind-tos-acceptance).
8+
9+
**Diagnostic note:** an HTTP 403 from a Kaggle fetch can also indicate a wrong slug. Double-check the Kaggle URL before reaching for this pattern.
10+
11+
Steps:
12+
13+
1. **Add the spec** as a normal `fetch.type: "kaggle"` entry but mark it as gated and leave `expect.rows: null`:
14+
15+
```jsonc
16+
"fetch": {
17+
"type": "kaggle",
18+
"urls": ["https://www.kaggle.com/datasets/<owner>/<dataset>"],
19+
"auth": "kaggle",
20+
"requires_interactive_accept": true,
21+
"notes": "Kaggle gates this dataset behind a one-time click-through ToS acceptance."
22+
},
23+
"expect": { "rows": null, "notes": "Row count populated after the first successful build." }
24+
```
25+
26+
Use the [Python load-edit-dump pattern](../../context/AGENTS.md#safe-ways-to-edit-sourcesjson) — never `sed`.
27+
28+
2. **Confirm Kaggle creds are set up:** `~/.kaggle/kaggle.json` with `chmod 600`, and the project synced via `uv sync --extra kaggle`.
29+
30+
3. **Try the first build via `/raincloud-build <slug> --loose`.** Pre-flight will print `kaggle (ToS-gated): ...`. Expect a 403 on the first try. The error message will point at the exact Kaggle URL the user must visit.
31+
32+
4. **Tell the user to click Download once** in a browser signed into Kaggle. The 403 handler is generic — it triggers whether or not `requires_interactive_accept` is set, so forgetting the flag still yields a useful error; the flag only improves the up-front announcement.
33+
34+
5. **Re-run `/raincloud-build <slug> --loose`.** It should succeed.
35+
36+
6. **Update `expect.rows`** in the manifest with the actual count and **drop `expect.notes`** (now that the row count is known).
37+
38+
7. **Regenerate docs** via `/raincloud-docs`.

0 commit comments

Comments
 (0)