feat: scaffold AI Risk Index platform (research corpus, methodology v1 draft, monorepo)
- docs/research: 4-document research corpus grounding v1 (indices, data sources, rater API, landscape/evidence), compiled 2026-08-05 - docs/methodology: METHODOLOGY.md v1 draft, changelog, worked example - packages/scoring: pure deterministic engine (5 weighted dimensions, barriers inverted, exposure/substitution/augmentation sub-scores with CI bounds), snapshot + fast-check property tests - packages/db: Prisma schema with rating audit trail and immutable runs - apps/web: Next.js 14 site + public API v1 (health, methodology, occupations) - apps/worker: BullMQ multi-model rater pipeline (Message Batches, versioned rubric prompt v1) + score:recompute - apps/etl: Python skeleton with O*NET 30.3 downloader - infra: compose files, deploy.sh, ngrok.yml, env template Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
74 changed files +7,101 −0
added
.gitignore
+33 −0
@@ -0,0 +1,33 @@ | ||
| 1 | +# deps / builds | |
| 2 | +node_modules/ | |
| 3 | +.next/ | |
| 4 | +dist/ | |
| 5 | +.turbo/ | |
| 6 | +*.tsbuildinfo | |
| 7 | + | |
| 8 | +# env & secrets — never commit (infra/.env.example is the template) | |
| 9 | +.env | |
| 10 | +.env.* | |
| 11 | +!.env.example | |
| 12 | + | |
| 13 | +# raw data is immutable and fetched by script — never committed (CLAUDE.md §5) | |
| 14 | +data/raw/* | |
| 15 | +!data/raw/.gitkeep | |
| 16 | + | |
| 17 | +# derived data: only manifests are committed, not payloads | |
| 18 | +data/derived/**/*.csv | |
| 19 | +data/derived/**/*.parquet | |
| 20 | +data/derived/**/*.jsonl | |
| 21 | +!data/derived/**/manifest.json | |
| 22 | + | |
| 23 | +# python (apps/etl) | |
| 24 | +__pycache__/ | |
| 25 | +.venv/ | |
| 26 | +*.egg-info/ | |
| 27 | +.pytest_cache/ | |
| 28 | +.ruff_cache/ | |
| 29 | + | |
| 30 | +# misc | |
| 31 | +.DS_Store | |
| 32 | +coverage/ | |
| 33 | +playwright-report/ | |
added
.prettierrc
+6 −0
@@ -0,0 +1,6 @@ | ||
| 1 | +{ | |
| 2 | + "semi": true, | |
| 3 | + "singleQuote": false, | |
| 4 | + "trailingComma": "all", | |
| 5 | + "printWidth": 100 | |
| 6 | +} | |
added
CLAUDE.md
+211 −0
@@ -0,0 +1,211 @@ | ||
| 1 | +# CLAUDE.md — AI Risk Index Platform (www.airiskindex.io) | |
| 2 | + | |
| 3 | +This file provides guidance to Claude Code when working in this repository. | |
| 4 | + | |
| 5 | +--- | |
| 6 | + | |
| 7 | +## 1. Project Overview | |
| 8 | + | |
| 9 | +**AI Risk Index** (airiskindex.io) is a public platform that scores occupations on their exposure to AI-driven automation, using a **task-based methodology** (not whole-occupation scoring). Every score must be transparent, versioned, and reproducible. | |
| 10 | + | |
| 11 | +**Core product promise:** "The most methodologically rigorous, fully transparent AI job-exposure index." | |
| 12 | + | |
| 13 | +### The three concepts (never confuse them) | |
| 14 | +- **Exposure** — AI is technically capable of performing the task | |
| 15 | +- **Substitution** — AI actually replaces the human performing it | |
| 16 | +- **Augmentation** — AI assists the human, increasing productivity | |
| 17 | + | |
| 18 | +The composite index reports all three as separate sub-scores. Never collapse them into a single undifferentiated "risk" number in code, UI copy, or API responses without also exposing the sub-scores. | |
| 19 | + | |
| 20 | +### Scoring model (v1) | |
| 21 | +Composite score 0–100 per occupation, computed from task-level scores weighted by task importance/frequency (O*NET weights). Dimensions: | |
| 22 | + | |
| 23 | +| Dimension | Key | Weight (v1) | | |
| 24 | +|---|---|---| | |
| 25 | +| Task automatability | `automatability` | 0.35 | | |
| 26 | +| Current technical feasibility | `feasibility` | 0.20 | | |
| 27 | +| Cost of substitution vs. wage | `cost_ratio` | 0.15 | | |
| 28 | +| Adoption barriers (regulation, liability, human-contact requirement) | `barriers` | 0.20 | | |
| 29 | +| Sector adoption velocity | `adoption_velocity` | 0.10 | | |
| 30 | + | |
| 31 | +- Weights live in `packages/scoring/src/weights.ts` — **never hardcode weights anywhere else**. | |
| 32 | +- Every scoring release gets a semver version (`INDEX_VERSION` in `packages/scoring/src/version.ts`) and a changelog entry in `docs/methodology/CHANGELOG.md`. | |
| 33 | +- Scores are stored with confidence intervals (`score_low`, `score`, `score_high`). UI must always be able to display uncertainty. | |
| 34 | + | |
| 35 | +### Data sources | |
| 36 | +- **O*NET 30.x** database dumps (occupations + tasks + importance ratings) → `data/raw/onet/` — 30.3 as of May 2026, 31.0 expected late Aug 2026. ⚠️ 30.x renamed *Technology Skills → Software Skills* and split *Skills* into Essential/Transferable; ETL loaders must target the 30.x schema. License CC BY 4.0 (attribution required). | |
| 37 | +- ESCO crosswalk for EU/France occupations (ROME codes) → `data/raw/esco/` | |
| 38 | +- LLM-as-evaluator task ratings (see §6) → `data/derived/ratings/` | |
| 39 | +- Expert panel (Delphi) validation overrides → `data/derived/expert_overrides/` | |
| 40 | + | |
| 41 | +Raw data is **never** edited in place. All transformations go through the ETL pipeline (`apps/etl`). | |
| 42 | + | |
| 43 | +--- | |
| 44 | + | |
| 45 | +## 2. Tech Stack | |
| 46 | + | |
| 47 | +- **Monorepo:** pnpm workspaces + Turborepo | |
| 48 | +- **Language:** TypeScript everywhere (strict mode). Python 3.12 only inside `apps/etl` for data science steps. | |
| 49 | +- **Frontend:** Next.js 14 (App Router), React 18, Tailwind CSS, shadcn/ui, Recharts for visualizations | |
| 50 | +- **API:** Next.js route handlers for public API (`/api/v1/*`) + tRPC for internal app calls | |
| 51 | +- **Database:** PostgreSQL 16 (Prisma ORM). Read-heavy → materialized views for score lookups. | |
| 52 | +- **Cache:** Redis (score lookups, rate limiting) | |
| 53 | +- **Jobs:** BullMQ workers in `apps/worker` (score recomputation, LLM rating batches) | |
| 54 | +- **Auth:** Auth.js (email magic link + OAuth). Public browsing requires no auth. | |
| 55 | +- **Testing:** Vitest (unit), Playwright (e2e), pytest (ETL) | |
| 56 | +- **Lint/format:** ESLint + Prettier, ruff for Python. CI fails on warnings. | |
| 57 | + | |
| 58 | +## 3. Repository Layout | |
| 59 | + | |
| 60 | +``` | |
| 61 | +airiskindex/ | |
| 62 | +├── apps/ | |
| 63 | +│ ├── web/ # Next.js app (site + public API routes) | |
| 64 | +│ ├── worker/ # BullMQ background workers | |
| 65 | +│ └── etl/ # Python data pipeline (raw → derived → DB) | |
| 66 | +├── packages/ | |
| 67 | +│ ├── scoring/ # Pure TS scoring engine — NO I/O, fully deterministic | |
| 68 | +│ ├── db/ # Prisma schema + client + seeds | |
| 69 | +│ ├── ui/ # Shared React components | |
| 70 | +│ └── config/ # Shared eslint/ts/tailwind configs | |
| 71 | +├── data/ | |
| 72 | +│ ├── raw/ # Immutable source dumps (gitignored, fetched by script) | |
| 73 | +│ └── derived/ # Pipeline outputs (versioned manifests committed) | |
| 74 | +├── docs/ | |
| 75 | +│ └── methodology/ # Public methodology doc, changelog, sensitivity analyses | |
| 76 | +├── infra/ # Deployment scripts, ngrok config, systemd units | |
| 77 | +└── CLAUDE.md | |
| 78 | +``` | |
| 79 | + | |
| 80 | +## 4. Commands | |
| 81 | + | |
| 82 | +```bash | |
| 83 | +pnpm install # install all workspaces | |
| 84 | +pnpm dev # run web app on :3000 (+ worker with --filter) | |
| 85 | +pnpm build # turbo build all | |
| 86 | +pnpm test # vitest across packages | |
| 87 | +pnpm test:e2e # playwright (requires pnpm dev running) | |
| 88 | +pnpm lint && pnpm typecheck # must pass before any commit | |
| 89 | +pnpm db:migrate # prisma migrate dev | |
| 90 | +pnpm db:seed # seed occupations + demo scores | |
| 91 | +pnpm score:recompute # full index recomputation (writes new INDEX_VERSION run) | |
| 92 | +cd apps/etl && make pipeline # full ETL: raw → derived → DB load | |
| 93 | +``` | |
| 94 | + | |
| 95 | +Single test file: `pnpm vitest run packages/scoring/src/composite.test.ts` | |
| 96 | + | |
| 97 | +## 5. Coding Conventions | |
| 98 | + | |
| 99 | +- `packages/scoring` must stay **pure and deterministic**: no network, no DB, no `Date.now()`, no randomness. It takes typed inputs and returns typed scores. This is what makes the methodology auditable. | |
| 100 | +- Every scoring function has property-based tests (fast-check) + snapshot tests against the published methodology examples in `docs/methodology/examples/`. | |
| 101 | +- All user-facing risk language follows the tone guide: the product frames results as **adaptation guidance, not doom**. Avoid copy like "your job will disappear"; prefer "X% of tasks in this occupation are highly exposed". | |
| 102 | +- API responses are versioned (`/api/v1/...`) and include `index_version` in every payload. | |
| 103 | +- Money/wages: store as integer cents + ISO currency. Percentages: store as 0–1 floats, format only at the UI layer. | |
| 104 | +- Never commit anything under `data/raw/`. Derived data commits only the manifest JSON (hashes + row counts), not the payloads. | |
| 105 | +- Migrations: additive-first. Destructive migrations require a `-- DESTRUCTIVE` comment and a manual approval in PR review. | |
| 106 | +- Accessibility: all charts need a data-table fallback (`<VisuallyHidden>` table) — this is a public-interest tool. | |
| 107 | + | |
| 108 | +## 6. LLM-as-Evaluator (task rating pipeline) | |
| 109 | + | |
| 110 | +Task automatability ratings are produced by an LLM rater in `apps/worker/src/raters/`, then validated by human experts. | |
| 111 | + | |
| 112 | +- Use the Anthropic API via the official SDK. Model names come from the `RATER_MODELS` env var (comma-separated list — **multi-model rating is required**: single-model LLM exposure ratings show up to 19× spread across frontier raters; see `docs/research/01-existing-indices.md` §7.2). Never hardcode model IDs in source. | |
| 113 | +- Newer models (Sonnet 5 / Opus 5) reject the `temperature` parameter — rating variance comes from the multi-model panel, not sampling temperature. | |
| 114 | +- Prompts live in `apps/worker/src/raters/prompts/*.md` and are versioned; a prompt change bumps `RATER_PROMPT_VERSION` and invalidates cached ratings. | |
| 115 | +- Every rating stores: model, prompt version, raw response, parsed score, timestamp. Full audit trail, always. | |
| 116 | +- Ratings are sampled (5%) for human review; disagreement > 1 point on the 5-point scale flags the task for the expert panel queue. | |
| 117 | +- Batch jobs must be idempotent and resumable (BullMQ job IDs = deterministic hash of task_id + prompt version). | |
| 118 | +- For current API details (batch endpoints, rate limits, model names), check https://platform.claude.com/docs (docs.claude.com redirects there) rather than relying on memory. A vetted snapshot lives in `docs/research/03-llm-rater-api.md`. | |
| 119 | + | |
| 120 | +## 7. Environments & Deployment | |
| 121 | + | |
| 122 | +### Environments | |
| 123 | +- `local` — developer machine, SQLite optional shortcut is **not** allowed; always Postgres via Docker (`infra/docker-compose.dev.yml`). | |
| 124 | +- `staging` — node **m3u96b**, exposed via **ngrok** (see below). | |
| 125 | +- `production` — airiskindex.io (target: VPS/managed later; staging setup is the current deployment). | |
| 126 | + | |
| 127 | +### Deploying to node m3u96b (staging/current prod) | |
| 128 | + | |
| 129 | +The app currently runs on the self-hosted node `m3u96b` and is exposed publicly through an ngrok tunnel mapped to `www.airiskindex.io`. | |
| 130 | + | |
| 131 | +**Process layout on m3u96b:** | |
| 132 | +- `airiskindex-web.service` (systemd) → `node apps/web/.next/standalone/server.js` on `127.0.0.1:3000` | |
| 133 | +- `airiskindex-worker.service` → BullMQ worker | |
| 134 | +- `postgres` + `redis` via Docker Compose (`infra/docker-compose.prod.yml`) | |
| 135 | +- `ngrok.service` → tunnel `127.0.0.1:3000` → public edge | |
| 136 | + | |
| 137 | +**Deploy procedure (scripted in `infra/deploy.sh`):** | |
| 138 | +```bash | |
| 139 | +ssh m3u96b | |
| 140 | +cd /srv/airiskindex | |
| 141 | +git pull --ff-only origin main | |
| 142 | +pnpm install --frozen-lockfile | |
| 143 | +pnpm build | |
| 144 | +pnpm db:migrate:deploy # prisma migrate deploy (no interactive) | |
| 145 | +sudo systemctl restart airiskindex-web airiskindex-worker | |
| 146 | +sudo systemctl status airiskindex-web --no-pager # verify healthy | |
| 147 | +curl -fsS http://127.0.0.1:3000/api/v1/health # must return {"ok":true,...} | |
| 148 | +``` | |
| 149 | + | |
| 150 | +**ngrok configuration (`infra/ngrok.yml`, copied to `/etc/ngrok/ngrok.yml` on the node):** | |
| 151 | +```yaml | |
| 152 | +version: 3 | |
| 153 | +agent: | |
| 154 | + authtoken: ${NGROK_AUTHTOKEN} # from env, never committed | |
| 155 | +endpoints: | |
| 156 | + - name: airiskindex | |
| 157 | + url: https://www.airiskindex.io # requires custom domain configured in ngrok dashboard + CNAME | |
| 158 | + upstream: | |
| 159 | + url: 3000 | |
| 160 | +``` | |
| 161 | +- The custom domain must be added in the ngrok dashboard and DNS `CNAME` for `www.airiskindex.io` pointed at the ngrok edge target they provide. Verify with `dig CNAME www.airiskindex.io`. | |
| 162 | +- ngrok runs as `ngrok.service` (systemd, `Restart=always`). Logs: `journalctl -u ngrok -f`. | |
| 163 | +- Health rule: after every deploy, hit the public URL, not just localhost — tunnel failures are the most common outage cause. | |
| 164 | +- Because the app sits behind ngrok, trust `X-Forwarded-*` headers: Next.js config already sets trusted proxy handling; do not remove it. Rate limiting keys off `x-forwarded-for` first IP. | |
| 165 | + | |
| 166 | +**Environment variables** (`/srv/airiskindex/.env`, template in `infra/.env.example`): | |
| 167 | +``` | |
| 168 | +DATABASE_URL=postgresql://... | |
| 169 | +REDIS_URL=redis://... | |
| 170 | +ANTHROPIC_API_KEY=... # rater pipeline | |
| 171 | +RATER_MODELS=... # comma-separated model IDs (multi-model rating panel) | |
| 172 | +NGROK_AUTHTOKEN=... | |
| 173 | +NEXTAUTH_URL=https://www.airiskindex.io | |
| 174 | +NEXTAUTH_SECRET=... | |
| 175 | +PUBLIC_BASE_URL=https://www.airiskindex.io | |
| 176 | +``` | |
| 177 | +Never print, log, or commit secrets. `infra/.env.example` lists keys with empty values only. | |
| 178 | + | |
| 179 | +### Rollback | |
| 180 | +```bash | |
| 181 | +ssh m3u96b | |
| 182 | +cd /srv/airiskindex | |
| 183 | +git checkout <previous-tag> | |
| 184 | +pnpm install --frozen-lockfile && pnpm build | |
| 185 | +sudo systemctl restart airiskindex-web airiskindex-worker | |
| 186 | +``` | |
| 187 | +DB rollbacks: only via forward-fix migrations. Nightly `pg_dump` to `/srv/backups` (retained 14 days) — verify the cron is alive when touching infra. | |
| 188 | + | |
| 189 | +## 8. Public API rules | |
| 190 | + | |
| 191 | +- `/api/v1/occupations` — list/search (paginated, cached 1h) | |
| 192 | +- `/api/v1/occupations/:code` — full score breakdown incl. sub-scores, CI bounds, task list, `index_version` | |
| 193 | +- `/api/v1/methodology` — machine-readable weights + version metadata | |
| 194 | +- Rate limit: 60 req/min unauthenticated, 600 with API key. Return `429` with `Retry-After`. | |
| 195 | +- Breaking changes require a new `/api/v2` — never mutate v1 response shapes. | |
| 196 | + | |
| 197 | +## 9. Methodology Integrity Rules (non-negotiable) | |
| 198 | + | |
| 199 | +1. Any change to weights, formulas, or rater prompts ⇒ bump `INDEX_VERSION`, add changelog entry, regenerate `docs/methodology/sensitivity/` outputs. | |
| 200 | +2. Scores shown anywhere must be traceable to a stored computation run (`score_runs` table) — no ad-hoc numbers. | |
| 201 | +3. Historical scores are immutable; recomputations create new runs, old runs remain queryable. | |
| 202 | +4. The public methodology doc (`docs/methodology/METHODOLOGY.md`) is the source of truth; code comments link to its section anchors. If code and doc disagree, stop and flag it — do not silently pick one. | |
| 203 | + | |
| 204 | +## 10. When Working in This Repo, Claude Should | |
| 205 | + | |
| 206 | +- Run `pnpm typecheck && pnpm lint && pnpm test` before declaring any task done. | |
| 207 | +- Touch `packages/scoring` only with accompanying tests and a methodology changelog note. | |
| 208 | +- Ask before: destructive migrations, changing index weights, editing ngrok/systemd config, or anything that alters public API shapes. | |
| 209 | +- Prefer small, reviewable commits with conventional-commit messages (`feat(scoring): ...`, `fix(api): ...`, `infra(deploy): ...`). | |
| 210 | +- Keep UI copy aligned with the "adaptation, not doom" tone guide. | |
| 211 | +- When uncertain about Anthropic API specifics (models, batch API, limits), consult the official docs instead of guessing. | |
added
README.md
+29 −0
@@ -0,0 +1,29 @@ | ||
| 1 | +# AI Risk Index — airiskindex.io | |
| 2 | + | |
| 3 | +The most methodologically rigorous, fully transparent AI job-exposure index. | |
| 4 | +Task-based scoring of occupations on AI-driven automation exposure, with three | |
| 5 | +separate sub-scores per occupation — **exposure**, **substitution**, **augmentation** — | |
| 6 | +each with confidence intervals, versioned methodology, and a public API. | |
| 7 | + | |
| 8 | +- Methodology (source of truth): [`docs/methodology/METHODOLOGY.md`](docs/methodology/METHODOLOGY.md) | |
| 9 | +- Research corpus grounding v1: [`docs/research/`](docs/research/README.md) | |
| 10 | +- Contributor rules: [`CLAUDE.md`](CLAUDE.md) | |
| 11 | + | |
| 12 | +## Quick start | |
| 13 | + | |
| 14 | +```bash | |
| 15 | +pnpm install | |
| 16 | +docker compose -f infra/docker-compose.dev.yml up -d # postgres 16 + redis | |
| 17 | +cp infra/.env.example .env # fill values | |
| 18 | +pnpm db:migrate && pnpm db:seed | |
| 19 | +pnpm dev # web on :3000 | |
| 20 | +``` | |
| 21 | + | |
| 22 | +Checks: `pnpm typecheck && pnpm lint && pnpm test` | |
| 23 | + | |
| 24 | +## Layout | |
| 25 | + | |
| 26 | +Monorepo (pnpm + Turborepo): `apps/web` (Next.js site + public API), `apps/worker` | |
| 27 | +(BullMQ jobs incl. the multi-model LLM rater), `apps/etl` (Python pipeline), | |
| 28 | +`packages/scoring` (pure, deterministic scoring engine), `packages/db` (Prisma), | |
| 29 | +`packages/ui`, `packages/config`. See CLAUDE.md §3. | |
added
apps/etl/Makefile
+21 −0
@@ -0,0 +1,21 @@ | ||
| 1 | +PYTHON ?= python3 | |
| 2 | +export PYTHONPATH := src | |
| 3 | + | |
| 4 | +.PHONY: pipeline download transform load test lint | |
| 5 | + | |
| 6 | +pipeline: download transform load | |
| 7 | + | |
| 8 | +download: | |
| 9 | + $(PYTHON) -m airiskindex_etl.download_onet | |
| 10 | + | |
| 11 | +transform: | |
| 12 | + @echo "TODO: raw -> derived transforms (task statements + importance ratings -> data/derived/)" | |
| 13 | + | |
| 14 | +load: | |
| 15 | + @echo "TODO: derived -> Postgres load (occupations + tasks via Prisma-compatible schema)" | |
| 16 | + | |
| 17 | +test: | |
| 18 | + $(PYTHON) -m pytest | |
| 19 | + | |
| 20 | +lint: | |
| 21 | + ruff check . | |
added
apps/etl/pyproject.toml
+19 −0
@@ -0,0 +1,19 @@ | ||
| 1 | +[project] | |
| 2 | +name = "airiskindex-etl" | |
| 3 | +version = "0.1.0" | |
| 4 | +description = "AI Risk Index data pipeline: raw source dumps -> derived artifacts -> Postgres" | |
| 5 | +requires-python = ">=3.12" | |
| 6 | +dependencies = [ | |
| 7 | + "requests>=2.32", | |
| 8 | + "pandas>=2.2", | |
| 9 | +] | |
| 10 | + | |
| 11 | +[project.optional-dependencies] | |
| 12 | +dev = ["pytest>=8", "ruff>=0.5"] | |
| 13 | + | |
| 14 | +[tool.ruff] | |
| 15 | +line-length = 100 | |
| 16 | +target-version = "py312" | |
| 17 | + | |
| 18 | +[tool.pytest.ini_options] | |
| 19 | +testpaths = ["tests"] | |
added
apps/etl/src/airiskindex_etl/__init__.py
+6 −0
@@ -0,0 +1,6 @@ | ||
| 1 | +"""AI Risk Index ETL: raw source dumps -> derived artifacts -> Postgres. | |
| 2 | + | |
| 3 | +Raw data is never edited in place (CLAUDE.md §1); every derived artifact gets a | |
| 4 | +manifest JSON (hashes + row counts) — only manifests are committed. | |
| 5 | +Source catalogue with verified URLs: docs/research/02-data-sources.md. | |
| 6 | +""" | |
added
apps/etl/src/airiskindex_etl/download_onet.py
+64 −0
@@ -0,0 +1,64 @@ | ||
| 1 | +"""Fetch the O*NET database text dump into data/raw/onet/ (immutable). | |
| 2 | + | |
| 3 | +O*NET 30.x — CC BY 4.0, attribution required. 30.x renamed | |
| 4 | +"Technology Skills" -> "Software Skills" and split Skills into | |
| 5 | +Essential/Transferable (see docs/research/02-data-sources.md §O*NET). | |
| 6 | +Check https://www.onetcenter.org/database.html for the current release | |
| 7 | +(31.0 expected late Aug 2026) before bumping ONET_VERSION. | |
| 8 | +""" | |
| 9 | + | |
| 10 | +from __future__ import annotations | |
| 11 | + | |
| 12 | +import hashlib | |
| 13 | +import json | |
| 14 | +import sys | |
| 15 | +import zipfile | |
| 16 | +from pathlib import Path | |
| 17 | + | |
| 18 | +import requests | |
| 19 | + | |
| 20 | +ONET_VERSION = "30_3" | |
| 21 | +ONET_URL = f"https://www.onetcenter.org/dl_files/database/db_{ONET_VERSION}_text.zip" | |
| 22 | + | |
| 23 | +# Some sources (notably BLS OEWS) block non-browser user agents; use a plain | |
| 24 | +# descriptive UA with contact info everywhere for consistency and courtesy. | |
| 25 | +USER_AGENT = "airiskindex-etl/0.1 (https://www.airiskindex.io; data pipeline)" | |
| 26 | + | |
| 27 | +REPO_ROOT = Path(__file__).resolve().parents[4] | |
| 28 | +RAW_DIR = REPO_ROOT / "data" / "raw" / "onet" | |
| 29 | + | |
| 30 | + | |
| 31 | +def download() -> Path: | |
| 32 | + RAW_DIR.mkdir(parents=True, exist_ok=True) | |
| 33 | + archive = RAW_DIR / f"db_{ONET_VERSION}_text.zip" | |
| 34 | + if archive.exists(): | |
| 35 | + print(f"already present: {archive}") | |
| 36 | + return archive | |
| 37 | + | |
| 38 | + print(f"downloading {ONET_URL}") | |
| 39 | + response = requests.get(ONET_URL, headers={"User-Agent": USER_AGENT}, timeout=300, stream=True) | |
| 40 | + response.raise_for_status() | |
| 41 | + with archive.open("wb") as fh: | |
| 42 | + for chunk in response.iter_content(chunk_size=1 << 20): | |
| 43 | + fh.write(chunk) | |
| 44 | + | |
| 45 | + with zipfile.ZipFile(archive) as zf: | |
| 46 | + zf.extractall(RAW_DIR) | |
| 47 | + | |
| 48 | + manifest = { | |
| 49 | + "source": ONET_URL, | |
| 50 | + "version": ONET_VERSION, | |
| 51 | + "sha256": hashlib.sha256(archive.read_bytes()).hexdigest(), | |
| 52 | + "files": sorted(p.name for p in RAW_DIR.iterdir()), | |
| 53 | + } | |
| 54 | + (RAW_DIR / "manifest.json").write_text(json.dumps(manifest, indent=2)) | |
| 55 | + print(f"downloaded and extracted to {RAW_DIR}") | |
| 56 | + return archive | |
| 57 | + | |
| 58 | + | |
| 59 | +if __name__ == "__main__": | |
| 60 | + try: | |
| 61 | + download() | |
| 62 | + except requests.RequestException as error: | |
| 63 | + print(f"download failed: {error}", file=sys.stderr) | |
| 64 | + sys.exit(1) | |
added
apps/etl/tests/test_smoke.py
+7 −0
@@ -0,0 +1,7 @@ | ||
| 1 | +from airiskindex_etl.download_onet import ONET_URL, ONET_VERSION, RAW_DIR | |
| 2 | + | |
| 3 | + | |
| 4 | +def test_onet_source_configuration() -> None: | |
| 5 | + assert ONET_VERSION.startswith("30_") | |
| 6 | + assert ONET_URL.endswith(f"db_{ONET_VERSION}_text.zip") | |
| 7 | + assert RAW_DIR.parts[-3:] == ("data", "raw", "onet") | |
added
apps/web/app/api/v1/health/route.ts
+11 −0
@@ -0,0 +1,11 @@ | ||
| 1 | +import { INDEX_VERSION } from "@airiskindex/scoring"; | |
| 2 | + | |
| 3 | +export const dynamic = "force-dynamic"; | |
| 4 | + | |
| 5 | +export function GET(): Response { | |
| 6 | + return Response.json({ | |
| 7 | + ok: true, | |
| 8 | + index_version: INDEX_VERSION, | |
| 9 | + time: new Date().toISOString(), | |
| 10 | + }); | |
| 11 | +} | |
added
apps/web/app/api/v1/methodology/route.ts
+27 −0
@@ -0,0 +1,27 @@ | ||
| 1 | +import { | |
| 2 | + DIMENSIONS, | |
| 3 | + EXPOSURE_DIMENSIONS, | |
| 4 | + HIGH_EXPOSURE_THRESHOLD, | |
| 5 | + INDEX_VERSION, | |
| 6 | + INVERTED_DIMENSIONS, | |
| 7 | + WEIGHTS, | |
| 8 | +} from "@airiskindex/scoring"; | |
| 9 | + | |
| 10 | +export function GET(): Response { | |
| 11 | + return Response.json( | |
| 12 | + { | |
| 13 | + index_version: INDEX_VERSION, | |
| 14 | + dimensions: DIMENSIONS, | |
| 15 | + weights: WEIGHTS, | |
| 16 | + inverted_dimensions: [...INVERTED_DIMENSIONS], | |
| 17 | + exposure_dimensions: EXPOSURE_DIMENSIONS, | |
| 18 | + high_exposure_threshold: HIGH_EXPOSURE_THRESHOLD, | |
| 19 | + scales: { | |
| 20 | + ratings: "1-5 per task and dimension, multi-model LLM panel with expert overrides", | |
| 21 | + scores: "0-100, each with low/score/high confidence bounds", | |
| 22 | + }, | |
| 23 | + documentation: "https://www.airiskindex.io/methodology", | |
| 24 | + }, | |
| 25 | + { headers: { "Cache-Control": "public, max-age=3600" } }, | |
| 26 | + ); | |
| 27 | +} | |
added
apps/web/app/api/v1/occupations/[code]/route.ts
+70 −0
@@ -0,0 +1,70 @@ | ||
| 1 | +import { prisma } from "@airiskindex/db"; | |
| 2 | +import { INDEX_VERSION } from "@airiskindex/scoring"; | |
| 3 | +import type { NextRequest } from "next/server"; | |
| 4 | + | |
| 5 | +export const dynamic = "force-dynamic"; | |
| 6 | + | |
| 7 | +export async function GET( | |
| 8 | + _request: NextRequest, | |
| 9 | + { params }: { params: { code: string } }, | |
| 10 | +): Promise<Response> { | |
| 11 | + try { | |
| 12 | + const occupation = await prisma.occupation.findUnique({ | |
| 13 | + where: { code: params.code }, | |
| 14 | + include: { | |
| 15 | + tasks: { | |
| 16 | + select: { id: true, statement: true, importance: true }, | |
| 17 | + orderBy: { id: "asc" }, | |
| 18 | + }, | |
| 19 | + }, | |
| 20 | + }); | |
| 21 | + if (!occupation) { | |
| 22 | + return Response.json({ error: "not_found" }, { status: 404 }); | |
| 23 | + } | |
| 24 | + | |
| 25 | + // Latest run's scores; historical runs stay queryable (CLAUDE.md §9). | |
| 26 | + const latest = await prisma.occupationScore.findFirst({ | |
| 27 | + where: { occupationCode: occupation.code }, | |
| 28 | + orderBy: { run: { createdAt: "desc" } }, | |
| 29 | + include: { run: true }, | |
| 30 | + }); | |
| 31 | + | |
| 32 | + return Response.json({ | |
| 33 | + index_version: latest?.run.indexVersion ?? INDEX_VERSION, | |
| 34 | + occupation: { | |
| 35 | + code: occupation.code, | |
| 36 | + title: occupation.title, | |
| 37 | + description: occupation.description, | |
| 38 | + esco_uri: occupation.escoUri, | |
| 39 | + rome_code: occupation.romeCode, | |
| 40 | + median_wage_cents: occupation.medianWageCents, | |
| 41 | + wage_currency: occupation.wageCurrency, | |
| 42 | + }, | |
| 43 | + scores: latest | |
| 44 | + ? { | |
| 45 | + run_id: latest.runId, | |
| 46 | + computed_at: latest.run.createdAt, | |
| 47 | + substitution: { | |
| 48 | + low: latest.substitutionLow, | |
| 49 | + score: latest.substitution, | |
| 50 | + high: latest.substitutionHigh, | |
| 51 | + }, | |
| 52 | + exposure: { | |
| 53 | + low: latest.exposureLow, | |
| 54 | + score: latest.exposure, | |
| 55 | + high: latest.exposureHigh, | |
| 56 | + }, | |
| 57 | + augmentation: { | |
| 58 | + low: latest.augmentationLow, | |
| 59 | + score: latest.augmentation, | |
| 60 | + high: latest.augmentationHigh, | |
| 61 | + }, | |
| 62 | + highly_exposed_task_share: latest.highlyExposedTaskShare, | |
| 63 | + } | |
| 64 | + : null, | |
| 65 | + tasks: occupation.tasks, | |
| 66 | + }); | |
| 67 | + } catch { | |
| 68 | + return Response.json({ error: "database_unavailable" }, { status: 503 }); | |
| 69 | + } | |
| 70 | +} | |
added
apps/web/app/api/v1/occupations/route.ts
+40 −0
@@ -0,0 +1,40 @@ | ||
| 1 | +import { prisma } from "@airiskindex/db"; | |
| 2 | +import { INDEX_VERSION } from "@airiskindex/scoring"; | |
| 3 | +import type { NextRequest } from "next/server"; | |
| 4 | + | |
| 5 | +export const dynamic = "force-dynamic"; | |
| 6 | + | |
| 7 | +export async function GET(request: NextRequest): Promise<Response> { | |
| 8 | + const { searchParams } = new URL(request.url); | |
| 9 | + const q = searchParams.get("q") ?? undefined; | |
| 10 | + const page = Math.max(1, Number(searchParams.get("page") ?? "1") || 1); | |
| 11 | + const perPage = Math.min(100, Math.max(1, Number(searchParams.get("per_page") ?? "25") || 25)); | |
| 12 | + | |
| 13 | + const where = q | |
| 14 | + ? { | |
| 15 | + OR: [ | |
| 16 | + { title: { contains: q, mode: "insensitive" as const } }, | |
| 17 | + { code: { startsWith: q } }, | |
| 18 | + ], | |
| 19 | + } | |
| 20 | + : {}; | |
| 21 | + | |
| 22 | + try { | |
| 23 | + const [total, items] = await Promise.all([ | |
| 24 | + prisma.occupation.count({ where }), | |
| 25 | + prisma.occupation.findMany({ | |
| 26 | + where, | |
| 27 | + orderBy: { code: "asc" }, | |
| 28 | + skip: (page - 1) * perPage, | |
| 29 | + take: perPage, | |
| 30 | + select: { code: true, title: true }, | |
| 31 | + }), | |
| 32 | + ]); | |
| 33 | + return Response.json( | |
| 34 | + { index_version: INDEX_VERSION, page, per_page: perPage, total, items }, | |
| 35 | + { headers: { "Cache-Control": "public, s-maxage=3600, stale-while-revalidate=600" } }, | |
| 36 | + ); | |
| 37 | + } catch { | |
| 38 | + return Response.json({ error: "database_unavailable" }, { status: 503 }); | |
| 39 | + } | |
| 40 | +} | |
added
apps/web/app/globals.css
+3 −0
@@ -0,0 +1,3 @@ | ||
| 1 | +@tailwind base; | |
| 2 | +@tailwind components; | |
| 3 | +@tailwind utilities; | |
added
apps/web/app/layout.tsx
+17 −0
@@ -0,0 +1,17 @@ | ||
| 1 | +import type { Metadata } from "next"; | |
| 2 | +import type { ReactNode } from "react"; | |
| 3 | +import "./globals.css"; | |
| 4 | + | |
| 5 | +export const metadata: Metadata = { | |
| 6 | + title: "AI Risk Index — task-based AI exposure scores for every occupation", | |
| 7 | + description: | |
| 8 | + "Transparent, versioned, task-based scores of how occupations are exposed to AI — with separate exposure, substitution and augmentation sub-scores and confidence intervals. Adaptation guidance, not doom.", | |
| 9 | +}; | |
| 10 | + | |
| 11 | +export default function RootLayout({ children }: { children: ReactNode }): JSX.Element { | |
| 12 | + return ( | |
| 13 | + <html lang="en"> | |
| 14 | + <body className="min-h-screen bg-white text-slate-900 antialiased">{children}</body> | |
| 15 | + </html> | |
| 16 | + ); | |
| 17 | +} | |
added
apps/web/app/page.tsx
+70 −0
@@ -0,0 +1,70 @@ | ||
| 1 | +import { ScoreBandPill } from "@airiskindex/ui"; | |
| 2 | +import { INDEX_VERSION } from "@airiskindex/scoring"; | |
| 3 | + | |
| 4 | +const CONCEPTS = [ | |
| 5 | + { | |
| 6 | + name: "Exposure", | |
| 7 | + description: | |
| 8 | + "AI is technically capable of performing the task. High exposure alone does not mean job loss — it means the occupation's tasks are changing.", | |
| 9 | + }, | |
| 10 | + { | |
| 11 | + name: "Substitution", | |
| 12 | + description: | |
| 13 | + "AI actually replaces the human performing the task, once cost, adoption and real-world barriers are accounted for.", | |
| 14 | + }, | |
| 15 | + { | |
| 16 | + name: "Augmentation", | |
| 17 | + description: | |
| 18 | + "AI assists the human, increasing productivity. For most occupations today, measured usage is augmentation, not replacement.", | |
| 19 | + }, | |
| 20 | +] as const; | |
| 21 | + | |
| 22 | +export default function HomePage(): JSX.Element { | |
| 23 | + return ( | |
| 24 | + <main className="mx-auto max-w-3xl px-6 py-16"> | |
| 25 | + <p className="text-sm font-medium uppercase tracking-wide text-slate-500"> | |
| 26 | + AI Risk Index · methodology {INDEX_VERSION} | |
| 27 | + </p> | |
| 28 | + <h1 className="mt-2 text-4xl font-bold tracking-tight"> | |
| 29 | + How is your occupation exposed to AI — task by task? | |
| 30 | + </h1> | |
| 31 | + <p className="mt-4 text-lg text-slate-600"> | |
| 32 | + We score occupations from their individual tasks, report three separate sub-scores with | |
| 33 | + confidence intervals, and publish every weight, prompt and formula. Built for adaptation | |
| 34 | + planning — not headlines. | |
| 35 | + </p> | |
| 36 | + | |
| 37 | + <div className="mt-8 flex flex-wrap gap-3"> | |
| 38 | + {/* Example occupation from the published methodology example */} | |
| 39 | + <ScoreBandPill label="Substitution" low={40.9} score={56.7} high={68.4} /> | |
| 40 | + <ScoreBandPill label="Exposure" low={42.0} score={57.1} high={74.1} /> | |
| 41 | + <ScoreBandPill label="Augmentation" low={46.9} score={71.9} high={84.4} /> | |
| 42 | + </div> | |
| 43 | + | |
| 44 | + <section className="mt-12 grid gap-6 sm:grid-cols-3"> | |
| 45 | + {CONCEPTS.map((concept) => ( | |
| 46 | + <div key={concept.name} className="rounded-lg border border-slate-200 p-5"> | |
| 47 | + <h2 className="font-semibold">{concept.name}</h2> | |
| 48 | + <p className="mt-2 text-sm text-slate-600">{concept.description}</p> | |
| 49 | + </div> | |
| 50 | + ))} | |
| 51 | + </section> | |
| 52 | + | |
| 53 | + <section className="mt-12 text-sm text-slate-600"> | |
| 54 | + <h2 className="text-base font-semibold text-slate-900">Public API</h2> | |
| 55 | + <ul className="mt-2 list-inside list-disc space-y-1"> | |
| 56 | + <li> | |
| 57 | + <code>/api/v1/occupations</code> — search occupations | |
| 58 | + </li> | |
| 59 | + <li> | |
| 60 | + <code>/api/v1/occupations/:code</code> — full score breakdown with sub-scores and CI | |
| 61 | + bounds | |
| 62 | + </li> | |
| 63 | + <li> | |
| 64 | + <code>/api/v1/methodology</code> — machine-readable weights and version metadata | |
| 65 | + </li> | |
| 66 | + </ul> | |
| 67 | + </section> | |
| 68 | + </main> | |
| 69 | + ); | |
| 70 | +} | |
added
apps/web/next-env.d.ts
+5 −0
@@ -0,0 +1,5 @@ | ||
| 1 | +/// <reference types="next" /> | |
| 2 | +/// <reference types="next/image-types/global" /> | |
| 3 | + | |
| 4 | +// NOTE: This file should not be edited | |
| 5 | +// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information. | |
added
apps/web/next.config.mjs
+9 −0
@@ -0,0 +1,9 @@ | ||
| 1 | +/** @type {import('next').NextConfig} */ | |
| 2 | +const nextConfig = { | |
| 3 | + // Workspace packages ship TypeScript sources directly. | |
| 4 | + transpilePackages: ["@airiskindex/scoring", "@airiskindex/db", "@airiskindex/ui"], | |
| 5 | + // Deployed as `node .next/standalone/server.js` behind ngrok (CLAUDE.md §7). | |
| 6 | + output: "standalone", | |
| 7 | +}; | |
| 8 | + | |
| 9 | +export default nextConfig; | |
added
apps/web/package.json
+28 −0
@@ -0,0 +1,28 @@ | ||
| 1 | +{ | |
| 2 | + "name": "@airiskindex/web", | |
| 3 | + "version": "0.0.0", | |
| 4 | + "private": true, | |
| 5 | + "scripts": { | |
| 6 | + "dev": "next dev", | |
| 7 | + "build": "next build", | |
| 8 | + "start": "next start", | |
| 9 | + "typecheck": "tsc --noEmit" | |
| 10 | + }, | |
| 11 | + "dependencies": { | |
| 12 | + "@airiskindex/db": "workspace:*", | |
| 13 | + "@airiskindex/scoring": "workspace:*", | |
| 14 | + "@airiskindex/ui": "workspace:*", | |
| 15 | + "next": "^14.2.5", | |
| 16 | + "react": "^18.3.1", | |
| 17 | + "react-dom": "^18.3.1" | |
| 18 | + }, | |
| 19 | + "devDependencies": { | |
| 20 | + "@types/node": "^20.14.11", | |
| 21 | + "@types/react": "^18.3.3", | |
| 22 | + "@types/react-dom": "^18.3.0", | |
| 23 | + "autoprefixer": "^10.4.19", | |
| 24 | + "postcss": "^8.4.39", | |
| 25 | + "tailwindcss": "^3.4.6", | |
| 26 | + "typescript": "^5.5.4" | |
| 27 | + } | |
| 28 | +} | |
added
apps/web/postcss.config.mjs
+6 −0
@@ -0,0 +1,6 @@ | ||
| 1 | +export default { | |
| 2 | + plugins: { | |
| 3 | + tailwindcss: {}, | |
| 4 | + autoprefixer: {}, | |
| 5 | + }, | |
| 6 | +}; | |
added
apps/web/tailwind.config.ts
+9 −0
@@ -0,0 +1,9 @@ | ||
| 1 | +import type { Config } from "tailwindcss"; | |
| 2 | + | |
| 3 | +export default { | |
| 4 | + content: ["./app/**/*.{ts,tsx}", "../../packages/ui/src/**/*.{ts,tsx}"], | |
| 5 | + theme: { | |
| 6 | + extend: {}, | |
| 7 | + }, | |
| 8 | + plugins: [], | |
| 9 | +} satisfies Config; | |
added
apps/web/tsconfig.json
+10 −0
@@ -0,0 +1,10 @@ | ||
| 1 | +{ | |
| 2 | + "extends": "../../packages/config/tsconfig/nextjs.json", | |
| 3 | + "compilerOptions": { | |
| 4 | + "paths": { | |
| 5 | + "@/*": ["./*"] | |
| 6 | + } | |
| 7 | + }, | |
| 8 | + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], | |
| 9 | + "exclude": ["node_modules"] | |
| 10 | +} | |
added
apps/worker/package.json
+25 −0
@@ -0,0 +1,25 @@ | ||
| 1 | +{ | |
| 2 | + "name": "@airiskindex/worker", | |
| 3 | + "version": "0.0.0", | |
| 4 | + "private": true, | |
| 5 | + "type": "module", | |
| 6 | + "scripts": { | |
| 7 | + "dev": "tsx watch src/index.ts", | |
| 8 | + "start": "tsx src/index.ts", | |
| 9 | + "score:recompute": "tsx src/scripts/recompute.ts", | |
| 10 | + "typecheck": "tsc --noEmit" | |
| 11 | + }, | |
| 12 | + "dependencies": { | |
| 13 | + "@airiskindex/db": "workspace:*", | |
| 14 | + "@airiskindex/scoring": "workspace:*", | |
| 15 | + "@anthropic-ai/sdk": ">=0.32.1 <1", | |
| 16 | + "bullmq": "^5.8.7", | |
| 17 | + "ioredis": "^5.4.1", | |
| 18 | + "zod": "^3.23.8" | |
| 19 | + }, | |
| 20 | + "devDependencies": { | |
| 21 | + "@types/node": "^20.14.11", | |
| 22 | + "tsx": "^4.16.2", | |
| 23 | + "typescript": "^5.5.4" | |
| 24 | + } | |
| 25 | +} | |
added
apps/worker/src/config.ts
+15 −0
@@ -0,0 +1,15 @@ | ||
| 1 | +/** | |
| 2 | + * Rater model panel — comma-separated model IDs from the environment, never | |
| 3 | + * hardcoded (CLAUDE.md §6). Multi-model rating is required: single-model | |
| 4 | + * exposure ratings show up to 19× spread across frontier raters | |
| 5 | + * (docs/research/01-existing-indices.md §7.2). | |
| 6 | + */ | |
| 7 | +export const RATER_MODELS: readonly string[] = (process.env.RATER_MODELS ?? "") | |
| 8 | + .split(",") | |
| 9 | + .map((value) => value.trim()) | |
| 10 | + .filter(Boolean); | |
| 11 | + | |
| 12 | +/** Bumping this invalidates cached ratings (CLAUDE.md §6). */ | |
| 13 | +export const RATER_PROMPT_VERSION = "v1"; | |
| 14 | + | |
| 15 | +export const REDIS_URL = process.env.REDIS_URL ?? "redis://127.0.0.1:6379"; | |
added
apps/worker/src/index.ts
+85 −0
@@ -0,0 +1,85 @@ | ||
| 1 | +import { Queue, Worker } from "bullmq"; | |
| 2 | +import IORedis from "ioredis"; | |
| 3 | +import { prisma } from "@airiskindex/db"; | |
| 4 | +import { RATER_MODELS, RATER_PROMPT_VERSION, REDIS_URL } from "./config"; | |
| 5 | +import { ingestBatchResults, isBatchComplete, submitRatingBatch, type RatingTask } from "./raters/batch"; | |
| 6 | +import { ratingJobId } from "./raters/job-id"; | |
| 7 | + | |
| 8 | +const connection = new IORedis(REDIS_URL, { maxRetriesPerRequest: null }); | |
| 9 | + | |
| 10 | +export const ratingQueue = new Queue("rating", { connection }); | |
| 11 | + | |
| 12 | +interface SubmitPayload { | |
| 13 | + model: string; | |
| 14 | +} | |
| 15 | + | |
| 16 | +interface PollPayload { | |
| 17 | + model: string; | |
| 18 | + batchId: string; | |
| 19 | + jobIdToTaskId: Record<string, string>; | |
| 20 | +} | |
| 21 | + | |
| 22 | +const worker = new Worker( | |
| 23 | + "rating", | |
| 24 | + async (job) => { | |
| 25 | + if (job.name === "submit") { | |
| 26 | + const { model } = job.data as SubmitPayload; | |
| 27 | + const tasks = await prisma.task.findMany({ | |
| 28 | + where: { | |
| 29 | + ratings: { none: { model, promptVersion: RATER_PROMPT_VERSION } }, | |
| 30 | + }, | |
| 31 | + include: { occupation: { select: { title: true } } }, | |
| 32 | + }); | |
| 33 | + if (tasks.length === 0) return { submitted: 0 }; | |
| 34 | + | |
| 35 | + const ratingTasks: RatingTask[] = tasks.map((task) => ({ | |
| 36 | + taskId: task.id, | |
| 37 | + occupationTitle: task.occupation.title, | |
| 38 | + statement: task.statement, | |
| 39 | + })); | |
| 40 | + const batchId = await submitRatingBatch(ratingTasks, model); | |
| 41 | + const jobIdToTaskId = Object.fromEntries( | |
| 42 | + ratingTasks.map((task) => [ratingJobId(task.taskId, model, RATER_PROMPT_VERSION), task.taskId]), | |
| 43 | + ); | |
| 44 | + await ratingQueue.add( | |
| 45 | + "poll", | |
| 46 | + { model, batchId, jobIdToTaskId } satisfies PollPayload, | |
| 47 | + { jobId: `poll:${batchId}`, delay: 60_000, attempts: 60, backoff: { type: "fixed", delay: 60_000 } }, | |
| 48 | + ); | |
| 49 | + return { submitted: ratingTasks.length, batchId }; | |
| 50 | + } | |
| 51 | + | |
| 52 | + if (job.name === "poll") { | |
| 53 | + const { model, batchId, jobIdToTaskId } = job.data as PollPayload; | |
| 54 | + if (!(await isBatchComplete(batchId))) { | |
| 55 | + throw new Error(`batch ${batchId} still processing`); // retried via backoff | |
| 56 | + } | |
| 57 | + return ingestBatchResults(batchId, model, new Map(Object.entries(jobIdToTaskId))); | |
| 58 | + } | |
| 59 | + | |
| 60 | + throw new Error(`unknown job ${job.name}`); | |
| 61 | + }, | |
| 62 | + { connection }, | |
| 63 | +); | |
| 64 | + | |
| 65 | +worker.on("failed", (job, error) => { | |
| 66 | + console.error(`[worker] job ${job?.name}:${job?.id} failed:`, error.message); | |
| 67 | +}); | |
| 68 | + | |
| 69 | +async function enqueueSubmitJobs(): Promise<void> { | |
| 70 | + if (RATER_MODELS.length < 2) { | |
| 71 | + console.warn( | |
| 72 | + "[worker] RATER_MODELS has fewer than 2 models — multi-model rating is required (CLAUDE.md §6).", | |
| 73 | + ); | |
| 74 | + } | |
| 75 | + for (const model of RATER_MODELS) { | |
| 76 | + await ratingQueue.add( | |
| 77 | + "submit", | |
| 78 | + { model } satisfies SubmitPayload, | |
| 79 | + { jobId: `submit:${model}:${RATER_PROMPT_VERSION}` }, | |
| 80 | + ); | |
| 81 | + } | |
| 82 | +} | |
| 83 | + | |
| 84 | +enqueueSubmitJobs().catch((error) => console.error("[worker] enqueue failed:", error)); | |
| 85 | +console.log(`[worker] rating worker up — prompt ${RATER_PROMPT_VERSION}, models: ${RATER_MODELS.join(", ") || "(none)"}`); | |
added
apps/worker/src/raters/batch.ts
+121 −0
@@ -0,0 +1,121 @@ | ||
| 1 | +import { readFileSync } from "node:fs"; | |
| 2 | +import Anthropic from "@anthropic-ai/sdk"; | |
| 3 | +import { prisma } from "@airiskindex/db"; | |
| 4 | +import { RATER_PROMPT_VERSION } from "../config"; | |
| 5 | +import { ratingJobId } from "./job-id"; | |
| 6 | +import { RATING_OUTPUT_JSON_SCHEMA, ratingResponseSchema } from "./schema"; | |
| 7 | + | |
| 8 | +// Batch rating flow (docs/research/03-llm-rater-api.md): one Message Batches | |
| 9 | +// request per task × model, custom_id = deterministic job ID, 1h-cached rubric | |
| 10 | +// in the system block, structured JSON output. 50% batch discount; results | |
| 11 | +// retrievable for 29 days. | |
| 12 | + | |
| 13 | +const anthropic = new Anthropic(); | |
| 14 | + | |
| 15 | +const rubric = readFileSync(new URL("./prompts/task-rating-v1.md", import.meta.url), "utf8"); | |
| 16 | + | |
| 17 | +export interface RatingTask { | |
| 18 | + taskId: string; | |
| 19 | + occupationTitle: string; | |
| 20 | + statement: string; | |
| 21 | +} | |
| 22 | + | |
| 23 | +export async function submitRatingBatch(tasks: RatingTask[], model: string): Promise<string> { | |
| 24 | + const batch = await anthropic.messages.batches.create({ | |
| 25 | + requests: tasks.map((task) => ({ | |
| 26 | + custom_id: ratingJobId(task.taskId, model, RATER_PROMPT_VERSION), | |
| 27 | + params: { | |
| 28 | + model, | |
| 29 | + max_tokens: 2048, | |
| 30 | + system: [ | |
| 31 | + { | |
| 32 | + type: "text" as const, | |
| 33 | + text: rubric, | |
| 34 | + cache_control: { type: "ephemeral" as const }, | |
| 35 | + }, | |
| 36 | + ], | |
| 37 | + messages: [ | |
| 38 | + { | |
| 39 | + role: "user" as const, | |
| 40 | + content: `Occupation: ${task.occupationTitle}\nTask statement: ${task.statement}\n\nRate this task per the rubric.`, | |
| 41 | + }, | |
| 42 | + ], | |
| 43 | + // Structured outputs (GA). Kept as an untyped extension so the code | |
| 44 | + // compiles across SDK versions; see docs/research/03-llm-rater-api.md. | |
| 45 | + ...({ | |
| 46 | + output_config: { | |
| 47 | + format: { type: "json_schema", schema: RATING_OUTPUT_JSON_SCHEMA }, | |
| 48 | + }, | |
| 49 | + } as Record<string, unknown>), | |
| 50 | + } as unknown as Anthropic.Messages.MessageCreateParamsNonStreaming, | |
| 51 | + })), | |
| 52 | + }); | |
| 53 | + return batch.id; | |
| 54 | +} | |
| 55 | + | |
| 56 | +export async function isBatchComplete(batchId: string): Promise<boolean> { | |
| 57 | + const batch = await anthropic.messages.batches.retrieve(batchId); | |
| 58 | + return batch.processing_status === "ended"; | |
| 59 | +} | |
| 60 | + | |
| 61 | +/** | |
| 62 | + * Ingest batch results: store the raw response and the parsed per-dimension | |
| 63 | + * scores (full audit trail, CLAUDE.md §6). Idempotent via the unique | |
| 64 | + * (taskId, dimension, model, promptVersion, sampleIndex) constraint. | |
| 65 | + */ | |
| 66 | +export async function ingestBatchResults( | |
| 67 | + batchId: string, | |
| 68 | + model: string, | |
| 69 | + jobIdToTaskId: ReadonlyMap<string, string>, | |
| 70 | +): Promise<{ ingested: number; failed: number }> { | |
| 71 | + let ingested = 0; | |
| 72 | + let failed = 0; | |
| 73 | + | |
| 74 | + for await (const entry of await anthropic.messages.batches.results(batchId)) { | |
| 75 | + const taskId = jobIdToTaskId.get(entry.custom_id); | |
| 76 | + if (!taskId || entry.result.type !== "succeeded") { | |
| 77 | + failed += 1; | |
| 78 | + continue; | |
| 79 | + } | |
| 80 | + const message = entry.result.message; | |
| 81 | + const textBlock = message.content.find((block) => block.type === "text"); | |
| 82 | + if (!textBlock || textBlock.type !== "text") { | |
| 83 | + failed += 1; | |
| 84 | + continue; | |
| 85 | + } | |
| 86 | + | |
| 87 | + const parsed = ratingResponseSchema.safeParse(JSON.parse(textBlock.text)); | |
| 88 | + if (!parsed.success) { | |
| 89 | + failed += 1; | |
| 90 | + continue; | |
| 91 | + } | |
| 92 | + | |
| 93 | + for (const [dimension, value] of Object.entries(parsed.data)) { | |
| 94 | + await prisma.taskRating.upsert({ | |
| 95 | + where: { | |
| 96 | + taskId_dimension_model_promptVersion_sampleIndex: { | |
| 97 | + taskId, | |
| 98 | + dimension, | |
| 99 | + model, | |
| 100 | + promptVersion: RATER_PROMPT_VERSION, | |
| 101 | + sampleIndex: 0, | |
| 102 | + }, | |
| 103 | + }, | |
| 104 | + update: {}, | |
| 105 | + create: { | |
| 106 | + taskId, | |
| 107 | + dimension, | |
| 108 | + model, | |
| 109 | + promptVersion: RATER_PROMPT_VERSION, | |
| 110 | + sampleIndex: 0, | |
| 111 | + rating: value.rating, | |
| 112 | + rationale: value.rationale, | |
| 113 | + rawResponse: JSON.parse(JSON.stringify(message)), | |
| 114 | + }, | |
| 115 | + }); | |
| 116 | + ingested += 1; | |
| 117 | + } | |
| 118 | + } | |
| 119 | + | |
| 120 | + return { ingested, failed }; | |
| 121 | +} | |
added
apps/worker/src/raters/job-id.ts
+14 −0
@@ -0,0 +1,14 @@ | ||
| 1 | +import { createHash } from "node:crypto"; | |
| 2 | + | |
| 3 | +/** | |
| 4 | + * Deterministic ID used both as the BullMQ job ID and the Message Batches | |
| 5 | + * `custom_id`, so retries and batch reconciliation are idempotent | |
| 6 | + * (CLAUDE.md §6; docs/research/03-llm-rater-api.md). One request rates all | |
| 7 | + * dimensions of one task with one model. | |
| 8 | + */ | |
| 9 | +export function ratingJobId(taskId: string, model: string, promptVersion: string): string { | |
| 10 | + return createHash("sha256") | |
| 11 | + .update(`${taskId}:${model}:${promptVersion}`) | |
| 12 | + .digest("hex") | |
| 13 | + .slice(0, 32); | |
| 14 | +} | |
added
apps/worker/src/raters/prompts/task-rating-v1.md
+70 −0
@@ -0,0 +1,70 @@ | ||
| 1 | +# Task rating rubric — v1 | |
| 2 | + | |
| 3 | +You are an expert rater for the AI Risk Index (airiskindex.io). You rate one | |
| 4 | +occupational task statement (from O*NET) on six dimensions, each on a 1–5 | |
| 5 | +integer scale. Be calibrated and conservative: rate what current, generally | |
| 6 | +available AI systems (including tool-using agents) can do **today**, not what | |
| 7 | +might be possible soon. Justify every rating in one or two sentences grounded | |
| 8 | +in the task statement itself. | |
| 9 | + | |
| 10 | +## Dimensions | |
| 11 | + | |
| 12 | +### automatability (1–5) | |
| 13 | +Could current AI perform this task end-to-end with **at least 50% time saving | |
| 14 | +at equal quality** (Eloundou et al. threshold)? | |
| 15 | +- 1 — No meaningful part of the task can be automated today. | |
| 16 | +- 3 — Roughly half of the task could be automated with significant setup. | |
| 17 | +- 5 — The full task meets the ≥50%-time-saving-at-equal-quality bar with off-the-shelf systems. | |
| 18 | + | |
| 19 | +### feasibility (1–5) | |
| 20 | +Do **deployed products demonstrably perform this task reliably today**? Distinguish | |
| 21 | +conceivable from deployable: benchmark results and demos rate lower than | |
| 22 | +production systems in real organizations. | |
| 23 | +- 1 — No product does this; research-stage only. | |
| 24 | +- 3 — Products exist but with material error rates or narrow scope. | |
| 25 | +- 5 — Mature products perform this reliably in production at scale. | |
| 26 | + | |
| 27 | +### cost_ratio (1–5) | |
| 28 | +Compare the AI cost per task-equivalent (inference + integration + oversight) | |
| 29 | +to the loaded human wage for the same output. | |
| 30 | +- 1 — AI is more expensive than the human, all-in. | |
| 31 | +- 3 — Roughly comparable cost. | |
| 32 | +- 5 — AI is at least an order of magnitude cheaper. | |
| 33 | + | |
| 34 | +### barriers (1–5) — NOTE: higher = MORE protected | |
| 35 | +Strength of adoption barriers: licensing/authorization requirements, liability | |
| 36 | +and error-cost asymmetry, regulatory coverage of the automation itself, | |
| 37 | +human-contact requirement, organizational friction. | |
| 38 | +- 1 — No meaningful barriers; nothing prevents substitution. | |
| 39 | +- 3 — Some friction (oversight requirements, customer preference for humans). | |
| 40 | +- 5 — Hard barriers: a licensed human must legally perform or sign off on the task. | |
| 41 | + | |
| 42 | +### adoption_velocity (1–5) | |
| 43 | +How fast and deep are the sectors where this task occurs actually adopting AI | |
| 44 | +(agents in production, measured displacement), per public adoption data? | |
| 45 | +- 1 — Laggard sectors (small firms, physical, low digitization). | |
| 46 | +- 3 — Middling adoption, pilots common, production rare. | |
| 47 | +- 5 — Fast, deep adoption (information, finance, professional services patterns). | |
| 48 | + | |
| 49 | +### augmentation (1–5) | |
| 50 | +Independently of replacement: does AI **assist** a human doing this task, | |
| 51 | +raising their productivity? High augmentation and low automatability can | |
| 52 | +coexist (assistive drafting for a task requiring human judgment). | |
| 53 | +- 1 — AI offers no meaningful assistance. | |
| 54 | +- 3 — Useful assistance on parts of the task. | |
| 55 | +- 5 — AI transforms productivity on this task while the human stays in the loop. | |
| 56 | + | |
| 57 | +## Output | |
| 58 | + | |
| 59 | +Return ONLY a JSON object of this shape (no prose outside JSON): | |
| 60 | + | |
| 61 | +```json | |
| 62 | +{ | |
| 63 | + "automatability": { "rating": 1, "rationale": "..." }, | |
| 64 | + "feasibility": { "rating": 1, "rationale": "..." }, | |
| 65 | + "cost_ratio": { "rating": 1, "rationale": "..." }, | |
| 66 | + "barriers": { "rating": 1, "rationale": "..." }, | |
| 67 | + "adoption_velocity": { "rating": 1, "rationale": "..." }, | |
| 68 | + "augmentation": { "rating": 1, "rationale": "..." } | |
| 69 | +} | |
| 70 | +``` | |
added
apps/worker/src/raters/schema.ts
+45 −0
@@ -0,0 +1,45 @@ | ||
| 1 | +import { z } from "zod"; | |
| 2 | +import { DIMENSIONS } from "@airiskindex/scoring"; | |
| 3 | + | |
| 4 | +const RATED_DIMENSIONS = [...DIMENSIONS, "augmentation"] as const; | |
| 5 | + | |
| 6 | +const dimensionRating = z.object({ | |
| 7 | + rating: z.number().int().min(1).max(5), | |
| 8 | + rationale: z.string(), | |
| 9 | +}); | |
| 10 | + | |
| 11 | +/** Parsed shape of one rater response (all six dimensions of one task). */ | |
| 12 | +export const ratingResponseSchema = z.object( | |
| 13 | + Object.fromEntries(RATED_DIMENSIONS.map((dimension) => [dimension, dimensionRating])) as Record< | |
| 14 | + (typeof RATED_DIMENSIONS)[number], | |
| 15 | + typeof dimensionRating | |
| 16 | + >, | |
| 17 | +); | |
| 18 | + | |
| 19 | +export type RatingResponse = z.infer<typeof ratingResponseSchema>; | |
| 20 | + | |
| 21 | +/** | |
| 22 | + * JSON schema for the structured-outputs feature (score as enum — numeric | |
| 23 | + * min/max is unsupported there; docs/research/03-llm-rater-api.md). | |
| 24 | + */ | |
| 25 | +export const RATING_OUTPUT_JSON_SCHEMA = { | |
| 26 | + type: "object", | |
| 27 | + additionalProperties: false, | |
| 28 | + required: [...RATED_DIMENSIONS], | |
| 29 | + properties: Object.fromEntries( | |
| 30 | + RATED_DIMENSIONS.map((dimension) => [ | |
| 31 | + dimension, | |
| 32 | + { | |
| 33 | + type: "object", | |
| 34 | + additionalProperties: false, | |
| 35 | + required: ["rating", "rationale"], | |
| 36 | + properties: { | |
| 37 | + rating: { enum: [1, 2, 3, 4, 5] }, | |
| 38 | + rationale: { type: "string" }, | |
| 39 | + }, | |
| 40 | + }, | |
| 41 | + ]), | |
| 42 | + ), | |
| 43 | +} as const; | |
| 44 | + | |
| 45 | +export { RATED_DIMENSIONS }; | |
added
apps/worker/src/scripts/recompute.ts
+136 −0
@@ -0,0 +1,136 @@ | ||
| 1 | +import { prisma } from "@airiskindex/db"; | |
| 2 | +import { | |
| 3 | + INDEX_VERSION, | |
| 4 | + scoreOccupation, | |
| 5 | + type RatingBand, | |
| 6 | + type TaskInput, | |
| 7 | + type TaskRatings, | |
| 8 | +} from "@airiskindex/scoring"; | |
| 9 | +import { RATED_DIMENSIONS } from "../raters/schema"; | |
| 10 | +import { RATER_MODELS, RATER_PROMPT_VERSION } from "../config"; | |
| 11 | + | |
| 12 | +// Full index recomputation (`pnpm score:recompute`): builds rating bands from | |
| 13 | +// the multi-model panel (min/mean/max across models — METHODOLOGY.md §3), | |
| 14 | +// applies expert overrides, scores every occupation, and writes ONE new | |
| 15 | +// immutable ScoreRun. Historical runs are never mutated (CLAUDE.md §9). | |
| 16 | + | |
| 17 | +function bandFromPanel(values: number[]): RatingBand { | |
| 18 | + const low = Math.min(...values); | |
| 19 | + const high = Math.max(...values); | |
| 20 | + const mid = values.reduce((sum, value) => sum + value, 0) / values.length; | |
| 21 | + return { low, mid, high }; | |
| 22 | +} | |
| 23 | + | |
| 24 | +async function main(): Promise<void> { | |
| 25 | + const occupations = await prisma.occupation.findMany({ | |
| 26 | + include: { | |
| 27 | + tasks: { | |
| 28 | + include: { | |
| 29 | + ratings: { where: { promptVersion: RATER_PROMPT_VERSION } }, | |
| 30 | + overrides: true, | |
| 31 | + }, | |
| 32 | + }, | |
| 33 | + }, | |
| 34 | + }); | |
| 35 | + | |
| 36 | + const run = await prisma.scoreRun.create({ | |
| 37 | + data: { | |
| 38 | + indexVersion: INDEX_VERSION, | |
| 39 | + raterPromptVersion: RATER_PROMPT_VERSION, | |
| 40 | + raterModels: [...RATER_MODELS], | |
| 41 | + }, | |
| 42 | + }); | |
| 43 | + | |
| 44 | + let scoredOccupations = 0; | |
| 45 | + let skippedTasks = 0; | |
| 46 | + | |
| 47 | + for (const occupation of occupations) { | |
| 48 | + const inputs: TaskInput[] = []; | |
| 49 | + | |
| 50 | + for (const task of occupation.tasks) { | |
| 51 | + const bands: Partial<Record<(typeof RATED_DIMENSIONS)[number], RatingBand>> = {}; | |
| 52 | + let complete = true; | |
| 53 | + | |
| 54 | + for (const dimension of RATED_DIMENSIONS) { | |
| 55 | + const override = task.overrides.find((entry) => entry.dimension === dimension); | |
| 56 | + if (override) { | |
| 57 | + bands[dimension] = { | |
| 58 | + low: override.ratingLow, | |
| 59 | + mid: override.ratingMid, | |
| 60 | + high: override.ratingHigh, | |
| 61 | + }; | |
| 62 | + continue; | |
| 63 | + } | |
| 64 | + const values = task.ratings | |
| 65 | + .filter((entry) => entry.dimension === dimension) | |
| 66 | + .map((entry) => entry.rating); | |
| 67 | + if (values.length === 0) { | |
| 68 | + complete = false; | |
| 69 | + break; | |
| 70 | + } | |
| 71 | + bands[dimension] = bandFromPanel(values); | |
| 72 | + } | |
| 73 | + | |
| 74 | + if (!complete) { | |
| 75 | + skippedTasks += 1; | |
| 76 | + continue; | |
| 77 | + } | |
| 78 | + inputs.push({ | |
| 79 | + taskId: task.id, | |
| 80 | + importance: task.importance ?? undefined, | |
| 81 | + ratings: bands as TaskRatings, | |
| 82 | + }); | |
| 83 | + } | |
| 84 | + | |
| 85 | + if (inputs.length === 0) continue; | |
| 86 | + | |
| 87 | + const scores = scoreOccupation(inputs); | |
| 88 | + await prisma.$transaction([ | |
| 89 | + prisma.occupationScore.create({ | |
| 90 | + data: { | |
| 91 | + runId: run.id, | |
| 92 | + occupationCode: occupation.code, | |
| 93 | + substitutionLow: scores.substitution.low, | |
| 94 | + substitution: scores.substitution.score, | |
| 95 | + substitutionHigh: scores.substitution.high, | |
| 96 | + exposureLow: scores.exposure.low, | |
| 97 | + exposure: scores.exposure.score, | |
| 98 | + exposureHigh: scores.exposure.high, | |
| 99 | + augmentationLow: scores.augmentation.low, | |
| 100 | + augmentation: scores.augmentation.score, | |
| 101 | + augmentationHigh: scores.augmentation.high, | |
| 102 | + highlyExposedTaskShare: scores.highlyExposedTaskShare, | |
| 103 | + }, | |
| 104 | + }), | |
| 105 | + ...scores.tasks.map((task) => | |
| 106 | + prisma.taskScore.create({ | |
| 107 | + data: { | |
| 108 | + runId: run.id, | |
| 109 | + taskId: task.taskId, | |
| 110 | + substitutionLow: task.substitution.low, | |
| 111 | + substitution: task.substitution.score, | |
| 112 | + substitutionHigh: task.substitution.high, | |
| 113 | + exposureLow: task.exposure.low, | |
| 114 | + exposure: task.exposure.score, | |
| 115 | + exposureHigh: task.exposure.high, | |
| 116 | + augmentationLow: task.augmentation.low, | |
| 117 | + augmentation: task.augmentation.score, | |
| 118 | + augmentationHigh: task.augmentation.high, | |
| 119 | + }, | |
| 120 | + }), | |
| 121 | + ), | |
| 122 | + ]); | |
| 123 | + scoredOccupations += 1; | |
| 124 | + } | |
| 125 | + | |
| 126 | + console.log( | |
| 127 | + `Run ${run.id} (index ${INDEX_VERSION}, prompt ${RATER_PROMPT_VERSION}): scored ${scoredOccupations}/${occupations.length} occupations; skipped ${skippedTasks} unrated tasks.`, | |
| 128 | + ); | |
| 129 | +} | |
| 130 | + | |
| 131 | +main() | |
| 132 | + .catch((error) => { | |
| 133 | + console.error(error); | |
| 134 | + process.exitCode = 1; | |
| 135 | + }) | |
| 136 | + .finally(() => prisma.$disconnect()); | |
added
apps/worker/tsconfig.json
+4 −0
@@ -0,0 +1,4 @@ | ||
| 1 | +{ | |
| 2 | + "extends": "../../packages/config/tsconfig/base.json", | |
| 3 | + "include": ["src"] | |
| 4 | +} | |
added
data/derived/expert_overrides/.gitkeep
+0 −0
added
data/derived/ratings/.gitkeep
+0 −0
added
data/raw/.gitkeep
+0 −0
added
docs/methodology/CHANGELOG.md
+19 −0
@@ -0,0 +1,19 @@ | ||
| 1 | +# Methodology Changelog | |
| 2 | + | |
| 3 | +All notable changes to the scoring methodology. Every entry corresponds to an | |
| 4 | +`INDEX_VERSION` (semver) in `packages/scoring/src/version.ts`. | |
| 5 | + | |
| 6 | +## [1.0.0] — UNRELEASED (draft) | |
| 7 | + | |
| 8 | +Initial methodology. | |
| 9 | + | |
| 10 | +- Task-based scoring on O*NET 30.x task statements, importance-weighted aggregation | |
| 11 | + to occupations (O*NET-SOC 2019). | |
| 12 | +- Five dimensions: automatability 0.35, feasibility 0.20, cost_ratio 0.15, | |
| 13 | + barriers 0.20 (inverted), adoption_velocity 0.10. | |
| 14 | +- Three sub-scores per occupation: exposure, substitution (headline composite), | |
| 15 | + augmentation — augmentation rated separately per task, outside the composite. | |
| 16 | +- Multi-model LLM rater panel (≥2 frontier models via `RATER_MODELS`); confidence | |
| 17 | + bounds (`score_low`/`score_high`) derived from rater disagreement envelopes. | |
| 18 | +- 5% human review sample; expert Delphi overrides replace LLM bands where triggered. | |
| 19 | +- Grounding research: `docs/research/01-…04-*.md` (compiled 2026-08-05). | |
added
docs/methodology/METHODOLOGY.md
+173 −0
@@ -0,0 +1,173 @@ | ||
| 1 | +# AI Risk Index — Methodology v1.0.0 (DRAFT) | |
| 2 | + | |
| 3 | +> Status: **draft, unreleased**. This document is the source of truth for the scoring | |
| 4 | +> methodology (see CLAUDE.md §9). Code in `packages/scoring` links to the section | |
| 5 | +> anchors below; if code and this document disagree, stop and flag it. | |
| 6 | +> | |
| 7 | +> Related work and evidence base: `docs/research/01-existing-indices.md` (indices), | |
| 8 | +> `02-data-sources.md` (data), `04-landscape-and-evidence.md` (adoption & barriers evidence). | |
| 9 | + | |
| 10 | +--- | |
| 11 | + | |
| 12 | +## 1. Principles <a name="principles"></a> | |
| 13 | + | |
| 14 | +1. **Task-based, not occupation-based.** Occupations are bundles of tasks with very | |
| 15 | + different AI exposure; whole-occupation scoring (Frey & Osborne 2013) has a poor | |
| 16 | + empirical record. Occupation scores are always *derived* from task scores | |
| 17 | + (Arntz, Gregory & Zierahn 2016; Eloundou et al. 2024). | |
| 18 | +2. **Three concepts, never collapsed** (see §5): | |
| 19 | + - **Exposure** — AI is technically capable of performing the task. | |
| 20 | + - **Substitution** — AI actually replaces the human performing it. This is the | |
| 21 | + headline composite. | |
| 22 | + - **Augmentation** — AI assists the human, increasing productivity. | |
| 23 | + Realized labor-market effects concentrate in *automation-classified* usage, not | |
| 24 | + augmentation (Brynjolfsson, Chandar & Chen 2025), so conflating the three is not | |
| 25 | + just imprecise — it is empirically wrong. | |
| 26 | +3. **Uncertainty is part of the score.** Every published score carries | |
| 27 | + `score_low / score / score_high`. Interval width is driven primarily by | |
| 28 | + disagreement between independent LLM raters (single-model ratings show up to a | |
| 29 | + 19× spread in headline statistics across frontier models — Yin, Vu & Persico 2026) | |
| 30 | + plus human-calibration error on the expert anchor set. | |
| 31 | +4. **Fully reproducible.** Weights, formulas, prompt versions, rater model IDs and | |
| 32 | + per-model ratings are all published. Every score traces to a stored | |
| 33 | + `score_runs` row; historical runs are immutable. | |
| 34 | +5. **Versioned.** Any change to weights, formulas, or rater prompts bumps | |
| 35 | + `INDEX_VERSION` (semver) with a changelog entry and regenerated sensitivity outputs. | |
| 36 | + | |
| 37 | +## 2. Data <a name="data"></a> | |
| 38 | + | |
| 39 | +| Input | Source | Role | | |
| 40 | +|---|---|---| | |
| 41 | +| Occupations & task statements | O*NET 30.x (O*NET-SOC 2019 taxonomy), CC BY 4.0 | Unit of analysis (~18k tasks, ~900 data-level occupations) | | |
| 42 | +| Task importance weights | O*NET Task Ratings (IM scale 1–5) | Aggregation weights (§6) | | |
| 43 | +| EU/France occupations | ESCO v1.2.x + official ESCO↔O*NET crosswalk; ROME 4.0 | Crosswalked scores (crosswalk loss documented per occupation) | | |
| 44 | +| Wages | BLS OEWS (latest May release); Eurostat SES; INSEE | `cost_ratio` denominator | | |
| 45 | +| Adoption data | Census BTOS AI supplement, Ramp AI Index, Anthropic Economic Index (Hugging Face), Challenger reports | `adoption_velocity` inputs (§4.5) | | |
| 46 | +| Human anchor ratings | Expert Delphi panel (`data/derived/expert_overrides/`) | LLM-rater calibration (§3) | | |
| 47 | + | |
| 48 | +Raw dumps are immutable (`data/raw/`, gitignored); all transformations go through | |
| 49 | +`apps/etl`; derived artifacts commit only manifests (hashes + row counts). | |
| 50 | + | |
| 51 | +## 3. Task rating (LLM-as-evaluator) <a name="task-rating"></a> | |
| 52 | + | |
| 53 | +Each O*NET task statement is rated on a **5-point scale** per dimension (§4) by a | |
| 54 | +**panel of ≥2 (target 3) frontier LLMs** (`RATER_MODELS`), using versioned rubric | |
| 55 | +prompts (`apps/worker/src/raters/prompts/`). Per task × dimension: | |
| 56 | + | |
| 57 | +- `rating_mid` = mean of panel ratings; | |
| 58 | +- `rating_low` / `rating_high` = min / max of panel ratings (rater-disagreement band). | |
| 59 | + | |
| 60 | +Rubric anchors follow the citable standards: automatability uses the Eloundou et al. | |
| 61 | +"≥50% time saving at equal quality" threshold, decomposed into named criteria | |
| 62 | +(SML-style multi-criterion rubric); ratings require structured justifications and are | |
| 63 | +stored with model ID, prompt version and raw response (full audit trail). | |
| 64 | + | |
| 65 | +**Calibration:** 5% of ratings are sampled for human review; panel-vs-human | |
| 66 | +disagreement > 1 point routes the task to the expert queue. Expert overrides replace | |
| 67 | +the LLM band for that task and are flagged in the API output. | |
| 68 | + | |
| 69 | +## 4. Dimensions <a name="dimensions"></a> | |
| 70 | + | |
| 71 | +Composite weights live in `packages/scoring/src/weights.ts` and are published at | |
| 72 | +`/api/v1/methodology`. Weights sum to 1.0. | |
| 73 | + | |
| 74 | +| Dimension | Key | Weight | Orientation | Rubric anchor (rating of 5 means…) | | |
| 75 | +|---|---|---|---|---| | |
| 76 | +| Task automatability | `automatability` | 0.35 | direct | Current AI (incl. tooling/agents) can do the task with ≥50% time saving at equal quality | | |
| 77 | +| Current technical feasibility | `feasibility` | 0.20 | direct | Deployed products demonstrably perform this task reliably today (not merely conceivable) | | |
| 78 | +| Cost of substitution vs. wage | `cost_ratio` | 0.15 | direct | AI cost per task-equivalent ≪ loaded human wage for the same output | | |
| 79 | +| Adoption barriers | `barriers` | 0.20 | **inverted** | Strong barriers: licensing/liability/regulation/human-contact requirements block substitution | | |
| 80 | +| Sector adoption velocity | `adoption_velocity` | 0.10 | direct | Occupation's dominant sectors adopt AI fast and deep (agents in production, measured displacement) | | |
| 81 | + | |
| 82 | +Orientation: a *direct* dimension's higher rating increases substitution pressure; an | |
| 83 | +*inverted* dimension's higher rating decreases it. `barriers` is the only inverted | |
| 84 | +dimension in v1. Orientation is encoded once, in `packages/scoring/src/weights.ts`, | |
| 85 | +next to the weights. | |
| 86 | + | |
| 87 | +Additionally, **`augmentation`** is rated per task on the same 5-point scale (does AI | |
| 88 | +assist the human on this task, raising productivity without replacing them?). It is | |
| 89 | +**not** part of the substitution composite; it feeds the augmentation sub-score (§5). | |
| 90 | + | |
| 91 | +### 4.4 `barriers` components <a name="barriers"></a> | |
| 92 | +Rated against five named criteria (see research doc 04): licensing/authorization | |
| 93 | +requirement (0.30), liability & error-cost asymmetry (0.25), regulatory-process | |
| 94 | +coverage (0.20, jurisdiction-specific), human-contact requirement (0.15), | |
| 95 | +organizational friction (0.10). Pre-2022 null results (OECD 2021; Acemoglu et al. | |
| 96 | +2022) show barriers dominate short-run outcomes — hence the 0.20 weight. | |
| 97 | + | |
| 98 | +### 4.5 `adoption_velocity` components <a name="adoption-velocity"></a> | |
| 99 | +Grounded in *measured* adoption, not forecasts: sector AI-use rate employment-weighted | |
| 100 | +(0.35), sector adoption momentum (0.20), agentic deployment depth (0.20), realized | |
| 101 | +displacement intensity (0.15), occupation-level usage intensity from the Anthropic | |
| 102 | +Economic Index (0.10). Refreshed each index release; sources are public and dated. | |
| 103 | + | |
| 104 | +## 5. Scoring formulas <a name="formulas"></a> | |
| 105 | + | |
| 106 | +All formulas are implemented, pure and deterministic, in `packages/scoring`. | |
| 107 | +Ratings `r ∈ [1,5]` normalize to pressure `p ∈ [0,1]`: | |
| 108 | + | |
| 109 | +``` | |
| 110 | +p = (r − 1) / 4 (direct dimensions) | |
| 111 | +p = 1 − (r − 1) / 4 (inverted dimensions) | |
| 112 | +``` | |
| 113 | + | |
| 114 | +Per task: | |
| 115 | + | |
| 116 | +``` | |
| 117 | +substitution_task = 100 · Σ_d w_d · p_d (all five dimensions) | |
| 118 | +exposure_task = 100 · (w_auto·p_auto + w_feas·p_feas) / (w_auto + w_feas) | |
| 119 | +augmentation_task = 100 · p_augmentation | |
| 120 | +``` | |
| 121 | + | |
| 122 | +Confidence bounds: `score_low` is computed with each dimension's | |
| 123 | +pressure-minimizing rating bound (for direct dimensions the low rating; for inverted | |
| 124 | +dimensions the **high** rating), `score_high` symmetrically. Bounds are therefore | |
| 125 | +worst/best-case envelopes over rater disagreement, and `low ≤ score ≤ high` always | |
| 126 | +holds. | |
| 127 | + | |
| 128 | +## 6. Aggregation to occupations <a name="aggregation"></a> | |
| 129 | + | |
| 130 | +Occupation scores are the **importance-weighted mean** of task scores, weights from | |
| 131 | +O*NET Task Ratings importance (IM, 1–5), normalized to sum to 1 within the | |
| 132 | +occupation. Applied identically to `low`, `score` and `high`. Tasks lacking | |
| 133 | +importance ratings receive the occupation-mean importance. | |
| 134 | + | |
| 135 | +The API additionally reports the share of tasks with `substitution_task ≥ 70` | |
| 136 | +("highly exposed task share") — this, not the composite alone, is the preferred | |
| 137 | +headline in UI copy ("X% of tasks in this occupation are highly exposed"). | |
| 138 | + | |
| 139 | +## 7. Versioning & runs <a name="versioning"></a> | |
| 140 | + | |
| 141 | +- `INDEX_VERSION` (semver) in `packages/scoring/src/version.ts`. | |
| 142 | +- MAJOR: formula/weight changes. MINOR: data-source version bumps (new O*NET release, | |
| 143 | + new adoption data). PATCH: recomputation with refreshed adoption inputs, prompt | |
| 144 | + clarifications that don't change the rubric semantics. | |
| 145 | +- Every computation writes a `score_runs` row (index version, prompt version, rater | |
| 146 | + models). Old runs stay queryable forever; the API serves the latest by default and | |
| 147 | + any run on request. | |
| 148 | + | |
| 149 | +## 8. Validation & sensitivity <a name="validation"></a> | |
| 150 | + | |
| 151 | +Published with every MAJOR/MINOR release under `docs/methodology/sensitivity/`: | |
| 152 | + | |
| 153 | +1. **Convergent validity:** Spearman correlation of our occupation scores against | |
| 154 | + Felten AIOE, Eloundou β, and ILO WP140 gradients (ρ ≈ 0.84 between independent | |
| 155 | + modern methodologies is the reference bar). | |
| 156 | +2. **Rater stability:** cross-model agreement distribution; occupations with the | |
| 157 | + widest bands flagged in-product. | |
| 158 | +3. **Outcome tracking:** correlation against the Stanford "Canaries" dashboard | |
| 159 | + (entry-level employment in exposed occupations) and AEI usage shares — exposure | |
| 160 | + indices individually explain <11% of realized unemployment risk (Frank, Ahn & | |
| 161 | + Moro 2025), so we report outcome tracking honestly rather than claiming prediction. | |
| 162 | +4. **Weight sensitivity:** composite rank stability under ±25% perturbation of each | |
| 163 | + weight. | |
| 164 | + | |
| 165 | +## 9. Known limitations <a name="limitations"></a> | |
| 166 | + | |
| 167 | +- LLM raters co-evolve with the technology they measure (the "ruler" problem); | |
| 168 | + multi-model panels bound but do not eliminate this. | |
| 169 | +- Sector-level adoption inputs are priors, corrected by occupation-level usage data. | |
| 170 | +- ESCO/ROME crosswalked scores inherit crosswalk loss; flagged per occupation. | |
| 171 | +- Scores describe *tasks as currently constituted*; occupations reorganize. | |
| 172 | +- This index measures exposure and substitution *pressure*, not certainty of job | |
| 173 | + loss. Product copy follows the adaptation-not-doom tone guide accordingly. | |
added
docs/methodology/examples/example-analyst.json
+74 −0
@@ -0,0 +1,74 @@ | ||
| 1 | +{ | |
| 2 | + "$comment": "Worked example for METHODOLOGY.md §5-6. Hand-computed with v1 weights (automatability .35, feasibility .20, cost_ratio .15, barriers .20 inverted, adoption_velocity .10). packages/scoring snapshot tests assert against `expected` to 3 decimal places. Any change here requires an INDEX_VERSION bump.", | |
| 3 | + "occupation": { | |
| 4 | + "code": "99-9999.00", | |
| 5 | + "title": "Example Analyst (synthetic)" | |
| 6 | + }, | |
| 7 | + "tasks": [ | |
| 8 | + { | |
| 9 | + "taskId": "T1", | |
| 10 | + "importance": 4, | |
| 11 | + "ratings": { | |
| 12 | + "automatability": { "low": 3, "mid": 4, "high": 5 }, | |
| 13 | + "feasibility": { "low": 4, "mid": 4, "high": 5 }, | |
| 14 | + "cost_ratio": { "low": 3, "mid": 3, "high": 4 }, | |
| 15 | + "barriers": { "low": 2, "mid": 2, "high": 3 }, | |
| 16 | + "adoption_velocity": { "low": 3, "mid": 4, "high": 4 }, | |
| 17 | + "augmentation": { "low": 4, "mid": 5, "high": 5 } | |
| 18 | + } | |
| 19 | + }, | |
| 20 | + { | |
| 21 | + "taskId": "T2", | |
| 22 | + "importance": 3, | |
| 23 | + "ratings": { | |
| 24 | + "automatability": { "low": 1, "mid": 2, "high": 2 }, | |
| 25 | + "feasibility": { "low": 2, "mid": 2, "high": 3 }, | |
| 26 | + "cost_ratio": { "low": 2, "mid": 2, "high": 2 }, | |
| 27 | + "barriers": { "low": 4, "mid": 4, "high": 5 }, | |
| 28 | + "adoption_velocity": { "low": 2, "mid": 3, "high": 3 }, | |
| 29 | + "augmentation": { "low": 2, "mid": 3, "high": 4 } | |
| 30 | + } | |
| 31 | + }, | |
| 32 | + { | |
| 33 | + "taskId": "T3", | |
| 34 | + "importance": 1, | |
| 35 | + "ratings": { | |
| 36 | + "automatability": { "low": 5, "mid": 5, "high": 5 }, | |
| 37 | + "feasibility": { "low": 2, "mid": 3, "high": 4 }, | |
| 38 | + "cost_ratio": { "low": 4, "mid": 4, "high": 5 }, | |
| 39 | + "barriers": { "low": 1, "mid": 1, "high": 2 }, | |
| 40 | + "adoption_velocity": { "low": 4, "mid": 5, "high": 5 }, | |
| 41 | + "augmentation": { "low": 1, "mid": 2, "high": 3 } | |
| 42 | + } | |
| 43 | + } | |
| 44 | + ], | |
| 45 | + "expected": { | |
| 46 | + "taskScores": [ | |
| 47 | + { | |
| 48 | + "taskId": "T1", | |
| 49 | + "substitution": { "low": 55.0, "score": 71.25, "high": 88.75 }, | |
| 50 | + "exposure": { "low": 59.0909, "score": 75.0, "high": 100.0 }, | |
| 51 | + "augmentation": { "low": 75.0, "score": 100.0, "high": 100.0 } | |
| 52 | + }, | |
| 53 | + { | |
| 54 | + "taskId": "T2", | |
| 55 | + "substitution": { "low": 11.25, "score": 27.5, "high": 32.5 }, | |
| 56 | + "exposure": { "low": 9.0909, "score": 25.0, "high": 34.0909 }, | |
| 57 | + "augmentation": { "low": 25.0, "score": 50.0, "high": 75.0 } | |
| 58 | + }, | |
| 59 | + { | |
| 60 | + "taskId": "T3", | |
| 61 | + "substitution": { "low": 73.75, "score": 86.25, "high": 95.0 }, | |
| 62 | + "exposure": { "low": 72.7273, "score": 81.8182, "high": 90.9091 }, | |
| 63 | + "augmentation": { "low": 0.0, "score": 25.0, "high": 50.0 } | |
| 64 | + } | |
| 65 | + ], | |
| 66 | + "occupation": { | |
| 67 | + "taskWeights": [0.5, 0.375, 0.125], | |
| 68 | + "substitution": { "low": 40.9375, "score": 56.71875, "high": 68.4375 }, | |
| 69 | + "exposure": { "low": 42.0455, "score": 57.1023, "high": 74.1477 }, | |
| 70 | + "augmentation": { "low": 46.875, "score": 71.875, "high": 84.375 }, | |
| 71 | + "highlyExposedTaskShare": 0.6667 | |
| 72 | + } | |
| 73 | + } | |
| 74 | +} | |
added
docs/research/01-existing-indices.md
+406 −0
@@ -0,0 +1,406 @@ | ||
| 1 | +# Existing AI Job-Exposure / Automation-Risk Indices and Methodologies | |
| 2 | + | |
| 3 | +**Research memo — AI Risk Index (airiskindex.io)** | |
| 4 | +**Date compiled:** 2026-08-05 (web research current to August 2026) | |
| 5 | +**Scope:** All major academic and industry indices measuring occupational exposure to AI/automation, their methodologies, criticisms, empirical validation, and lessons for the airiskindex.io v1 scoring model. | |
| 6 | + | |
| 7 | +--- | |
| 8 | + | |
| 9 | +## Table of Contents | |
| 10 | + | |
| 11 | +1. [First wave: pre-generative-AI automation risk (2013–2019)](#1-first-wave) | |
| 12 | +2. [Second wave: AI-specific exposure measures (2018–2021)](#2-second-wave) | |
| 13 | +3. [Third wave: LLM/generative-AI exposure (2023–2024)](#3-third-wave) | |
| 14 | +4. [Institutional indices: ILO, OECD, IMF (2023–2026)](#4-institutional-indices) | |
| 15 | +5. [Industry & consultancy estimates](#5-industry-estimates) | |
| 16 | +6. [Usage-based measures: Anthropic Economic Index & OpenAI (2025–2026)](#6-usage-based-measures) | |
| 17 | +7. [Fourth wave: 2025–2026 indices and meta-critiques](#7-fourth-wave) | |
| 18 | +8. [Empirical validation: do exposure scores predict real outcomes?](#8-empirical-validation) | |
| 19 | +9. [Master comparison table](#9-comparison-table) | |
| 20 | +10. [Implications for airiskindex.io v1 methodology](#10-implications) | |
| 21 | + | |
| 22 | +--- | |
| 23 | + | |
| 24 | +<a name="1-first-wave"></a> | |
| 25 | +## 1. First wave: pre-generative-AI automation risk (2013–2019) | |
| 26 | + | |
| 27 | +### 1.1 Frey & Osborne — "The Future of Employment" (2013 working paper; 2017 published) | |
| 28 | + | |
| 29 | +- **Authors/year:** Carl Benedikt Frey & Michael A. Osborne (Oxford Martin School). Working paper Sept 2013; published in *Technological Forecasting and Social Change* 114 (2017): 254–280. | |
| 30 | +- **Unit of analysis:** Whole occupations (702 SOC occupations). | |
| 31 | +- **Methodology:** | |
| 32 | + - ML experts hand-labeled ~70 occupations as automatable (1) or not (0) at a workshop. | |
| 33 | + - Identified three "engineering bottlenecks" to computerisation: **perception & manipulation**, **creative intelligence**, **social intelligence**, operationalized via 9 O*NET variables (e.g., finger dexterity, originality, social perceptiveness, persuasion, negotiation, assisting/caring for others, cramped work spaces). | |
| 34 | + - A **Gaussian process classifier** trained on the 70 labels extrapolated a "probability of computerisation" (0–1) to all 702 occupations. | |
| 35 | + - Occupations bucketed: high risk (p > 0.7), medium (0.3–0.7), low (p < 0.3). | |
| 36 | +- **Key numbers:** **47% of US employment** at "high risk" of computerisation "over the next decade or two" (i.e., roughly by 2030). Transportation, logistics, office/administrative support, and production occupations most at risk. | |
| 37 | +- **Criticisms (extensive):** | |
| 38 | + - **Occupation-level, not task-level:** treats occupations as monolithic. Arntz et al. (2016) showed within-occupation task heterogeneity slashes the estimate to ~9%. | |
| 39 | + - **Technical capability ≠ adoption:** no economics (cost, wages, regulation, preferences) in the model. | |
| 40 | + - **Subjective training labels** from a small expert workshop; only ~70 seed labels drive all 702 predictions. | |
| 41 | + - **Model-selection sensitivity:** "The Future of Employment Revisited" (arXiv:2104.13747) shows automation forecasts swing heavily with classifier choice on the same labels. | |
| 42 | + - **Poor ex-post predictive record:** occupations flagged high-risk did not experience differential employment declines through the late 2010s (see §8; Frank, Ahn & Moro 2025 find F&O scores explain <3% of unemployment-risk variation individually). | |
| 43 | +- **URLs:** | |
| 44 | + - Published paper: https://doi.org/10.1016/j.techfore.2016.08.019 (PDF mirror: http://reparti.free.fr/freyosborne17.pdf) | |
| 45 | + - Oxford Martin 2013 working paper: https://www.oxfordmartin.ox.ac.uk/downloads/academic/The_Future_of_Employment.pdf | |
| 46 | + - Critique (Melbourne Institute): https://melbourneinstitute.unimelb.edu.au/__data/assets/pdf_file/0005/3197111/wp2019n10.pdf | |
| 47 | + - Critique (model selection): https://arxiv.org/abs/2104.13747 | |
| 48 | + | |
| 49 | +### 1.2 Arntz, Gregory & Zierahn — OECD task-based critique (2016) | |
| 50 | + | |
| 51 | +- **Authors/year:** Melanie Arntz, Terry Gregory, Ulrich Zierahn (ZEW/OECD). *"The Risk of Automation for Jobs in OECD Countries"*, OECD Social, Employment and Migration Working Paper No. 189 (2016); follow-up "Revisiting the risk of automation" in *Economics Letters* 159 (2017). | |
| 52 | +- **Unit of analysis:** Individual workers' **task bundles** (PIAAC Survey of Adult Skills microdata), not occupation averages. | |
| 53 | +- **Methodology:** Transferred Frey–Osborne occupation-level risk to individual workers, then re-estimated risk as a function of each worker's *actual reported tasks* (PIAAC), allowing within-occupation heterogeneity. High risk = automatability > 70%. | |
| 54 | +- **Key numbers:** Only **~9% of jobs across 21 OECD countries** at high risk (US 9%, Germany 12%, Korea 6%) — versus 47% under F&O. | |
| 55 | +- **Significance:** Founded the **task-based paradigm** that every serious index since has adopted — including airiskindex.io. Key insight: workers in the "same" occupation do different task mixes; scoring must start at the task level. | |
| 56 | +- **Criticisms:** Still anchored on F&O's original subjective labels; PIAAC task self-reports are coarse; may *understate* risk if task bundles themselves adjust post-automation. | |
| 57 | +- **URLs:** | |
| 58 | + - OECD WP 189: https://doi.org/10.1787/5jlz9h56dvq7-en | |
| 59 | + - Economics Letters 2017: https://doi.org/10.1016/j.econlet.2017.07.001 | |
| 60 | + | |
| 61 | +### 1.3 Brynjolfsson, Mitchell & Rock — Suitability for Machine Learning (SML) (2017–2018) | |
| 62 | + | |
| 63 | +- **Authors/year:** Erik Brynjolfsson, Tom Mitchell, Daniel Rock. "What Can Machine Learning Do? Workforce Implications" (*Science*, 2017); "What Can Machines Learn, and What Does It Mean for Occupations and the Economy?" (*AEA Papers & Proceedings* 108, 2018: 43–47). | |
| 64 | +- **Unit of analysis:** O*NET tasks (18,156 tasks; ~2,069 Detailed Work Activities), aggregated to ~950 occupations. | |
| 65 | +- **Methodology:** | |
| 66 | + - A **23-question rubric** capturing what (then-current, supervised) ML can do — e.g., mapping well-defined inputs to outputs, tolerance for error, no long chains of reasoning, digital data availability, no need for detailed physical manipulation. | |
| 67 | + - Each task scored 1–5 per question; rubric validated by ML experts, then scaled via CrowdFlower crowd workers; aggregated to task SML then occupation SML (task-importance weighted). | |
| 68 | +- **Key findings:** (1) ML affects *different* occupations than earlier automation waves; (2) most occupations have at least some high-SML tasks; (3) **almost no occupation is fully automatable**; (4) capturing value requires **task re-bundling / job redesign**. | |
| 69 | +- **Criticisms:** Rubric was tuned to pre-LLM supervised ML (weak on generation, reasoning, dialogue); crowd ratings noisy; scores never strongly validated against outcomes (explains ~0–3% of unemployment-risk variation individually per Frank et al. 2025). | |
| 70 | +- **Relevance to us:** Direct methodological ancestor of a **multi-criterion task rubric** — our `automatability` + `feasibility` split echoes SML's separation of "could ML do it" from "is it practical". | |
| 71 | +- **URLs:** | |
| 72 | + - AEA P&P: https://www.aeaweb.org/articles?id=10.1257/pandp.20181019 | |
| 73 | + - SSRN: https://papers.ssrn.com/sol3/papers.cfm?abstract_id=3224100 | |
| 74 | + - Replication data: https://www.openicpsr.org/openicpsr/project/114436 | |
| 75 | + - WorldSML rubric (Stanford Digital Economy Lab, ongoing): https://digitaleconomy.stanford.edu/research/suitability-for-machine-learning-rubric-worldsml/ | |
| 76 | + | |
| 77 | +--- | |
| 78 | + | |
| 79 | +<a name="2-second-wave"></a> | |
| 80 | +## 2. Second wave: AI-specific exposure measures (2018–2021) | |
| 81 | + | |
| 82 | +### 2.1 Webb (2020) — Patent-based exposure | |
| 83 | + | |
| 84 | +- **Author/year:** Michael Webb (Stanford). "The Impact of Artificial Intelligence on the Labor Market" (SSRN 3482150, Nov 2019/2020; still unpublished but heavily cited). | |
| 85 | +- **Unit of analysis:** O*NET task descriptions × patent text; aggregated to occupations. | |
| 86 | +- **Methodology:** | |
| 87 | + - Selected AI patents (~16,400) by keyword; dependency-parsed titles to extract **verb–object pairs** (~8,000 pairs, e.g., "diagnose disease", "detect fraud"). | |
| 88 | + - Extracted verb–object pairs from O*NET task statements; scored each task by the frequency with which its verb-object pairs appear in AI patents. | |
| 89 | + - Occupation score = task-importance-weighted average; reported as **exposure percentiles**. | |
| 90 | + - **Built-in validation strategy:** applied the same method to *robots* and *software* patents and showed those historical exposure measures predicted realized employment/wage declines in exposed occupations — then applied it to AI. | |
| 91 | +- **Key findings:** AI (unlike robots/software) exposes **high-skilled, high-wage, older** workers most: e.g., clinical lab technicians, chemical engineers, optometrists, radiologic technicians. Robots hit low-skill physical work; software hit mid-skill routine work. | |
| 92 | +- **Criticisms:** Patents lag and imperfectly reflect deployable capability (pre-LLM corpus, so misses generative AI entirely); verb-object matching is crude semantics; no distinction between substitution and augmentation. | |
| 93 | +- **URLs:** | |
| 94 | + - Paper: https://papers.ssrn.com/sol3/papers.cfm?abstract_id=3482150 / https://www.michaelwebb.co/webb_ai.pdf | |
| 95 | + - Brookings explainer: https://www.brookings.edu/articles/how-patents-can-tell-us-what-jobs-ai-is-poised-to-disrupt/ | |
| 96 | + | |
| 97 | +### 2.2 Felten, Raj & Seamans — AI Occupational Exposure (AIOE) (2018, 2021, 2023) | |
| 98 | + | |
| 99 | +- **Authors/year:** Edward Felten (Princeton), Manav Raj (Wharton), Robert Seamans (NYU). "Occupational, Industry, and Geographic Exposure to Artificial Intelligence: A Novel Dataset and Its Potential Uses", *Strategic Management Journal* 42(12), 2021: 2195–2217. Generative-AI update: "Occupational Heterogeneity in Exposure to Generative AI" (SSRN 4414065, April 2023). | |
| 100 | +- **Unit of analysis:** 52 O*NET **abilities** (not tasks) × 10 AI application areas; aggregated to occupations (also industry AIIE and county-level geography). | |
| 101 | +- **Methodology:** | |
| 102 | + - 10 AI applications from the EFF AI Progress Measurement project (image recognition, language modeling, translation, speech recognition, abstract strategy games, etc.). | |
| 103 | + - Amazon Mechanical Turk crowd workers rated **relatedness** of each application to each of 52 O*NET abilities → ability-level exposure = sum of relatedness scores. | |
| 104 | + - Occupation AIOE = weighted sum of ability exposures using O*NET ability **importance and prevalence** weights. | |
| 105 | + - 2023 generative-AI variant re-weights toward language modeling and image generation: top exposed = telemarketers, then post-secondary teachers (languages, history, law), sociologists, judges. | |
| 106 | +- **Key properties:** Continuous z-scored index; famously **positively correlated with wages and education** (white-collar exposure) — opposite sign to Frey–Osborne. | |
| 107 | +- **Criticisms:** Ability-level (even further from tasks than occupations); MTurk relatedness judgments are lay opinions; "exposure" deliberately **neutral between substitution and augmentation** (authors are explicit about this); static. | |
| 108 | +- **Data:** Public GitHub (occupation-, industry-, geography-level scores): https://github.com/AIOE-Data/AIOE | |
| 109 | +- **Adoption:** The **IMF** (Cazzaniga et al. 2024) and Pew (2023) analyses build directly on AIOE; the ECB European work (Albanesi et al.) uses AIOE + Webb. | |
| 110 | +- **URLs:** | |
| 111 | + - SMJ paper: https://doi.org/10.1002/smj.3286 | |
| 112 | + - SSRN 2021: https://papers.ssrn.com/sol3/papers.cfm?abstract_id=3822412 | |
| 113 | + - GenAI variant: https://papers.ssrn.com/sol3/papers.cfm?abstract_id=4414065 | |
| 114 | + | |
| 115 | +### 2.3 Other second-wave measures (brief) | |
| 116 | + | |
| 117 | +- **Georgieff & Hyee (OECD 2021), "Artificial intelligence and employment: New cross-country evidence"** — applied Felten-style AIOE to PIAAC across 23 OECD countries; most-exposed: business professionals, managers, chief executives, science/engineering professionals. Found **no negative employment relationship 2012–2019**; in high-computer-use occupations, higher AI exposure correlated with *higher* employment growth. URL: https://doi.org/10.1787/c2c1d276-en | |
| 118 | +- **Lassébie & Quintini (OECD 2022), "What skills and abilities can automation technologies replicate?"** — expert survey on automatability of ~100 O*NET skills/abilities; basis for OECD Employment Outlook 2023 statement that occupations at highest risk of automation account for **~27% of OECD employment**. URL: https://doi.org/10.1787/646aad77-en ; Employment Outlook 2023 AI chapter: https://www.oecd.org/en/publications/oecd-employment-outlook-2023_08785bba-en.html | |
| 119 | +- **Tolan et al. (2021, JRC/EC)** — mapped AI research benchmarks to cognitive abilities to tasks; early "capability→ability→occupation" chain, precursor of the 2026 OECD capability-indicator approach. | |
| 120 | +- **Startup-based exposure ("Follow the money", arXiv 2024)** — measures exposure via commercial AI startup activity mapped to occupations; argues patent/ability measures miss commercialization. URL: https://arxiv.org/abs/2412.04924 | |
| 121 | + | |
| 122 | +--- | |
| 123 | + | |
| 124 | +<a name="3-third-wave"></a> | |
| 125 | +## 3. Third wave: LLM/generative-AI exposure (2023–2024) | |
| 126 | + | |
| 127 | +### 3.1 Eloundou, Manning, Mishkin & Rock — "GPTs are GPTs" (OpenAI/Wharton, 2023; *Science* 2024) | |
| 128 | + | |
| 129 | +**The single most influential template for airiskindex.io's LLM-as-evaluator pipeline.** | |
| 130 | + | |
| 131 | +- **Authors/year:** Tyna Eloundou, Sam Manning, Pamela Mishkin (OpenAI), Daniel Rock (Wharton). arXiv:2303.10130 (Mar 2023); published as "GPTs are GPTs: Labor market impact potential of LLMs", *Science* 384(6702), June 2024: 1306–1308. | |
| 132 | +- **Unit of analysis:** O*NET task/DWA level (19,265 tasks; 2,087 DWAs), aggregated to 1,016 occupations with task weights; combined with BLS employment/wage data. | |
| 133 | +- **Exposure rubric (the core innovation):** Exposure = "whether access to an LLM or LLM-powered system would reduce the time required for a human to perform a specific task **by at least 50% while maintaining quality**". Three levels: | |
| 134 | + - **E0** — no exposure. | |
| 135 | + - **E1** — direct exposure: LLM alone (via chat/API) achieves the 50% time reduction. | |
| 136 | + - **E2** — LLM+ exposure: achievable only with additional software/tooling built on the LLM (image input, retrieval, agents, etc.). | |
| 137 | + - Aggregates: **α = E1** (lower bound), **β = E1 + 0.5·E2** (expected), **ζ = E1 + E2** (upper bound). | |
| 138 | +- **Raters:** Both human annotators (OpenAI staff, trained on rubric) and **GPT-4 itself as a rater** with the rubric as prompt; human–GPT-4 agreement was high (occupation-level correlations ≈ 0.80), pioneering the LLM-as-evaluator design we plan to use. | |
| 139 | +- **Key numbers:** | |
| 140 | + - ~**80% of US workers** have ≥10% of tasks exposed (β); **~19% of workers** have ≥50% of tasks exposed. | |
| 141 | + - ~1.8% of jobs have >half their tasks E1-exposed; rises to **~46% of jobs** under ζ (with LLM-powered software). | |
| 142 | + - Exposure **increases with wage and education** (up to a point); science and critical-thinking-intensive skills correlate negatively; programming and writing positively. | |
| 143 | +- **Criticisms:** | |
| 144 | + - Measures *potential time savings*, not substitution vs augmentation, adoption, or net employment effect (authors are explicit). | |
| 145 | + - Rater instability: the flagship statistic is wildly model-dependent (see Yin et al. 2026, §7.2: 2.7%–51.5% across frontier raters). | |
| 146 | + - Static snapshot of March-2023 GPT-4 capability; the "E2 software will exist" counterfactual is speculative. | |
| 147 | + - 50%-time-saving threshold is arbitrary; binary-ish levels lose information. | |
| 148 | +- **URLs:** | |
| 149 | + - arXiv: https://arxiv.org/abs/2303.10130 | |
| 150 | + - Science: https://www.science.org/doi/10.1126/science.adj0998 | |
| 151 | + - OpenAI page: https://openai.com/index/gpts-are-gpts/ | |
| 152 | + - Follow-up "Extending GPTs Are GPTs to Firms" (AEA P&P 2025): https://www.aeaweb.org/articles?id=10.1257/pandp.20251045 | |
| 153 | + | |
| 154 | +### 3.2 Goldman Sachs — Briggs & Kodnani (March 2023) | |
| 155 | + | |
| 156 | +See §5.1. Methodologically an O*NET task-importance exercise inspired by Eloundou-style exposure; headline "300 million FTE jobs exposed" globally. | |
| 157 | + | |
| 158 | +--- | |
| 159 | + | |
| 160 | +<a name="4-institutional-indices"></a> | |
| 161 | +## 4. Institutional indices: ILO, OECD, IMF (2023–2026) | |
| 162 | + | |
| 163 | +### 4.1 ILO — Gmyrek, Berg & Bescond (2023): "Generative AI and Jobs: A Global Analysis" | |
| 164 | + | |
| 165 | +- **Authors/year:** Paweł Gmyrek, Janine Berg, David Bescond. ILO Working Paper 96, August 2023. | |
| 166 | +- **Methodology:** Scored **ISCO-08** occupation task lists (not O*NET) with **GPT-4 as rater** (multiple prompts, averaged), producing task-level automation-potential scores; distinguished **automation potential** vs **augmentation potential** at occupation level; mapped to global employment via ILO harmonized microdata for 100+ countries, by income group and sex. | |
| 167 | +- **Key numbers:** | |
| 168 | + - Only **clerical support work** is highly exposed as a group: **24% of clerical tasks highly exposed**, +58% medium exposure. Other occupational groups: 1–4% of tasks highly exposed. | |
| 169 | + - Globally, ~**2.3% of employment (~75M jobs)** in the top automation-potential bucket; **13.4% (~427M)** in augmentation potential. | |
| 170 | + - Exposure concentrated in **high/upper-middle-income countries** (more clerical employment) and **strongly gendered** (clerical work is female-dominated: in high-income countries, several times more female than male employment in the highest-exposure category). | |
| 171 | +- **Framing:** "Augmentation, not automation, is the most likely impact" — the origin of the transformation-over-replacement institutional narrative. | |
| 172 | +- **URLs:** | |
| 173 | + - WP96 PDF: https://www.ilo.org/sites/default/files/2024-07/WP96_web.pdf | |
| 174 | + - SSRN: https://papers.ssrn.com/sol3/papers.cfm?abstract_id=4584219 | |
| 175 | + - Policy companion "Generative AI and Jobs: Policies to Manage the Transition": https://www.ilo.org/publications/generative-ai-and-jobs-policies-manage-transition | |
| 176 | + | |
| 177 | +### 4.2 ILO 2024 interim work | |
| 178 | + | |
| 179 | +- **"Mind the AI Divide: Shaping a Global Perspective on the Future of Work"** (ILO & World Bank, Aug 2024) — applies the 2023 index with a digital-infrastructure overlay: poor countries are less *exposed* but also less able to *capture augmentation gains* (the "AI divide"). | |
| 180 | +- **Gmyrek, Winkler & Garganta (2024, ILO/World Bank)** — "Buffer or bottleneck? Employment exposure to generative AI and the digital divide in Latin America": 26–38% of LAC jobs exposed; digital access gates both risk and benefit. | |
| 181 | +- URL hub: https://www.ilo.org/publications (search "generative AI"); LAC paper: https://openknowledge.worldbank.org/handle/10986/41808 | |
| 182 | + | |
| 183 | +### 4.3 ILO–NASK (2025): "Generative AI and Jobs: A Refined Global Index of Occupational Exposure" — current institutional state of the art | |
| 184 | + | |
| 185 | +- **Authors/year:** Paweł Gmyrek, Janine Berg, K. Kamiński, F. Konopczyński, A. Ładna, B. Nafradi, K. Rosłaniec, M. Troszyński (ILO + Poland's NASK). ILO Working Paper 140, May 2025 + Research Brief "Generative AI and jobs: a 2025 update". | |
| 186 | +- **Methodology (major upgrade over 2023):** | |
| 187 | + - **Hybrid human+LLM pipeline:** 52,558 human judgments on automation potential of 2,861 tasks (representative sample of 29,753 tasks in the Polish occupational classification), from a survey of 1,640 people (workers/experts), used to calibrate and validate GPT-4o task scoring; then scaled to the full ISCO task universe. | |
| 188 | + - Replaced the binary automation/augmentation split with a **4-gradient exposure spectrum** (from marginal exposure to highest exposure), acknowledging most jobs are partially transformed. | |
| 189 | + - Occupation scores mapped to global employment microdata by country income group, sex, and region. | |
| 190 | +- **Key numbers:** | |
| 191 | + - **1 in 4 jobs worldwide (25% of global employment)** has measurable GenAI exposure; **34% in high-income countries**. | |
| 192 | + - Highest-gradient (transformation most likely) ≈ 3–4% of global employment, still concentrated in clerical work. | |
| 193 | + - Gender gap persists: in high-income countries, ~**9.6% of female employment** vs ~3.5% of male employment in the top exposure gradient. | |
| 194 | + - Headline framing: "**transformation, not replacement**". | |
| 195 | +- **Criticisms:** LLM-rater dependence (flagged by Yin et al. 2026); Polish task-survey generalizability; exposure ≠ adoption in low-connectivity countries (self-acknowledged). | |
| 196 | +- **URLs:** | |
| 197 | + - WP140 PDF: https://www.ilo.org/sites/default/files/2025-05/WP140_web.pdf | |
| 198 | + - WP140 interactive: https://webapps.ilo.org/static/english/intserv/working-papers/wp140/index.html | |
| 199 | + - 2025 update brief: https://www.ilo.org/publications/generative-ai-and-jobs-2025-update (PDF: https://www.ilo.org/sites/default/files/2025-05/Research%20brief_GenAI%202025%20Update.pdf) | |
| 200 | + - Press release: https://www.ilo.org/resource/news/one-four-jobs-risk-being-transformed-genai-new-ilo–nask-global-index-shows | |
| 201 | + | |
| 202 | +### 4.4 OECD (2023–2026) | |
| 203 | + | |
| 204 | +- **Employment Outlook 2023 (AI chapters):** using Lassébie–Quintini expert-based measure, occupations at highest automation risk = **~27% of employment** across OECD; "no signs of slowing labour demand (yet)" in AI-exposed occupations. URLs: https://www.oecd.org/en/publications/oecd-employment-outlook-2023_08785bba-en/full-report/artificial-intelligence-and-jobs-no-signs-of-slowing-labour-demand-yet_5aebe670.html | |
| 205 | +- **AI case studies & job quality (2023–2024):** firm case studies in finance/manufacturing across 8 countries: 23% of firms reported AI reduced employment in affected roles; wages mostly unchanged. Georgieff (2024), "Artificial intelligence and wage inequality": https://www.oecd.org/en/publications/artificial-intelligence-and-wage-inequality_bf98a45c-en.html | |
| 206 | +- **OECD AI Capability Indicators (2025):** 5-year effort, 50+ experts; 9 capability domains (Language; Social interaction; Problem solving; Creativity; Metacognition & critical thinking; Knowledge/learning/memory; Vision; Manipulation; Robotic intelligence) each on an ordinal capability scale. URL: https://www.oecd.org/en/publications/introducing-the-oecd-ai-capability-indicators_be745f04-en.html | |
| 207 | +- **The OECD AI Exposure Measure (2025/2026):** maps occupations' required capability *levels* in each of the 9 domains against AI's *current attained level* per the Capability Indicators → exposure = overlap. Explicitly designed to be **forward-looking, transparent, and updateable** as AI capability levels advance — the first institutional index architected for versioned re-scoring (same philosophy as our `INDEX_VERSION`). URL: https://www.oecd.org/en/publications/the-oecd-ai-exposure-measure_f3da0f0a-en.html | |
| 208 | +- **Skills in the AI age (OECD AI Papers No. 60, July 2026):** applies the exposure measure to skills demand. URL: https://www.oecd.org/content/dam/oecd/en/publications/reports/2026/07/skills-in-the-ai-age_e8d8c1e6/972bd15e-en.pdf | |
| 209 | + | |
| 210 | +### 4.5 IMF — Cazzaniga et al. (2024) and the AI Preparedness Index | |
| 211 | + | |
| 212 | +- **Authors/year:** Mauro Cazzaniga, Florence Jaumotte, Longji Li, Giovanni Melina, Augustus Panton, Carlo Pizzinelli, Emma Rockall, Marina M. Tavares. "Gen-AI: Artificial Intelligence and the Future of Work", IMF Staff Discussion Note SDN/2024/001, January 2024. | |
| 213 | +- **Methodology:** Takes Felten's **AIOE** and adds a **potential complementarity index (C-AIOE)** (from Pizzinelli et al. 2023, IMF WP/23/216): occupations scored on shielding factors — required physical presence, human interaction, legal/social responsibility (judges are exposed *and* complemented; telemarketers exposed and *not*). Splits employment into: high exposure + high complementarity (augmentation likely) vs high exposure + low complementarity (displacement risk) vs low exposure. | |
| 214 | +- **Key numbers:** **~40% of global employment exposed** to AI (**60% advanced economies, 40% emerging, 26% low-income**). In AEs, roughly half of exposed jobs are high-complementarity. Women and college-educated more exposed but better positioned for gains. | |
| 215 | +- **AI Preparedness Index (AIPI):** country-level (174 economies) readiness across digital infrastructure, human capital & labor policies, innovation & integration, regulation & ethics — the macro complement to occupational exposure. Dashboard: https://www.imf.org/external/datamapper/AIPI@AIPI | |
| 216 | +- **Criticisms:** Inherits all AIOE limitations; complementarity ratings are judgment calls; country mapping via ISCO crosswalks is coarse. | |
| 217 | +- **URLs:** | |
| 218 | + - SDN PDF: https://www.imf.org/-/media/files/publications/sdn/2024/english/sdnea2024001.pdf | |
| 219 | + - eLibrary: https://www.elibrary.imf.org/view/journals/006/2024/001/006.2024.issue-001-en.xml | |
| 220 | + - Pizzinelli et al. WP/23/216 (C-AIOE): https://www.imf.org/en/Publications/WP/Issues/2023/10/04/Labor-Market-Exposure-to-AI-Cross-country-Differences-and-Distributional-Implications-539656 | |
| 221 | + - Follow-up: "Exposure to Artificial Intelligence and Occupational Mobility" (WP/24/116): https://www.imf.org/-/media/files/publications/wp/2024/english/wpiea2024116-print-pdf.pdf | |
| 222 | + | |
| 223 | +--- | |
| 224 | + | |
| 225 | +<a name="5-industry-estimates"></a> | |
| 226 | +## 5. Industry & consultancy estimates | |
| 227 | + | |
| 228 | +### 5.1 Goldman Sachs (Briggs & Kodnani, March 2023) | |
| 229 | + | |
| 230 | +- "The Potentially Large Effects of Artificial Intelligence on Economic Growth". O*NET task-level judgment of automatable share per occupation (26 US, 24 European task categories importance/complexity weighted). | |
| 231 | +- **Key numbers:** ~**2/3 of US/European occupations partially exposed**; generative AI could substitute up to **25% of current work** = **300M FTE jobs** globally exposed; +7% global GDP over 10 years. Most exposed: office/admin support (46% of tasks automatable), legal (44%), architecture/engineering (37%). | |
| 232 | +- **Criticism:** binary "automatable share" judgments, no adoption model; the 300M number is routinely misquoted as "job losses". | |
| 233 | +- URLs: https://www.goldmansachs.com/insights/articles/generative-ai-could-raise-global-gdp-by-7-percent ; follow-up US labor analysis: https://www.goldmansachs.com/insights/articles/how-will-ai-affect-the-us-labor-market | |
| 234 | + | |
| 235 | +### 5.2 McKinsey Global Institute (June 2023, updated) | |
| 236 | + | |
| 237 | +- "The economic potential of generative AI: the next productivity frontier". Proprietary work-activity/capability model (~2,100 work activities, 850 occupations). | |
| 238 | +- **Key numbers:** GenAI + existing tech could automate activities absorbing **60–70% of employees' time**; genAI value $2.6–4.4T/yr; **half of today's work activities automated between 2030 and 2060 (midpoint ~2045)** — pulled forward ~a decade vs pre-genAI estimate. | |
| 239 | +- **Criticism:** proprietary/black-box capability ratings; "time automatable" ≠ jobs; adoption scenarios highly assumption-driven. | |
| 240 | +- URL: https://www.mckinsey.com/capabilities/tech-and-ai/our-insights/the-economic-potential-of-generative-ai-the-next-productivity-frontier | |
| 241 | + | |
| 242 | +### 5.3 Pew Research Center (Kochhar, July 2023) | |
| 243 | + | |
| 244 | +- "Which U.S. Workers Are More Exposed to AI on Their Jobs?" — Felten-style ability-importance approach on O*NET. | |
| 245 | +- **Key numbers:** **19% of US workers in most-exposed jobs** vs 23% in least-exposed (2022). Most-exposed jobs pay *more* ($33/hr vs $20/hr); exposure higher for women, Asian, college-educated workers. Notably, workers in exposed industries did **not** feel their jobs at risk. | |
| 246 | +- URL: https://www.pewresearch.org/social-trends/2023/07/26/which-u-s-workers-are-more-exposed-to-ai-on-their-jobs/ | |
| 247 | + | |
| 248 | +### 5.4 PwC Global AI Jobs Barometer (2024, 2025, 2026) | |
| 249 | + | |
| 250 | +- Analyzes ~**1 billion job ads** worldwide + firm financials. 2025 edition ("The Fearless Future"): industries most exposed to AI saw productivity growth nearly **4x** (7%→27%); **56% wage premium** for AI-skilled workers (up from 25%); skills in AI-exposed occupations changing **66% faster**; employment *still growing* even in highly automatable roles. 2026 edition: labor market splitting into "two distinct paths", rewarding human skills. | |
| 251 | +- **Value to us:** the best large-scale *demand-side* signal (vacancies), useful to calibrate `adoption_velocity`. | |
| 252 | +- URLs: https://www.pwc.com/gx/en/issues/artificial-intelligence/job-barometer/2025/report.pdf ; 2026 PR: https://www.pwc.com/gx/en/news-room/press-releases/2026/pwc-2026-ai-jobs-barometer.html | |
| 253 | + | |
| 254 | +--- | |
| 255 | + | |
| 256 | +<a name="6-usage-based-measures"></a> | |
| 257 | +## 6. Usage-based measures: Anthropic Economic Index & OpenAI (2025–2026) | |
| 258 | + | |
| 259 | +The decisive innovation of 2025–26: replacing *predicted* exposure with **observed AI usage** mapped to the same O*NET task taxonomy. This is the empirical anchor airiskindex.io should exploit for `adoption_velocity` and to validate `automatability`. | |
| 260 | + | |
| 261 | +### 6.1 Anthropic Economic Index (AEI) — all releases to August 2026 | |
| 262 | + | |
| 263 | +**Methodology (constant across releases):** Clio, a privacy-preserving analysis pipeline in which Claude classifies large samples of real Claude.ai/API conversations against the **O*NET task taxonomy (~20,000 tasks)** and SOC occupations, plus interaction-mode classification (**automation** = full delegation/directive; **augmentation** = iterative collaboration, learning, validation). All aggregated data released openly on Hugging Face. | |
| 264 | + | |
| 265 | +| Release | Report | Data & model | Headline findings | | |
| 266 | +|---|---|---|---| | |
| 267 | +| **Feb 10, 2025** (paper arXiv:2503.04761, Handa et al., "Which Economic Tasks are Performed with AI?") | Launch report | 4M+ Claude.ai conversations | **36% of occupations** used AI for ≥25% of their tasks; only ~4% for ≥75%. Usage concentrated in **software development & writing** (computer/math ≈ 37% of conversations); peaks in **mid-to-high-wage** occupations, low at both wage extremes. **57% augmentation / 43% automation**. | | |
| 268 | +| **Mar 27, 2025** | v2 (Claude 3.7 Sonnet) | New conversations + cluster-level data | Usage patterns stable; extended-thinking usage concentrated in technical tasks; released bottom-up task clusters. | | |
| 269 | +| **Sep 15, 2025** (arXiv:2511.15080) | "Uneven geographic and enterprise adoption" | 1P API + geographic breakdowns; **Anthropic AI Usage Index (AUI)** = country share of usage ÷ share of working-age population | US 21.6% of usage; per-capita leaders Israel, Singapore, Australia, NZ, S. Korea. **+1% GDP/capita ↔ +0.7% AUI** (US states: 1.8% elasticity). DC highest state AUI (3.82). **Automation rose 27%→39%** of conversations since Dec 2024, surpassing augmentation for the first time; API usage even more automation-heavy. | | |
| 270 | +| **Jan 15, 2026** | "New building blocks" (economic primitives) | 1M Claude.ai + 1M 1P API transcripts (Sonnet 4.5); Nov 2025 data | Five **primitives**: task complexity, human/AI skill level, use case, AI autonomy, task success. College-level tasks: **12x estimated speedup** but 66% success rate vs 70% for simpler tasks; revised aggregate productivity estimate **+1.2 pp/yr** (down from 1.8 after reliability adjustment). Augmentation back above automation on Claude.ai (52% vs 45%). | | |
| 271 | +| **Mar 24, 2026** | "Learning curves" | Feb 2026 data (Opus 4.5/4.6) | Claude.ai task mix **de-concentrating** (top-10 tasks 24%→19%) while API concentrates (28%→33%). **49% of jobs in sample** now see Claude used for ≥25% of tasks (up from 36% in Jan 2025). 6-month+ tenure users: +10% conversation success; usage value ≈ $48–49/hr wage-equivalent tasks. | | |
| 272 | +| **Apr 2026** | AEI **Survey** launched | 9,700 Claude users, linked usage+perceptions | See below. | | |
| 273 | +| **Jun 26, 2026** | "Cadences" | Apr–Jun 2026, hourly sampling; artifact classifier | 93% of conversations produce artifacts (explanations 17%, documents/reports 15%). Higher-wage occupations' conversations consume 2.07x tokens. Survey: >⅓ of users expect AI to handle most of their work tasks within 12 months; only 10% rate own job loss likely; heavier automation users are *more* optimistic. Women use Claude less in automated modes (−0.33 SD). | | |
| 274 | + | |
| 275 | +- **Data availability (all releases):** https://huggingface.co/datasets/Anthropic/EconomicIndex (per-release folders `release_2025_03_27/`, `release_2025_09_15/`, etc., with documentation + replication notebooks; R package `aieconindex` on CRAN). | |
| 276 | +- **Index hub:** https://www.anthropic.com/economic-index — reports: https://www.anthropic.com/news/the-anthropic-economic-index ; https://www.anthropic.com/research/economic-index-geography ; https://www.anthropic.com/research/economic-index-primitives ; https://www.anthropic.com/research/economic-index-march-2026-report ; https://www.anthropic.com/research/economic-index-june-2026-report | |
| 277 | +- **Limitations (self-acknowledged):** Claude users ≠ workforce (selection bias toward developers/knowledge workers); conversation ≠ completed work; O*NET classification by LLM inherits classifier error; per-provider view only. | |
| 278 | + | |
| 279 | +### 6.2 OpenAI — "How People Use ChatGPT" (Chatterji et al., NBER w34255, Sept 2025) | |
| 280 | + | |
| 281 | +- Aaron Chatterji, Tom Cunningham, David Deming, Zoë Hitzig, Christopher Ong, Carl Shan, Kevin Wadman. Privacy-preserving classification of a representative sample of ChatGPT consumer conversations, Nov 2022–Jul 2025 (~10% of world adult population using ChatGPT). | |
| 282 | +- **Findings:** non-work usage grew from 53%→>70% of messages; work usage concentrated in **decision support** (advice, writing, information) rather than task execution; work usage highest among educated, high-paid professionals. Three-quarters of work messages: writing, information seeking, decision support. | |
| 283 | +- **Relevance:** independent replication that *realized* usage is augmentation-tilted and knowledge-work-concentrated — cross-provider triangulation for `adoption_velocity`. | |
| 284 | +- URLs: https://www.nber.org/papers/w34255 (PDF: https://www.nber.org/system/files/working_papers/w34255.pdf) | |
| 285 | + | |
| 286 | +--- | |
| 287 | + | |
| 288 | +<a name="7-fourth-wave"></a> | |
| 289 | +## 7. Fourth wave: 2025–2026 indices and meta-critiques | |
| 290 | + | |
| 291 | +### 7.1 Stanford Digital Economy Lab — "Canaries in the Coal Mine?" (Brynjolfsson, Chandar & Chen, Aug/Nov 2025) | |
| 292 | + | |
| 293 | +- **The most important realized-effects paper to date.** Uses **ADP payroll microdata** (millions of workers, monthly) linked to occupational AI-exposure measures (Eloundou/GPTs-are-GPTs based, cross-checked with Anthropic Economic Index automation/augmentation shares). | |
| 294 | +- **Six facts**, headline: since late 2022, **early-career workers (22–25) in the most AI-exposed occupations saw a ~13–16% relative employment decline** (16% in the Nov 2025 revision, controlling for firm-level shocks), while older workers in the same occupations and less-exposed young workers kept growing. Adjustment happens via **employment, not wages**. Declines concentrated where AEI data says AI **automates** rather than augments. Entry-level hiring is the "canary". | |
| 295 | +- **Live monitoring:** "Canaries Dashboard" — https://digitaleconomy.stanford.edu/project/indicators/canaries-dashboard/ | |
| 296 | +- **URLs:** paper page https://digitaleconomy.stanford.edu/publications/canaries-in-the-coal-mine ; PDF (Nov 2025) https://digitaleconomy.stanford.edu/app/uploads/2025/11/CanariesintheCoalMine_Nov25.pdf ; SIEPR WP: https://siepr.stanford.edu/publications/working-paper/canaries-coal-mine-six-facts-about-recent-employment-effects-artificial | |
| 297 | + | |
| 298 | +### 7.2 Yin, Vu & Persico (2026) — multi-model instability of LLM-rated exposure ("When the ruler is made of the thing it measures") | |
| 299 | + | |
| 300 | +- NBER WP 35110, "How (un)stable are LLM occupational exposure scores? Evidence from multi-model replication"; VoxEU column May 2026. | |
| 301 | +- Replicated the Eloundou rubric with **four frontier models on identical O*NET data**: share of US occupations with >50% of tasks at high direct exposure = **2.7% (Gemini 2.5) … 3.8% (GPT-4) … 20.3% (GPT-5) … 51.5% (Claude 4.5)** — a **19x spread**. Management occupations: >80% high-exposure under Claude, <20% under Gemini. Downstream diff-in-diff employment estimates **flip sign** across raters. Bias is systematic per model, doesn't wash out with sample size, and co-evolves with the technology being measured (feedback channel). | |
| 302 | +- **Recommendation (directly applicable to us):** any LLM-rated exposure analysis must report results from **≥2–3 different frontier models**; convergence ⇒ robust, divergence ⇒ model artifact. | |
| 303 | +- URL: https://cepr.org/voxeu/columns/when-ruler-made-thing-it-measures-multi-model-evidence-ai-occupational-exposure | |
| 304 | + | |
| 305 | +### 7.3 "AI Exposure Scores: what they measure, what they miss, and what comes next" (Lund, Euyang, Munyikwa & Fadaee, arXiv June 2026) | |
| 306 | + | |
| 307 | +- Field review. Diagnoses a **structural gap** (static scores can't answer dynamic who/when/where policy questions) and a **coordination gap** (policy still cites static 2023 GPTs-are-GPTs numbers despite methodological advances). Surveys five successor families: **dynamic/benchmark-based measures, ensembles, task-framework extensions, worker-centered metrics, adoption/usage data**. Recommends moving "from prediction to preparedness". | |
| 308 | +- URL: https://arxiv.org/abs/2606.23633 | |
| 309 | + | |
| 310 | +### 7.4 Other notable 2025–2026 entries | |
| 311 | + | |
| 312 | +- **Iceberg Index (Chopra et al., MIT + Oak Ridge National Laboratory, arXiv 2510.25137, late 2025):** skills-centered simulation — 151M US workers, 923 occupations, 32,000+ skills, ~3,000 counties; catalogued 13,000+ AI tools; agent-based simulation (AgentTorch on Frontier supercomputer). Visible "surface" tech-sector exposure = 2.2% of wage bill (~$211B); full skill-overlap exposure = **11.7% of US wage bill (~$1.2T)**. Explicitly technical exposure, not displacement. URLs: https://arxiv.org/abs/2510.25137 ; https://iceberg.mit.edu/report.pdf | |
| 313 | +- **Yale Budget Lab — "Evaluating the Impact of AI on the Labor Market: Current State of Affairs" (Gimbel, Kinder, Kendall & Lee, Oct 2025, updated):** occupational-mix dissimilarity analysis; finds **no broad acceleration** in labor-market compositional change attributable to AI 33 months post-ChatGPT — important null-result counterweight to Canaries. URL: https://budgetlab.yale.edu/research/evaluating-impact-ai-labor-market-current-state-affairs | |
| 314 | +- **UK task-based GenAI exposure index (arXiv 2507.22748, 2025):** novel LLM-scored task index applied to UK SOC codes — example of the national-adaptation pattern relevant to our ESCO/ROME crosswalk. URL: https://arxiv.org/abs/2507.22748 | |
| 315 | +- **OAIES / capability-staged exposure (2025–2026):** scores O*NET task automatable share at discrete **AI capability stages** (pre-LLM ML → early LLMs → multimodal → reasoning → agentic), multi-model rated (GPT-4o + Claude 3.5); cross-methodology Spearman ρ = 0.84 against independent scores. Overview: https://www.emergentmind.com/topics/ai-exposed-occupations ; theory-based variant (Moravec-paradox index): https://arxiv.org/abs/2510.13369 | |
| 316 | +- **"AI and jobs: A review of theory, estimates, and evidence" (arXiv 2509.15265, 2025):** comprehensive literature review; useful bibliography. URL: https://arxiv.org/abs/2509.15265 | |
| 317 | +- **Agentic-AI exposure analyses (2026):** e.g., "Agentic AI and Occupational Displacement" (arXiv 2604.00186) extends task exposure to autonomous multi-step agents across regions. URL: https://arxiv.org/abs/2604.00186 | |
| 318 | +- **"The Jagged Global Economy" (arXiv 2607.05404, 2026):** frontier-AI benchmark performance mapped to national economies — capability-grounded, benchmark-updated exposure. URL: https://arxiv.org/abs/2607.05404 | |
| 319 | + | |
| 320 | +--- | |
| 321 | + | |
| 322 | +<a name="8-empirical-validation"></a> | |
| 323 | +## 8. Empirical validation: do exposure scores predict real outcomes? | |
| 324 | + | |
| 325 | +**Bottom line: individually, classic exposure scores are weak predictors; ensembles + adoption data + post-2022 windows perform much better. Realized effects so far are concentrated (entry-level, automation-tilted tasks, online freelancing), not economy-wide.** | |
| 326 | + | |
| 327 | +1. **Frank, Ahn & Moro (PNAS Nexus, April 2025), "AI exposure predicts unemployment risk"** — built occupation-level *unemployment risk* from US unemployment-insurance claims (2010–2020); tested 10 exposure scores. **Every individual score performs poorly** (best single: Arntz automation probability, R² = 0.107; most < 3%; Frey–Osborne, SML, Felten, Webb all weak alone). An **ensemble of all scores** explains 29.8% (75.5% with education/skill/region controls) — +18 pp over baseline. Lesson: **no single score suffices; combine dimensions**. URL: https://pmc.ncbi.nlm.nih.gov/articles/PMC11983276/ (arXiv:2308.02624) | |
| 328 | +2. **Acemoglu, Autor, Hazell & Restrepo (JOLE 2022), "AI and Jobs: Evidence from Online Vacancies"** — AI-exposed establishments (Burning Glass) post more AI vacancies and *reduce* non-AI hiring, but **no detectable aggregate occupation-level employment effects** through 2018. URL: https://jadhazell.github.io/website/AI_And_Jobs.pdf | |
| 329 | +3. **Brynjolfsson, Chandar & Chen (2025) "Canaries"** — first large-scale realized-effect finding: −13–16% relative employment for early-career workers in most-exposed occupations; effects load on **automation-classified** (AEI) usage, not augmentation. (§7.1) | |
| 330 | +4. **Hui, Reshef & Zhou (2024), "The Short-Term Effects of Generative AI on Online Labor Markets"** — after ChatGPT, exposed freelancers (writing-heavy) on a large platform saw ~2% fewer jobs and ~5% lower earnings; top performers not spared. VoxEU: https://cepr.org/voxeu/columns/artificial-intelligence-and-its-short-term-effects-employment | |
| 331 | +5. **Hampole, Papanikolaou, Schmidt & Seegmiller (NBER w33509, 2025), "Artificial Intelligence and the Labor Market"** — vacancy-based measure of firm AI adoption; AI adoption predicts declining demand for exposed occupations within adopting firms, with reallocation toward AI-complementary roles. URL: https://www.nber.org/papers/w33509 | |
| 332 | +6. **Humlum & Vestergaard (2025), "Large Language Models, Small Labor Market Effects" (Denmark, NBER w33777)** — despite rapid ChatGPT adoption among exposed workers, **no detectable effects on earnings or hours** in 2023–24 administrative data; average time savings ~3%. Counterweight showing adoption ≠ displacement in the short run. URL: https://www.nber.org/papers/w33777 | |
| 333 | +7. **Wage-growth cross-section (2019 vs 2023, arXiv 2312.04714 & follow-ups):** one-unit higher AI exposure ↔ **−6.5 pp wage growth**, explaining ~34% of cross-sectional variation post-ChatGPT; late-2022→early-2025 CPS analyses find high-exposure occupations losing 5.6–8.5 pp employment per 10-point exposure. URL: https://arxiv.org/abs/2312.04714 | |
| 334 | +8. **Georgieff & Hyee (OECD 2021) / Employment Outlook 2023:** 2012–2019 — no negative employment relationship; **exposure without adoption predicts nothing** pre-2022. (§2.3, §4.4) | |
| 335 | +9. **Yale Budget Lab (2025–2026):** aggregate occupational mix shifting no faster than historical benchmarks (§7.4) — realized effects are **cohort- and task-specific, not (yet) aggregate**. | |
| 336 | + | |
| 337 | +**Synthesis for validation design:** (a) validate at task/cohort level, not aggregate; (b) test against unemployment/UI-claim risk and entry-level hiring, not just employment stocks; (c) use ensembles; (d) treat pre-2022 null results as evidence about *adoption gating*, which is exactly what our `barriers` and `adoption_velocity` dimensions model. | |
| 338 | + | |
| 339 | +--- | |
| 340 | + | |
| 341 | +<a name="9-comparison-table"></a> | |
| 342 | +## 9. Master comparison table | |
| 343 | + | |
| 344 | +| Index / Study | Year | Approach | Unit of analysis | Scale / output | Substitution vs augmentation split? | Validation status | Data availability | | |
| 345 | +|---|---|---|---|---|---|---|---| | |
| 346 | +| **Frey & Osborne** | 2013/2017 | Expert labels + Gaussian process classifier on O*NET bottleneck variables | Occupation (702 SOC) | P(computerisation) 0–1 | No | Poor ex-post; explains <3% of unemployment risk alone | Scores in paper appendix (public) | | |
| 347 | +| **Arntz, Gregory & Zierahn (OECD)** | 2016/2017 | F&O risk re-estimated on individual PIAAC task bundles | Worker/task bundle | P(automation) 0–1 | No | Best single predictor in Frank et al. ensemble (R²=0.107) | PIAAC public; scores replicable | | |
| 348 | +| **Brynjolfsson, Mitchell & Rock (SML)** | 2017/2018 | 23-question rubric, crowd-rated | O*NET task (18k) → occupation | SML 1–5 | Implicit (redesign framing) | Weak alone | openICPSR replication archive | | |
| 349 | +| **Webb** | 2020 | Patent–task text overlap (verb–object pairs) | Task → occupation | Exposure percentile | No | Historical validation on robots/software; AI portion pre-LLM | Author site / SSRN | | |
| 350 | +| **Felten, Raj & Seamans (AIOE)** | 2021 (genAI 2023) | 10 AI apps × 52 abilities, MTurk relatedness, importance-weighted | Ability → occupation (+industry, county) | Continuous z-score | No (explicitly neutral) | Weak alone; base of IMF/Pew analyses | GitHub (AIOE-Data/AIOE) | | |
| 351 | +| **Eloundou et al. "GPTs are GPTs"** | 2023/2024 | Rubric (≥50% time saving), human + GPT-4 raters | O*NET task/DWA → occupation | E0/E1/E2; α, β, ζ shares 0–1 | No (time-savings only) | Predicts Canaries cohort effects; rater-unstable (19x across models) | arXiv appendix; rubric public | | |
| 352 | +| **Goldman Sachs (Briggs & Kodnani)** | 2023 | Task-importance share judged automatable | Occupation | % tasks automatable | Partial (25% substitution assumption) | n/a | Report only (proprietary) | | |
| 353 | +| **McKinsey MGI** | 2023 | Proprietary activity–capability model | Work activity (~2,100) | % of work time automatable; adoption scenarios | Partial | n/a | Report only (proprietary) | | |
| 354 | +| **Pew Research (Kochhar)** | 2023 | Felten-style ability importance | Occupation | High/medium/low exposure | No | n/a | Report + appendix | | |
| 355 | +| **ILO Gmyrek et al. WP96** | 2023 | GPT-4-rated ISCO task scores | ISCO task → occupation → global employment | Automation vs augmentation potential, 0–1 | **Yes** | n/a | Scores in WP annexes | | |
| 356 | +| **IMF Cazzaniga et al. (AIOE + C-AIOE)** | 2024 | AIOE + complementarity shielding index | Occupation → country employment | Exposure × complementarity quadrants | **Yes** (complementarity) | n/a | AIPI dashboard; WP data | | |
| 357 | +| **ILO–NASK refined index (WP140)** | 2025 | Hybrid: 52,558 human ratings calibrating GPT-4o, 4-gradient scale | Task → ISCO occupation → 100+ countries | 4 exposure gradients | **Yes** (gradient) | n/a | WP + interactive tool | | |
| 358 | +| **OECD AI Exposure Measure** | 2025/2026 | Occupation capability requirements vs OECD AI Capability Indicators (9 domains, expert-set levels) | Ability-domain → occupation | Capability-overlap exposure; versioned as AI levels advance | No | New | OECD publication + indicators | | |
| 359 | +| **PwC AI Jobs Barometer** | 2024–2026 | ~1B job ads; demand-side | Vacancy/occupation/industry | Growth, wage premium, skill-change rates | No | Is itself outcome data | Annual reports | | |
| 360 | +| **Anthropic Economic Index** | 2025–2026 (6 releases) | Observed Claude usage classified to O*NET tasks (Clio); AUI; primitives | Conversation → task → occupation, geo | Usage shares; automation vs augmentation %; complexity/success | **Yes** (measured, not predicted) | Is itself adoption data; used in Canaries | **Hugging Face (open)** | | |
| 361 | +| **OpenAI / Chatterji et al.** | 2025 | Observed ChatGPT usage classification | Message → task category | Usage shares by intent | Partial (Asking/Doing/Expressing) | Is itself adoption data | NBER paper (aggregates) | | |
| 362 | +| **Canaries in the Coal Mine (Stanford DEL)** | 2025 | ADP payroll × exposure scores (realized effects) | Worker-level panel | Employment effects by age × exposure | Uses AEI automation/augmentation | **Is the validation** | Dashboard public; ADP restricted | | |
| 363 | +| **Iceberg Index (MIT/ORNL)** | 2025 | Skill-level tool coverage + agent-based simulation | 32k skills → 923 occupations → counties | % of wage bill exposed ($) | No | New | iceberg.mit.edu; arXiv | | |
| 364 | +| **Yin, Vu & Persico (multi-model replication)** | 2026 | Meta: Eloundou rubric × 4 frontier raters | Task → occupation | Rater-dispersion bounds | n/a | Meta-validation | NBER WP 35110 | | |
| 365 | +| **Frank, Ahn & Moro (ensemble)** | 2025 | Ensemble of 10 exposure scores vs UI-claims risk | Occupation × state × month | Unemployment-risk R² | No | **Is the validation** | PNAS Nexus (open access) | | |
| 366 | + | |
| 367 | +--- | |
| 368 | + | |
| 369 | +<a name="10-implications"></a> | |
| 370 | +## 10. Implications for airiskindex.io v1 methodology | |
| 371 | + | |
| 372 | +Mapping the literature onto our five dimensions (weights from `packages/scoring/src/weights.ts`) and our exposure/substitution/augmentation triad. | |
| 373 | + | |
| 374 | +### Cross-cutting lessons (apply to the whole index) | |
| 375 | + | |
| 376 | +1. **Task-based is settled science** (Arntz 2016 → everyone since). Our O*NET task-level scoring with importance/frequency weights is the correct v1 backbone. Keep occupation scores as *derived*, never primary. | |
| 377 | +2. **Never collapse to one number without sub-scores.** Felten's "neutral exposure", ILO's automation/augmentation split, IMF's complementarity, and AEI's measured automation:augmentation ratio all show the field converging on our exposure/substitution/augmentation triad. This is a genuine differentiator — most indices still publish one headline number and get misquoted (Goldman's "300M jobs lost" problem). Our tone rule ("adaptation, not doom") is empirically supported: realized effects so far are cohort-specific task reallocation, not mass unemployment (Yale Budget Lab; Humlum & Vestergaard) — with real, measurable pain at the entry level (Canaries). | |
| 378 | +3. **LLM-as-evaluator is standard but fragile — multi-model rating is now table stakes.** Yin et al. (2026): 19x spread in headline statistics across frontier raters on identical data; Claude-family raters score exposure *highest* of all models. Concrete requirements for `apps/worker/src/raters/`: | |
| 379 | + - Rate every task with **≥2 (ideally 3) different frontier models** (`RATER_MODEL` must become a list or we add `RATER_MODEL_SECONDARY`); store per-model scores; publish cross-model agreement per occupation. | |
| 380 | + - Report **confidence intervals derived from rater disagreement** — this slots directly into our existing `score_low`/`score`/`score_high` schema. | |
| 381 | + - Calibrate LLM ratings against a **human-rated anchor set** (ILO–NASK's 52,558-judgment survey design is the gold standard; our expert Delphi overrides in `data/derived/expert_overrides/` serve this role — sample deliberately across the exposure spectrum, not just flagged disagreements). | |
| 382 | + - Prompt-version everything (we already do) and re-rate on model change — because the instrument co-evolves with the phenomenon (the "ruler" problem). | |
| 383 | +4. **Anchor thresholds in explicit, documented rubrics.** Eloundou's "≥50% time saving at equal quality" is the citable standard; SML's 23 questions show multi-criterion rubrics beat single judgments. Our prompts should decompose ratings into named criteria and require structured justifications (auditable per §6 of CLAUDE.md). | |
| 384 | +5. **Validate against outcomes, and say so publicly.** Frank et al. (2025): single scores explain <11% of unemployment risk; ensembles ~30–75%. We should (a) benchmark our composite against the public AEI usage data, PwC vacancy signals, and the Canaries dashboard; (b) publish a `docs/methodology/sensitivity/` correlation report against AIOE, GPTs-are-GPTs β, and ILO WP140 scores each release. Spearman ρ ≈ 0.84 between independent modern methodologies is the bar for "capturing the same signal". | |
| 385 | + | |
| 386 | +### Dimension-by-dimension | |
| 387 | + | |
| 388 | +| Dimension (weight) | Lessons from the literature | | |
| 389 | +|---|---| | |
| 390 | +| **`automatability` (0.35)** | This is Eloundou's E1/E2 construct + SML's rubric. Use a multi-criterion, multi-model LLM rubric with a time-savings-at-quality threshold; keep 5-point scale (matches ILO gradient practice and our expert-review disagreement rule). Distinguish "LLM alone" (E1) from "LLM + tooling/agents" (E2) — capability-staged variants (OAIES; OECD capability levels) show staging by AI generation makes scores updateable rather than obsolete. Critically: rate **automation vs augmentation potential separately per task** (ILO 2023/2025) so the composite's sub-scores are computed, not asserted. | | |
| 391 | +| **`feasibility` (0.20)** | Separating "conceivable" from "deployable now" is what F&O failed to do and what killed their forecast. Ground feasibility in *observed evidence*: AEI task-success rates (66–70% by complexity — Jan 2026 primitives), benchmark-linked measures (OECD AI Capability Indicators' 9 domains with attained levels; "Jagged Global Economy"), and Iceberg's tool-catalogue approach (is there an actual product performing this skill?). Feasibility should decay-adjust automatability: high automatability + low current success rate ⇒ wide CI, lower composite. | | |
| 392 | +| **`cost_ratio` (0.15)** | Least developed dimension in the literature — a genuine gap we can own. Only Webb (implicitly, via wages), Goldman (25% substitution assumption), and AEI's $/hr wage-equivalent task values touch it. Use O*NET-linked BLS wages (as AEI does: $48–49/hr average task value) vs API/inference cost per task-equivalent; store as integer cents per our conventions. Note Acemoglu's caution ("so-so automation"): low cost ratio can drive adoption even with mediocre quality — interact with feasibility. | | |
| 393 | +| **`barriers` (0.20)** | Directly validated by IMF's C-AIOE complementarity (physical presence, human contact, legal responsibility) — the reason judges are exposed but safe and telemarketers are not. Pre-2022 null results (OECD 2021; Acemoglu et al. 2022) prove barriers dominate short-run outcomes. Operationalize the IMF/Pizzinelli shielding factors at task level: regulation/licensing, liability, required physical presence, human-contact preference, data confidentiality. Humlum & Vestergaard (Denmark) show even *adoption* without workflow redesign yields ~3% time savings — organizational barriers belong here too. | | |
| 394 | +| **`adoption_velocity` (0.10)** | The 2025–26 revolution: use **measured** adoption, not guesses. Sources: AEI Hugging Face releases (task-level usage shares, automation:augmentation ratio, AUI by geography — open data, quarterly cadence), PwC Jobs Barometer (vacancy-side skill change 66% faster in exposed occupations), OpenAI usage paper. Sector velocity is empirically uneven (API vs consumer concentration diverging; GDP-elasticity of adoption 0.7) — justify per-sector velocity scores with these citations. Design for **time-series updates**: AEI shows adoption shares move 10+ pp in a year (automation 27%→39%→45–52% oscillation), so this dimension must re-score every `INDEX_VERSION`. | | |
| 395 | + | |
| 396 | +### Positioning / product implications | |
| 397 | + | |
| 398 | +- **Transparency is our moat and the field's known weakness:** McKinsey/Goldman are black boxes; even academic scores rarely ship rater-level data. We publish weights (`/api/v1/methodology`), prompts (versioned), per-model ratings, and CIs — no major index does all four. | |
| 399 | +- **Versioning is becoming an explicit norm** (OECD exposure measure designed to be updateable; AEI releases dated datasets). Our `INDEX_VERSION` + immutable `score_runs` architecture matches best practice; cite OECD/AEI precedent in METHODOLOGY.md. | |
| 400 | +- **CI bounds have empirical semantics now:** rater disagreement (Yin et al.) + human-LLM calibration error (ILO–NASK) + feasibility uncertainty (AEI success rates) are the three quantifiable components of `score_low`/`score_high`. | |
| 401 | +- **EU/France (ESCO/ROME) crosswalk:** ILO WP140 (ISCO-based) and the UK index (arXiv 2507.22748) are the reference patterns for adapting O*NET-trained scores to other taxonomies; document crosswalk loss explicitly. | |
| 402 | +- **Watch list for future versions:** agentic-AI exposure extensions (arXiv 2604.00186), benchmark-grounded dynamic scores (OECD capability levels; "Jagged Global Economy"), worker-centered metrics (Lund et al. taxonomy), and the Canaries dashboard as a rolling validation target. | |
| 403 | + | |
| 404 | +--- | |
| 405 | + | |
| 406 | +*Compiled via WebSearch, Tavily, WebFetch, and OpenAlex queries, 2026-08-05. All URLs verified live at compile time unless noted. This document feeds `docs/methodology/METHODOLOGY.md` §Related Work.* | |
added
docs/research/02-data-sources.md
+129 −0
Large diff (129 lines) is not loaded by default to keep the page fast.Load diff
added
docs/research/03-llm-rater-api.md
+329 −0
Large diff (329 lines) is not loaded by default to keep the page fast.Load diff
added
docs/research/04-landscape-and-evidence.md
+307 −0
Large diff (307 lines) is not loaded by default to keep the page fast.Load diff
added
docs/research/README.md
+41 −0
Large diff (41 lines) is not loaded by default to keep the page fast.Load diff
added
eslint.config.mjs
+25 −0
Large diff (25 lines) is not loaded by default to keep the page fast.Load diff
added
infra/.env.example
+20 −0
Large diff (20 lines) is not loaded by default to keep the page fast.Load diff
added
infra/deploy.sh
+19 −0
Large diff (19 lines) is not loaded by default to keep the page fast.Load diff
added
infra/docker-compose.dev.yml
+19 −0
Large diff (19 lines) is not loaded by default to keep the page fast.Load diff
added
infra/docker-compose.prod.yml
+23 −0
Large diff (23 lines) is not loaded by default to keep the page fast.Load diff
added
infra/ngrok.yml
+11 −0
Large diff (11 lines) is not loaded by default to keep the page fast.Load diff
added
package.json
+29 −0
Large diff (29 lines) is not loaded by default to keep the page fast.Load diff
added
packages/config/package.json
+6 −0
Large diff (6 lines) is not loaded by default to keep the page fast.Load diff
added
packages/config/tsconfig/base.json
+16 −0
Large diff (16 lines) is not loaded by default to keep the page fast.Load diff
added
packages/config/tsconfig/nextjs.json
+13 −0
Large diff (13 lines) is not loaded by default to keep the page fast.Load diff
added
packages/db/package.json
+30 −0
Large diff (30 lines) is not loaded by default to keep the page fast.Load diff
added
packages/db/prisma/schema.prisma
+150 −0
Large diff (150 lines) is not loaded by default to keep the page fast.Load diff
added
packages/db/prisma/seed.ts
+46 −0
Large diff (46 lines) is not loaded by default to keep the page fast.Load diff
added
packages/db/src/index.ts
+12 −0
Large diff (12 lines) is not loaded by default to keep the page fast.Load diff
added
packages/db/tsconfig.json
+4 −0
Large diff (4 lines) is not loaded by default to keep the page fast.Load diff
added
packages/scoring/package.json
+24 −0
Large diff (24 lines) is not loaded by default to keep the page fast.Load diff
added
packages/scoring/src/composite.test.ts
+71 −0
Large diff (71 lines) is not loaded by default to keep the page fast.Load diff
added
packages/scoring/src/composite.ts
+60 −0
Large diff (60 lines) is not loaded by default to keep the page fast.Load diff
added
packages/scoring/src/index.ts
+18 −0
Large diff (18 lines) is not loaded by default to keep the page fast.Load diff
added
packages/scoring/src/properties.test.ts
+118 −0
Large diff (118 lines) is not loaded by default to keep the page fast.Load diff
added
packages/scoring/src/task.ts
+99 −0
Large diff (99 lines) is not loaded by default to keep the page fast.Load diff
added
packages/scoring/src/types.ts
+51 −0
Large diff (51 lines) is not loaded by default to keep the page fast.Load diff
added
packages/scoring/src/version.ts
+5 −0
Large diff (5 lines) is not loaded by default to keep the page fast.Load diff
added
packages/scoring/src/weights.ts
+30 −0
Large diff (30 lines) is not loaded by default to keep the page fast.Load diff
added
packages/scoring/tsconfig.json
+4 −0
Large diff (4 lines) is not loaded by default to keep the page fast.Load diff
added
packages/ui/package.json
+25 −0
Large diff (25 lines) is not loaded by default to keep the page fast.Load diff
added
packages/ui/src/index.ts
+1 −0
Large diff (1 lines) is not loaded by default to keep the page fast.Load diff
added
packages/ui/src/score-band.tsx
+23 −0
Large diff (23 lines) is not loaded by default to keep the page fast.Load diff
added
packages/ui/tsconfig.json
+8 −0
Large diff (8 lines) is not loaded by default to keep the page fast.Load diff
added
pnpm-lock.yaml
+3425 −0
Large diff (3,425 lines) is not loaded by default to keep the page fast.Load diff
added
pnpm-workspace.yaml
+3 −0
Large diff (3 lines) is not loaded by default to keep the page fast.Load diff
added
turbo.json
+19 −0
Large diff (20 lines) is not loaded by default to keep the page fast.Load diff