From 02cfedc09229fa1f5cce964988b57c27abe8cf3e Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 22 Sep 2026 17:24:46 +0000 Subject: [PATCH] Count the citations 15 studies actually carry, and split the link check out countReferences() matched "References", "Sources" and "Bibliography". 189 of the 204 studies use the first of those. The 15 written to the later, longer template head their citations "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. The function 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, Narayanan and Das in EPW. 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 in fact a heading the regex did not know. Acting on the warning as written would have meant adding citations to studies that already have them. No study carries both headings, verified across all 204, so widening the regex counts nothing twice. One consequence worth recording rather than patching. evidenceTier() returns "strong" when refs >= 4, and every one of the 204 studies has four or five references, so after this fix the function returns "strong" for all of them and the moderate and emerging branches are unreachable. Before the fix eight studies read "emerging", and all eight were among the fifteen whose references were not being counted: the tier was reporting the parser bug rather than the evidence. What counts as strong evidence is an editorial judgement about the library, not a bug to fix here, so CLAUDE.md states the position and the options and leaves the threshold alone. Separately, the lychee link check moves out of ci.yml into its own daily workflow. 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, where a job cancelled by its own timeout cancels the whole run and a slow third-party host could have taken the validation with it. On the schedule it fails, because external link rot needs no commit. Adds CLAUDE.md, which this repository did not have. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01MrvR2NXsJFVRCeJZFCPuNL --- .github/workflows/ci.yml | 12 ----- .github/workflows/link-check.yml | 31 ++++++++++++ CLAUDE.md | 85 ++++++++++++++++++++++++++++++++ data/master-list.json | 46 ++++++++--------- data/stats.json | 8 +-- scripts/build.mjs | 11 ++++- 6 files changed, 151 insertions(+), 42 deletions(-) create mode 100644 .github/workflows/link-check.yml create mode 100644 CLAUDE.md diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bcd8f3e..99055b3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 diff --git a/.github/workflows/link-check.yml b/.github/workflows/link-check.yml new file mode 100644 index 0000000..b8da939 --- /dev/null +++ b/.github/workflows/link-check.yml @@ -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 diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..d89f7ad --- /dev/null +++ b/CLAUDE.md @@ -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=`, rendered client side from + `data/studies/.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. diff --git a/data/master-list.json b/data/master-list.json index 6a5fb5d..166bd3c 100644 --- a/data/master-list.json +++ b/data/master-list.json @@ -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, @@ -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" }, { @@ -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" }, { @@ -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" }, { @@ -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, @@ -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, @@ -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" }, { @@ -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" }, { @@ -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, @@ -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, @@ -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" }, { @@ -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, @@ -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, @@ -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, @@ -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" }, { diff --git a/data/stats.json b/data/stats.json index 1101ca0..fbc6f75 100644 --- a/data/stats.json +++ b/data/stats.json @@ -4,7 +4,7 @@ "countries": 117, "regions": 7, "topics": 10, - "references": 867, + "references": 942, "words": 227458 }, "byTopic": [ @@ -132,11 +132,7 @@ "byEvidence": [ { "label": "strong", - "count": 196 - }, - { - "label": "emerging", - "count": 8 + "count": 204 } ] } diff --git a/scripts/build.mjs b/scripts/build.mjs index 0e10755..2d4836a 100644 --- a/scripts/build.mjs +++ b/scripts/build.mjs @@ -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())