Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 0 additions & 12 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,3 @@ jobs:

- name: Validate data & derived artefacts are in sync
run: npm run validate

check-links:
name: Check links
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6

- name: Link Checker
uses: lycheeverse/lychee-action@v2
with:
args: --accept 200,204,301,302,403,429 --exclude-mail --exclude "localhost|127.0.0.1|example.com|twitter.com|x.com|linkedin.com|facebook.com|instagram.com|t.me|wa.me|play.google.com|apps.apple.com" --timeout 30 --max-retries 2 "**/*.html" "**/*.md"
fail: false
31 changes: 31 additions & 0 deletions .github/workflows/link-check.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
name: Link check

# Split out of ci.yml on 2026-09-22 for two reasons.
#
# It carried `fail: false` on pull requests, so a contributor got a result they
# could neither act on nor be blocked by. And it shared a workflow run with the
# data validation: this job talks to a few hundred third-party hosts, and a job
# cancelled by its own timeout cancels the whole run, so a slow external host
# could take the validation down with it and report the run as cancelled.
#
# On the schedule it fails, because external link rot needs no commit to happen
# and so belongs on a timer rather than on a pull request.
on:
schedule:
- cron: '41 6 * * *'
workflow_dispatch:

jobs:
links:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4

- uses: lycheeverse/lychee-action@v2
with:
args: >-
--accept 200,204,301,302,403,429 --exclude-mail
--exclude "localhost|127.0.0.1|example.com|twitter.com|x.com|linkedin.com|facebook.com|instagram.com|t.me|wa.me|play.google.com|apps.apple.com"
--timeout 30 --max-retries 2 "**/*.html" "**/*.md"
fail: true
85 changes: 85 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# dev-case-studies

Research library of 204 cited development case studies from across the Global
South. Static HTML, no framework, one generator script. Deployed to GitHub
Pages under `/dev-case-studies/`.

## Commands

```bash
npm run validate # node scripts/build.mjs --check, run by CI
npm run build # rewrites master-list.json, search-index.json, stats.json, sitemap.xml
npm run dev # local server
```

There are **no dependencies**. `package.json` declares neither `dependencies`
nor `devDependencies`, so there is no lockfile to commit and `npm ci` has
nothing to install. That is deliberate, not an omission.

## One source, four derived files

`data/studies/*.json` is the only thing anyone edits. `scripts/build.mjs`
derives `data/master-list.json`, `data/search-index.json`, `data/stats.json`
and `sitemap.xml` from it, and `--check` fails if any of the four has drifted.
Verified on 2026-09-22: all 204 slugs agree across all four.

## Two templates, and the citation heading differs between them

189 studies head their citations `## References`. The 15 written to the later,
longer template head them `## Further Reading`, and that set is almost exactly
South Asia: six India studies, two Bangladesh, two Nepal, Pakistan, Sri Lanka,
Bhutan, the Maldives and a regional stunting study.

`countReferences()` matched `References|Sources|Bibliography` and not
`Further Reading`, so it returned **0** for all fifteen. They carry five full
academic citations each (Imbert and Papp in *AEJ: Applied* 2015, Dreze and
Khera in *World Development* 2017, Sukhtankar in the *India Policy Forum*, and
so on), so the library reported 867 references where it holds **942**.

The `--check` run printed `no parseable References section` for each of them,
which reads as a content gap and was a heading the regex did not know. The
distinction matters: acting on the warning as written would mean adding
citations to studies that already have them. No study carries both headings,
so widening the regex counts nothing twice.

**If you add a third template, add its heading to that regex.**

## The evidence tier no longer distinguishes anything

`evidenceTier()` returns `strong` when `refs >= 4`. Every one of the 204
studies has four or five references, so after the reference fix the function
returns `strong` for all 204, and the `moderate` and `emerging` branches are
unreachable.

Before the fix, eight studies read `emerging`. All eight were among the fifteen
whose references were not being counted. **The tier was reporting the parser
bug rather than the evidence**, which is worth knowing before anyone reads
meaning into the field.

This is left as it is on purpose. What counts as strong evidence is an
editorial judgement about the library, not a bug to patch: the honest options
are to raise the threshold, weight the `hasRCT` and `hasMeta` signals that the
`refs >= 4` branch currently short-circuits, or drop the field and stop
implying a distinction the data does not carry. Picking one is the owner's
call.

## Watch out for

- **The sitemap uses query strings.** Every study is
`/dev-case-studies/study.html?slug=<slug>`, rendered client side from
`data/studies/<slug>.json`. A script comparing sitemap paths against files on
disk will report all 204 as missing pages; compare slugs.
- **The GitHub Pages path prefix is in the sitemap** and not in the repository
layout, for the same reason.
- **Counts stated in prose**: `README.md` says 204 case studies, which is
correct today. Nothing checks it.

## Testing

`.github/workflows/ci.yml` runs `npm run validate` on every push and pull
request. The lychee link check moved to its own `link-check.yml` on
2026-09-22: it carried `fail: false` on pull requests, so a contributor got a
result they could neither act on nor be blocked by, and it shared a run with
the data validation, where a job cancelled by its own timeout cancels the whole
run and would have taken the validation down with it. On the daily schedule it
fails, because external link rot needs no commit to happen.
46 changes: 23 additions & 23 deletions data/master-list.json
Original file line number Diff line number Diff line change
Expand Up @@ -640,8 +640,8 @@
"summary": "India's Unified Payments Interface (UPI), launched in 2016, became the world's most successful real-time digital payments system, processing over 13 billion transactions monthly by 2024. UPI enabled India to leapfrog card-based payment infrastructure, bringing millions of small merchants into the digital economy, though its zero-fee model raises long-term sustainability questions.",
"readingMinutes": 16,
"words": 3397,
"refs": 0,
"evidence": "emerging"
"refs": 5,
"evidence": "strong"
},
{
"id": 47,
Expand All @@ -654,7 +654,7 @@
"summary": "India built the world's most ambitious digital welfare delivery system using the JAM Trinity -- Jan Dhan bank accounts, Aadhaar biometric identity, and Mobile phones -- channeling over 30 lakh crore rupees in direct benefit transfers since 2013. While the system significantly reduced leakage in programs like LPG subsidies, it also created painful exclusion errors where genuine beneficiaries lost access due to biometric failures and connectivity gaps.",
"readingMinutes": 12,
"words": 2604,
"refs": 0,
"refs": 5,
"evidence": "strong"
},
{
Expand All @@ -668,7 +668,7 @@
"summary": "The Mahatma Gandhi National Rural Employment Guarantee Act, enacted in 2005, guarantees 100 days of manual work per year to every rural household, making it the world's largest workfare program with over 260 million registered workers. Rigorous evidence shows it raised rural wages by 4-8% and reduced poverty, while serving as a critical safety net during droughts and COVID-19, though chronic payment delays and wide interstate variation in implementation quality persist.",
"readingMinutes": 13,
"words": 2651,
"refs": 0,
"refs": 5,
"evidence": "strong"
},
{
Expand All @@ -682,7 +682,7 @@
"summary": "Kerala achieved life expectancy of 77.3 years, literacy of 96.2%, and infant mortality of 6 per 1,000 -- comparable to middle-income countries -- on per-capita income that remained below the Indian average for most of its history. Built on radical land reform, massive public investment in education and health, and a strong redistributive political culture, the model now faces challenges from fiscal stress, aging, and dependence on Gulf remittances.",
"readingMinutes": 15,
"words": 3072,
"refs": 0,
"refs": 5,
"evidence": "strong"
},
{
Expand All @@ -696,8 +696,8 @@
"summary": "India's PLI scheme, launched in 2020-21 with a committed outlay of 1.97 lakh crore rupees across 14 sectors, represents the country's most explicit embrace of industrial policy since 1991 liberalization. Mobile phone manufacturing has been the standout success, with exports surging from $3 billion to over $15 billion, though results across other sectors remain deeply uneven.",
"readingMinutes": 14,
"words": 2910,
"refs": 0,
"evidence": "emerging"
"refs": 5,
"evidence": "strong"
},
{
"id": 51,
Expand All @@ -710,8 +710,8 @@
"summary": "The Swachh Bharat Mission built over 110 million household toilets between 2014 and 2019, dramatically reducing open defecation from 550 million people to an estimated 50-100 million. While this was one of the largest behavior change campaigns in history, significant gaps between declared Open Defecation Free status and actual toilet usage persist, particularly in northern India, and health impact evidence remains mixed.",
"readingMinutes": 15,
"words": 3127,
"refs": 0,
"evidence": "emerging"
"refs": 5,
"evidence": "strong"
},
{
"id": 52,
Expand All @@ -724,7 +724,7 @@
"summary": "BRAC pioneered the ultra-poor graduation model combining asset transfers, skills training, cash stipends, and intensive coaching to permanently lift the extreme poor out of destitution. A landmark six-country RCT published in Science found sustained income gains of 38% three years after the program ended, making it one of the most rigorously validated approaches to ending extreme poverty, now replicated in over 50 countries.",
"readingMinutes": 17,
"words": 3548,
"refs": 0,
"refs": 5,
"evidence": "strong"
},
{
Expand All @@ -738,7 +738,7 @@
"summary": "Bangladesh's ready-made garment sector grew from $31 million in exports in 1983 to over $45 billion by 2023, employing roughly 4 million workers -- approximately 80% women -- and becoming the single most important driver of the country's poverty reduction. The 2013 Rana Plaza collapse, which killed 1,134 workers, exposed systemic safety failures, but post-disaster reforms demonstrated that international pressure could improve factory conditions without destroying the industry.",
"readingMinutes": 14,
"words": 3026,
"refs": 0,
"refs": 5,
"evidence": "strong"
},
{
Expand All @@ -752,8 +752,8 @@
"summary": "Nepal's community forestry program transferred management of national forests to over 22,500 local Community Forest User Groups managing 2.3 million hectares -- roughly one-third of the country's forests -- involving 2.9 million households. The program reversed decades of deforestation in the middle hills, though serious equity critiques persist around elite capture of benefits and limited women's participation in decision-making.",
"readingMinutes": 16,
"words": 3379,
"refs": 0,
"evidence": "emerging"
"refs": 5,
"evidence": "strong"
},
{
"id": 55,
Expand All @@ -766,8 +766,8 @@
"summary": "Nepal's migrant worker remittances reached $9.3 billion in 2022-23, equivalent to 23-25% of GDP, making it one of the world's most remittance-dependent economies. While remittances drove a 50% poverty reduction since 2000, at least 7,500 Nepali workers died abroad between 2008 and 2020, and the economy exhibits classic Dutch disease symptoms as manufacturing and agriculture stagnate.",
"readingMinutes": 18,
"words": 3755,
"refs": 0,
"evidence": "emerging"
"refs": 5,
"evidence": "strong"
},
{
"id": 56,
Expand All @@ -780,7 +780,7 @@
"summary": "Pakistan's BISP, launched in 2008, grew into one of the developing world's largest social safety nets, reaching over 9 million families through an innovative poverty scorecard system with payments made directly to women. A rare example of institutional continuity across Pakistan's fractured political landscape, BISP proved that even fragile states can build durable social protection, though transfers remain too small to be transformative.",
"readingMinutes": 14,
"words": 3024,
"refs": 0,
"refs": 5,
"evidence": "strong"
},
{
Expand All @@ -794,8 +794,8 @@
"summary": "Sri Lanka experienced its worst economic crisis since independence in 2022, defaulting on $51 billion in foreign debt as foreign reserves fell below $50 million and inflation spiked to 69.8%. The crisis was driven by populist tax cuts, pandemic-induced tourism collapse, and a catastrophic overnight ban on synthetic fertilizers, ultimately forcing the president to flee the country and triggering an IMF bailout program.",
"readingMinutes": 17,
"words": 3493,
"refs": 0,
"evidence": "emerging"
"refs": 5,
"evidence": "strong"
},
{
"id": 58,
Expand All @@ -808,8 +808,8 @@
"summary": "Since 2008, Bhutan has operationalized Gross National Happiness as a formal policy screening tool with a 33-indicator index, a dedicated commission with veto power over legislation, and periodic national surveys across 9 domains of wellbeing. While achieving strong forest conservation, near-universal education, and low inequality, Bhutan faces 29% youth unemployment and economic dependence on hydropower.",
"readingMinutes": 15,
"words": 3052,
"refs": 0,
"evidence": "emerging"
"refs": 5,
"evidence": "strong"
},
{
"id": 59,
Expand All @@ -822,8 +822,8 @@
"summary": "The Maldives, with 80% of its islands less than one metre above sea level, faces an existential climate threat and has pursued adaptation ranging from building the artificial island of Hulhumale to house a third of its population, to pioneering climate diplomacy at the UN. The nation's survival strategy is constrained by the paradox that its economic lifeline -- luxury tourism -- drives the very emissions threatening its existence.",
"readingMinutes": 15,
"words": 3062,
"refs": 0,
"evidence": "emerging"
"refs": 5,
"evidence": "strong"
},
{
"id": 60,
Expand All @@ -836,7 +836,7 @@
"summary": "South Asia is home to approximately 57 million stunted children under five -- more than sub-Saharan Africa -- with India alone accounting for one-third of the world's stunted children. Bangladesh achieved the region's most dramatic reduction from 51% to 28%, largely through sanitation elimination, women's empowerment, and effective community health workers, while India and Pakistan have lagged despite far greater resources.",
"readingMinutes": 16,
"words": 3347,
"refs": 0,
"refs": 5,
"evidence": "strong"
},
{
Expand Down
8 changes: 2 additions & 6 deletions data/stats.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"countries": 117,
"regions": 7,
"topics": 10,
"references": 867,
"references": 942,
"words": 227458
},
"byTopic": [
Expand Down Expand Up @@ -132,11 +132,7 @@
"byEvidence": [
{
"label": "strong",
"count": 196
},
{
"label": "emerging",
"count": 8
"count": 204
}
]
}
11 changes: 10 additions & 1 deletion scripts/build.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,17 @@ function readingMinutes(content) {
return Math.max(1, Math.round(countWords(content) / WORDS_PER_MINUTE));
}

// The citation section is headed "References" in 189 studies and "Further
// Reading" in the 15 written to the later, longer template, which is every
// South Asia study bar two. Those 15 carry five full academic citations each
// (Imbert and Papp in AEJ: Applied, Dreze and Khera in World Development, and
// so on) and this function returned 0 for all of them, so the library reported
// 867 references where it holds 942, and the --check run printed "no parseable
// References section" as a warning that reads like missing citations and was
// in fact a heading this regex did not know. No study carries both headings,
// so nothing is counted twice.
function countReferences(content) {
const m = (content || '').match(/## (?:References|Sources|Bibliography)\s*\n([\s\S]*?)(?=\n## |\n---\s*$|$)/i);
const m = (content || '').match(/## (?:References|Sources|Bibliography|Further Reading)\s*\n([\s\S]*?)(?=\n## |\n---\s*$|$)/i);
if (!m) return 0;
return m[1].trim().split('\n')
.map((l) => l.replace(/^[-*]\s*/, '').replace(/^\d+\.\s*/, '').trim())
Expand Down
Loading