Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
137 commits
Select commit Hold shift + click to select a range
a0ba3e1
feat(#218): canonical Panama food_acquired + sample; source-flag s-axis
ligon Jun 21, 2026
23fb11f
fix(#218): register Panama in data_info.yml Currency section (PAB)
Jun 25, 2026
2d3d5f7
Merge pull request #581 from ligon/feat/218-panama-food-acquired-cano…
ligon Jun 27, 2026
d97ad8d
feat(coverage): adopt the `blessed` tier (bless-on-first-use)
ligon Jul 12, 2026
e55146c
ledger: household_characteristics residence filter — roster_to_charac…
ligon Jul 12, 2026
8dcd2bc
feat(coverage): Phase 0 — fix the ruler (absent verdicts + wave-slice…
ligon Jul 12, 2026
85bdf4a
fix(household_characteristics): stop the residence filter deleting wh…
ligon Jul 12, 2026
e22b442
docs(coverage): design doc — the discipline and workplan for filling …
ligon Jul 12, 2026
6af3d32
feat(coverage): Phase 0 cont. — unconfigured countries, no-microdata …
ligon Jul 12, 2026
1f48cb3
docs(CLAUDE): correct the .pth / PYTHONPATH worktree guidance — it wa…
ligon Jul 12, 2026
c3fbdfe
docs(Burkina_Faso/2014): record WHY MonthsSpent B3A maps 12/0, not 12/6
ligon Jul 12, 2026
6bd1f31
feat(provenance): record each wave's WB catalog id; match discover_wa…
ligon Jul 12, 2026
364dd72
docs(coverage): correct Defect 3 — my "no wave records its provenance…
ligon Jul 12, 2026
facdc7b
chore(Benin): remove the 2018-2019 duplicate-wave directory
ligon Jul 12, 2026
f7b5291
Merge pull request #593 from ligon/feat/coverage-blessed
ligon Jul 12, 2026
68b9425
Merge pull request #594 from ligon/fix/roster-characteristics-wave-de…
ligon Jul 12, 2026
598b89c
Merge pull request #595 from ligon/feat/wave-provenance-catalog-id
ligon Jul 12, 2026
b745ffd
feat(provenance): resolve the last 5 unknown waves from evidence
ligon Jul 12, 2026
3265ff9
feat(coverage): add the `blocked` tier — wanted, known, unobtainable
ligon Jul 12, 2026
4857930
docs(coverage): record the acquisition queue + the #597 scope decision
ligon Jul 12, 2026
96fe292
feat(coverage): absorb GH #552's obtainability tiering into blocked_s…
ligon Jul 12, 2026
7f80b67
docs: a plan for the backlog — ordered by harm, with the matrix takin…
ligon Jul 12, 2026
150f318
Merge pull request #596 from ligon/feat/wave-provenance-resolve-unknowns
ligon Jul 12, 2026
006cdb3
Merge pull request #598 from ligon/feat/coverage-blocked-tier
ligon Jul 12, 2026
96c10c3
feat(discovery): search the repositories where a country's series act…
ligon Jul 12, 2026
027c3b1
feat(discovery): Liberia HIES in remit; separate identity from remit
ligon Jul 12, 2026
4b927c5
feat(capability): record what an instrument MEASURES, at acquisition …
ligon Jul 12, 2026
c8c25f6
fix(schema): enforce declared spellings; fix 5 countries shipping non…
ligon Jul 12, 2026
c96d605
fix(#548): GhanaLSS split households merged into one by bespoke panel…
ligon Jul 12, 2026
5e136db
fix(#602): drop India's Rural declaration, correct strata assertion, …
ligon Jul 12, 2026
3fd235c
docs(#602): correct two false statements flagged by red-team (prose o…
Jul 12, 2026
2a775f3
ci(#602): silent-skip the per-country regressions where there is no d…
ligon Jul 12, 2026
3ef4143
fix(guatemala): use the real ENCOVI 2000 PSU as v (GH #323)
ligon Jul 13, 2026
3ce1b0f
fix(framework): audit the grain collapse, and make the signal survive…
ligon Jul 13, 2026
36170a5
fix(#323): Benin/Togo plot_inputs — make harmonize_seed_crop injective
ligon Jul 13, 2026
ef45231
ledger(#323): Benin/Togo — CIV already solved this; crop map was the …
ligon Jul 13, 2026
66b2114
fix(#323 site 2): audit the household -> cluster collapse in Wave.clu…
ligon Jul 13, 2026
4d6f172
fix(#323): Mali 2014-15 — pid is s01q00, not s01q (32,026 people rest…
ligon Jul 13, 2026
45ca6cd
ledger(#323): Mali pid — the correct formula was already in people_la…
ligon Jul 13, 2026
48f4d08
feat(#323): shared explicit-reducer helpers (country-invoked, fail-loud)
ligon Jul 13, 2026
ac2e847
fix(#323 site 2): retire the GPS .mean() -- core now aggregates nothing
ligon Jul 13, 2026
1899421
docs: a script is a complication -- go read CONTENTS.org before you edit
Jul 13, 2026
ad98db9
fix(EthiopiaRHS): stop double-counting data-entry double-punches in f…
ligon Jul 13, 2026
e0dbc45
fix: get development back to green (3 pre-existing failures)
ligon Jul 13, 2026
3a387bc
fix(Tanzania): name the household grain; stop guessing an ambiguous c…
ligon Jul 13, 2026
fe9e471
Merge origin/development into fix/323-framework
Jul 13, 2026
db55bc8
Merge pull request #614 from ligon/fix/323-framework
ligon Jul 13, 2026
0f64e29
Merge fix/323-framework into fix/323-site2-cluster-features
Jul 13, 2026
3488b79
fix(#323): geo config for Ethiopia + Niger — unblock the site-4 requi…
ligon Jul 13, 2026
552d57a
Merge pull request #617 from ligon/fix/323-site2-cluster-features
ligon Jul 13, 2026
5ebb36f
Merge pull request #624 from ligon/fix/red-development
ligon Jul 13, 2026
34f3c0d
data(CotedIvoire): DVC-track the GPS file; Lat/Lon are REQUIRED again
Jul 13, 2026
51a501f
Merge pull request #599 from ligon/fix/597-widen-discovery
ligon Jul 13, 2026
eae9def
Merge pull request #619 from ligon/docs/script-is-a-complication
ligon Jul 14, 2026
4e11ec2
Merge pull request #604 from ligon/fix/548-panelids
ligon Jul 14, 2026
67fb527
Merge pull request #630 from ligon/fix/civ-gps-dvc
ligon Jul 20, 2026
6278474
Merge pull request #605 from ligon/fix/602-spellings
ligon Jul 20, 2026
a45db8b
Merge pull request #608 from ligon/fix/323-guatemala
ligon Jul 20, 2026
fc7b4ad
Merge pull request #615 from ligon/fix/323-benin-togo
ligon Jul 20, 2026
db45c8c
Merge pull request #616 from ligon/fix/323-mali-pid
ligon Jul 20, 2026
b4ff38b
Merge pull request #618 from ligon/fix/323-explicit-reducers
ligon Jul 20, 2026
6076bc8
Merge pull request #621 from ligon/fix/323-ethiopiarhs-config
ligon Jul 20, 2026
53ef782
Merge pull request #626 from ligon/fix/323-tanzania-config
ligon Jul 20, 2026
53ef3a5
Merge pull request #628 from ligon/fix/323-geo-config-eth-ner
ligon Jul 20, 2026
e576752
fix(#323): the D1 guard checks coupling via AST, not a substring
ligon Jul 21, 2026
7ca778d
fix(#323): close the re-export evasion in the D1 coupling guard
ligon Jul 21, 2026
c26e27f
test(#323): state the grain contract as behaviour, not as names
ligon Jul 21, 2026
7d8647a
Merge pull request #635 from ligon/fix/323-guard-ast
ligon Jul 21, 2026
11b2ffd
merge development
ligon Jul 21, 2026
051ee5e
test(#323): correct P2 -- the composite is COMPLETION, not fabrication
ligon Jul 21, 2026
6d5f963
fix(Malawi): 2004-05's cluster key was an intra-TA sequence number (G…
ligon Jul 21, 2026
cbbba76
docs: the DVC rule is "never invoke the CLI", not "avoid two subcomma…
ligon Jul 21, 2026
dea63ed
fix(#323): the D1 guard had its polarity inverted -- guard choosing, …
ligon Jul 21, 2026
20f7ec0
Merge pull request #631 from ligon/docs/dvc-access-rule
ligon Jul 21, 2026
d7d57ce
Merge pull request #640 from ligon/fix/323-guard-polarity
ligon Jul 21, 2026
465f447
Merge pull request #636 from ligon/test/323-grain-contract
ligon Jul 21, 2026
243cb21
test: one creds guard, and a net for the modules that forget it
Jul 21, 2026
31e123d
ledger(#323): condition on Uganda crop_production -- reuse _harmonize…
ligon Jul 21, 2026
b28b502
fix(#323): add a `condition` index level to Uganda crop_production
ligon Jul 21, 2026
45aee17
Merge pull request #648 from ligon/test/conftest-creds-skip
ligon Jul 22, 2026
24712f6
verify(#323): Ethiopia is GREEN under #627 — plus one real education-…
ligon Jul 21, 2026
2014b7a
fix(#323): Mali 2021-22 cluster_features — delete the 4.3M-phantom-ro…
ligon Jul 21, 2026
4b379a8
fix(#323): Benin cluster_features — project in the extraction, enforc…
ligon Jul 21, 2026
7b8265a
fix(Guinea-Bissau): map the Lusophone 'Urbano' label (#323)
ligon Jul 13, 2026
b84b192
fix(#323): Nigeria -- rekey `v` to cluster_id(state, lga, ea) [config…
Jul 13, 2026
a4599a5
fix(#323): site 4 — a cardinality guard on the `dfs:` sub-frame merge
ligon Jul 13, 2026
4043c71
test(#323): Nigeria cluster-identity tests must silent-skip without S…
Jul 21, 2026
c7dee21
docs(#637): record the (t, i, pid) key review at the six EHCVM people…
ligon Jul 21, 2026
620615f
fix(Malawi): 2004-05's cluster key was an intra-TA sequence number (G…
ligon Jul 21, 2026
38be590
fix(Tanzania): describe a cluster from inside it; re-key 2020-21 (GH …
ligon Jul 21, 2026
a54568f
docs(#323): record the stale-cache masking in the site-4 ledger
ligon Jul 13, 2026
6c54279
fix(India): employment is ACTIVITY-level — declare `act`, ending the …
ligon Jul 13, 2026
952e6f7
fix(#323): assets Value is additive across Nigeria W2's per-unit roster
Jul 13, 2026
8bef453
fix(#323): assets Value is additive; an all-NA group is NA, not a fab…
Jul 13, 2026
6cb2b78
Merge remote-tracking branch 'origin/development' into fix/323-malawi…
ligon Jul 22, 2026
7aa08ec
fix(#323): losslessness is per COLUMN, so the additive audit must be too
ligon Jul 22, 2026
c8b7e63
fix(#323): apply PR #649 review — correct three comments, close the s…
ligon Jul 22, 2026
3aa605b
docs(Tanzania): the NPS back-casting is a COUNTRY fact, not a food_ac…
Jul 22, 2026
38e6d28
fix(#323/#627): Malawi's and Guinea-Bissau's cartesian `dfs:` merges
ligon Jul 22, 2026
1234660
docs(#323): record the per-wave-label-drift convention harvest_condit…
ligon Jul 22, 2026
eadafe7
Merge pull request #611 from ligon/fix/323-india
ligon Jul 22, 2026
976c127
docs: record WHY this needs to be a dispatcher rule -- two existing r…
Jul 22, 2026
74d135f
docs(#323): record where this departs from Nigeria's KNOWN OPEN DEFEC…
ligon Jul 22, 2026
7bfd571
test: load `requires_s3` by PATH, not by import
ligon Jul 22, 2026
194b55d
Merge pull request #625 from ligon/fix/323-nigeria-config
ligon Jul 22, 2026
206727a
Merge pull request #633 from ligon/fix/323-benin-config
ligon Jul 22, 2026
0659da2
Merge pull request #638 from ligon/docs/637-ehcvm-people-last7days-ke…
ligon Jul 22, 2026
e06752d
Merge pull request #639 from ligon/fix/323-malawi-config
ligon Jul 22, 2026
a7cdee6
Merge pull request #641 from ligon/fix/323-mali-cartesian
ligon Jul 22, 2026
dedb77c
Merge pull request #642 from ligon/fix/323-tanzania-config
ligon Jul 22, 2026
4c236d1
Merge pull request #644 from ligon/fix/323-ethiopia-config
ligon Jul 22, 2026
63f243a
test(#323): make the Guinea-Bissau test discriminate, and correct the…
Jul 22, 2026
827ce1d
docs(#603): population-statement survey -- all 111 waves, verbatim
ligon Jul 22, 2026
fe9f6c8
Merge branch 'development' into fix/323-site4-dfs-merge
ligon Jul 22, 2026
0bdf33d
fix(#645): to_parquet stops deleting real data via string-matched nulls
ligon Jul 22, 2026
2b66967
test(#645): pin the pyarrow refusal to ArrowTypeError/ArrowInvalid
ligon Jul 22, 2026
67ec061
test(#645): load requires_s3 by path — the by-name import fails in CI
ligon Jul 22, 2026
f270e53
fix(Nigeria): correct the assets KNOWN OPEN DEFECT note -- edit 1 is …
Jul 22, 2026
a689f88
Merge remote-tracking branch 'origin/development' into fix/323-malawi…
ligon Jul 22, 2026
4a57ebf
fix(#648): the documented conftest import does not work — use a marke…
Jul 22, 2026
b42ce5b
fix(#627 review): one reader for LSMS_GRAIN_STRICT; judge "required c…
ligon Jul 22, 2026
7af2ff0
docs(#627 review): correct four false claims in tracked text, and re-…
ligon Jul 22, 2026
b8b772f
Merge pull request #656 from ligon/fix/nigeria-assets-note-correction
ligon Jul 22, 2026
76c865c
Merge pull request #652 from ligon/docs/contents-org-standing-rule
ligon Jul 22, 2026
2b5cd33
Merge pull request #654 from ligon/docs/601-population-statements
ligon Jul 22, 2026
c99a57b
Merge pull request #623 from ligon/fix/323-guinea-bissau-config
ligon Jul 22, 2026
2bbadf8
Merge pull request #649 from ligon/fix/323-uganda-crop-condition
ligon Jul 22, 2026
ef4570f
Merge pull request #653 from ligon/fix/323-malawi-cartesian
ligon Jul 22, 2026
0fc0b8f
Merge pull request #629 from ligon/fix/323-assets-item-seq
ligon Jul 22, 2026
54fc561
Merge pull request #655 from ligon/fix/645-to-parquet-null-coercion
ligon Jul 22, 2026
212340b
Merge pull request #657 from ligon/fix/648-conftest-import-marker
ligon Jul 22, 2026
4a7df10
docs(#627 review): record the full-suite run — 2 failures, both pre-e…
ligon Jul 22, 2026
765f90e
docs(#601): record the sampling universe, exclusions and sub-samples …
ligon Jul 22, 2026
bcf3d5b
Merge pull request #627 from ligon/fix/323-site4-dfs-merge
ligon Jul 22, 2026
fb749f0
Merge pull request #658 from ligon/docs/601-universe-in-contents
ligon Jul 22, 2026
611ac9e
docs(release): v0.9.0 notes
Jul 22, 2026
faa3a94
Merge pull request #664 from ligon/docs/release-notes-v0.9.0
ligon Jul 22, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
4 changes: 2 additions & 2 deletions .claude/skills/add-feature/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -423,8 +423,8 @@ The corresponding `data_scheme.yml` entry:
- **Subdirectory structure:** Some waves use `Data/Cross_Sectional/` and `Data/Panel/` subdirectories; others have files directly in `Data/`.
- **Multi-round files:** A single `.dta` containing multiple survey rounds (e.g., Tanzania 2008-15) cannot be handled by `data_info.yml` alone — use a `.py` script.
- **Missing columns across waves:** Earlier survey instruments may not include all variables. Columns absent from a wave's `data_info.yml` entry will be NaN in the output — this is expected.
- **DVC-tracked files:** Pull data with `dvc pull {path}.dvc` before building. Run from the DVC root (`lsms_library/countries/`).
- **DVC lock contention:** If `dvc pull` hangs with `Unable to acquire lock`, clear stale locks: `rm -f lsms_library/countries/.dvc/tmp/*.lock lsms_library/countries/.dvc/tmp/rwlock`, then retry. For parallel work across agents, use git worktrees so each has its own DVC lock.
- **DVC-tracked files:** You do **not** pre-pull anything. `get_dataframe('../Data/file.dta')` materializes the blob on first read, lock-free and directly from S3. For a non-tabular file (questionnaire PDF, Excel codebook) use `data_access.get_data_file('{Country}/{wave}/Documentation/foo.pdf')`, which returns a local `Path`. **Never invoke the `dvc` CLI** — see `CLAUDE.md` §"Data Access".
- **DVC lock contention:** Should not arise, because the read path never takes `.dvc/tmp/lock`. If you are seeing it, something is shelling out to `dvc` — find and remove that call rather than working around it. **Do not `rm` the lock file**: it may belong to a live sibling process, and deleting it corrupts that writer. Worktrees do *not* give an agent its own DVC lock — the DVC repo is shared, which is exactly why the lock-free read path exists.
- **Pre-ISA vs ISA waves:** Earlier waves (e.g., Malawi 2004-05 "IHS2") predate the LSMS-ISA standardization and often use completely different module letters and variable naming conventions. Module L might be "non-food expenditures" in 2004-05 but "durable goods" in 2010+. Always verify via the World Bank data dictionary — never assume module letters are stable across survey instruments.
- **`convert_categoricals` — value-label decode (bit several features in 2026-06):** `get_dataframe()` (and the YAML path) decode Stata/SPSS value labels by **default** (`convert_categoricals=True`). This cuts both ways:
- In a **script** (`materialize: make`) that assumes *numeric codes* — e.g. `df['itemcode'].astype('Int64').map(CODE_MAP)` or `pd.to_numeric(df['have_flag'])` — the default makes the column come back as the **label string** (`'Watches'`, `'Yes'`), so `int(...)`/`to_numeric` fails or NaNs out (→ empty melt, `No objects to concatenate`). Pass `get_dataframe(path, convert_categoricals=False)` to keep numeric codes.
Expand Down
6 changes: 4 additions & 2 deletions .claude/skills/add-feature/food-acquired/units/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,10 @@ from the first source that has it:
`slurm_logs/build_ehcvm_unit_codebook.py` if a new sibling is added.

3. **The questionnaire / codebook.** Survey documentation (Excel `Unites`
sheets, IHPS "CODES FOR UNIT" PDF pages) lists the code→label map. Pull it
from `{Country}/{wave}/Documentation/` via DVC (`.venv/bin/dvc pull …`) and
sheets, IHPS "CODES FOR UNIT" PDF pages) lists the code→label map. Fetch it
with `data_access.get_data_file('{Country}/{wave}/Documentation/foo.pdf')`,
which returns a local `Path` (lock-free; **never** shell out to `dvc` — see
`CLAUDE.md` §"Data Access"), then
read with `pdftotext` or `pd.read_excel`. **Watch the scheme:** the
questionnaire's generic list may use *different numbering* than the data
(EHCVM's `Unites` sheet is 1–57; the data codes are 100–700 — they do not
Expand Down
83 changes: 83 additions & 0 deletions .claude/skills/add-feature/sample/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,89 @@ sample:
- t
```

Note `merge_on: i` — the **household** key, unique in both sub-frames. That is
what makes this merge a join.

### The `dfs:` merge contract (GH #323 site 4)

**`merge_on` must be unique in at least one sub-frame.** If both sub-frames are
finer-grained than the merge key, the merge is many-to-many and pandas emits a
*cartesian product within each key group* — it MANUFACTURES rows that exist in
no survey. The classic error is joining two **household**-grain frames on the
**cluster** key `v`: Ethiopia's `cluster_features` did exactly this and produced
65,508 rows in 2013-14 and 57,786 in 2015-16 from tables that have 433 and 432
clusters. The downstream `groupby().first()` collapse then tidied it away, so
the table looked perfectly clean.

`Wave._merge_subframes` now checks this exactly (a key value duplicated in
*both* sub-frames is the definition of the cartesian) and warns with the phantom
row count; `LSMS_GRAIN_STRICT` makes it fatal. That variable is read through the
same `_grain_strict()` predicate as sites 1–2, so all three sites agree on what
counts as "strict" (`1`/`true`/`yes`, case-insensitive) — and turning it on in
CI is gated on the whole of #323, not just this site. **Fix it by making the
merge correct — re-key to `i`, or reduce a sub-frame to the merge-key grain in a
wave script.** Never by aggregating the explosion away afterwards: core does not
aggregate (`SkunkWorks/grain_aggregation_policy.org`), and a reducer applied to
a cartesian only puts a signature on the corpse.

#### `merge_how:` (optional, default `outer`)

```yaml
merge_on:
- i
merge_how: left # default is `outer`
```

Declare `merge_how: left` when the **primary** sub-df (`dfs[0]`) is authoritative
for which rows exist and the others are strict enrichments. A geovariable file
that carries households the cover page does not is the usual case: under `outer`
those orphans arrive with a null value in every index level the cover owned.

**Be accurate about what this buys you.** Measured on Ethiopia 2013-14 (geo file
carries 25 households the cover page does not):

| | wave rows | null-`v` rows | delivered clusters | ΣLatitude | `District` dtype |
|---|---:|---:|---:|---:|---|
| `outer` | 5,287 | 25 | 433 | 4070.3702 | float64 |
| `left` | 5,262 | 0 | 433 | 4070.3702 | int8 |

The orphans do **not** "collapse into one phantom cluster" — the downstream
cluster-grain collapse *deletes* them, because `groupby` drops null keys. The
delivered values are identical. So `merge_how: left` is not a data fix; it is
worth declaring because it stops manufacturing null-keyed rows for site 1 to
delete, and because it stops the merge widening an integer column. It has a
cost: under `outer` the site-1 grain report *told* you 25 geo households had no
cover page. `left` drops them silently. If that signal matters for a wave, keep
`outer` and let site 1 report it.

#### A dropped sub-df that owned a required column is now a hard error

The GH #515 fallback drops a secondary sub-df that fails to load and proceeds
with a warning. That is right for a file that is *unavailable* (nothing you can
edit fixes it). It is **wrong** for a file that loaded fine but does not carry
the column your YAML names — a typo, a casing mismatch (`lat_dd_mod` vs
`LAT_DD_MOD`), a renamed variable. Ethiopia lost `Latitude`/`Longitude` from
three of five waves exactly that way, behind a warning nobody read. If such a
drop leaves a column declared **required** in `data_scheme.yml` entirely absent,
`grab_data` now raises. Presence is judged after `derived:`, `drop:` and the
wave's `df_edit` hook, so a column the wave module supplies is not reported
absent.

**Fix the column name.** In all ten cells this guard caught, the data existed:
Ethiopia and Nigeria were casing mismatches or a wrong key column, and Niger
2011-12's coordinates were in a *sibling* file in the same directory
(`NER_EA_Offsets.dta`, `LAT_DD_MOD`/`LON_DD_MOD`) while the file the YAML named
had none.

**Know what this guard is and is not.** It is *"a mis-named column in a `dfs:`
sub-df is fatal"* — not *"a required column is never absent"*. It fires only
when a sub-df was dropped, so a wave with no `dfs:` block can serve a declared
column 100% absent and never trip it (Niger **2014-15** does: `Latitude` is
declared `float`, 0 of 270 rows populated, no raise). And `optional: true` is a
blunt escape hatch — `data_scheme.yml` is **country**-grain, so it disarms the
column for *every* wave and every script-path build of that country while the
check is per-wave. Use it only where the country genuinely never has the column.

### Multi-round files (Tanzania 2008-15 pattern)

When a single `.dta` file contains multiple survey rounds with a `round` column, the YAML path cannot handle it --- use a Python script. The script reads the file, maps round numbers to wave labels, and splits panel vs refresh households for `panel_weight`:
Expand Down
85 changes: 77 additions & 8 deletions .claude/skills/add-wave/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,81 @@ from lsms_library.data_access import discover_waves
discover_waves("Ethiopia")
```

Returns a list of dicts annotated with `"local": True/False`. Waves marked `local=False` are on the WB but not yet in the repo.
Returns a list of dicts annotated with:

| key | meaning |
|----------------|-------------------------------------------------------------------------|
| `local` | `bool` — `True` only when a wave dir *records* this catalog id. |
| `local_status` | `"yes"` / `"no"` / `"unknown"` — the honest tri-state (see below). |
| `local_waves` | the wave directories backing this entry. |

Waves with `local_status == "no"` are on the WB but not yet in the repo.

**Matching is on the WB catalog id, not on the wave label.** Each wave dir
records the catalog entry it came from in `Documentation/SOURCE.org`
(`#+CATALOG_ID:` — see `lsms_library/provenance.py`). Label matching used to
be the mechanism and was wrong in both directions: two different surveys can
share a year range (Nigeria's GHS-Panel W4 **3557** and Living Standards
Survey **3827** both span 2018–2019), and one catalog entry can span two of
our wave dirs (Uganda **1001** covers both `2005-06/` and `2009-10/`).

`local_status == "unknown"` means a directory whose label matches exists but
has **no recorded WB catalog id** — so we cannot say whether it holds this
study or a different one with the same year range. Such rows carry
`local=False`: an unverified claim is treated as not-held, because wrongly
believing we hold a survey is the failure mode that hides missing data.

To (re)stamp provenance across the tree:

```sh
python scripts/backfill_wave_provenance.py --dry-run # report only
python scripts/backfill_wave_provenance.py # write SOURCE.org
```

Countries that are not WB datasets (`EthiopiaRHS`, `KenyaLPS`) are marked
`discoverable=False` in `_COUNTRY_CATALOG` and return `[]` with an explanatory
log — deliberately, rather than being silently absent.

#### Which repositories get searched (GH #597)

`discover_waves()` searches the collections listed in the country's
`repositories` field, **default `("lsms",)`**. The World Bank publishes whole
series outside `lsms` — Armenia's Integrated Living Conditions Survey (18
waves) is in `central`, South Africa's General Household Survey (21 waves) is
in `datafirst` — and a lsms-only search cannot see them *at all*.

**Adding a new country requires a one-time broad sweep** to learn which
repositories hold its series (`_wb_catalog_search(code, collection=None)`
searches everything). Curate the answer into config; do not infer it at
runtime.

> **Widening a country to a second repository REQUIRES an `idno_pattern` that
> pins the survey series.** Dropping the collection filter inflates results
> 30–400× (Findex, DHS, Afrobarometer, enterprise surveys; `datafirst` alone
> returns 320 South African rows — censuses, election studies, school
> registers). Worse, it resurfaces studies we *already hold* under a different
> catalog id in another repository: `central` 3016 (`MWI_2010_IHS-III_..._A_ML`)
> is the same Malawi IHS3 as `lsms` 1003, and `datafirst` 902 (`ZAF_1993_PSLSD`)
> is the same 1993 survey as `lsms` 297. Nothing in the catalog metadata links
> those pairs, so id-matching cannot catch them — only the series pin can.
> A missing-wave list nobody trusts is worse than no list.

```python
"Armenia": CountryCatalog("ARM", idno_pattern=r"_(HBS|ILCS)_",
repositories=("lsms", "central")),
"South Africa": CountryCatalog("ZAF", idno_pattern=r"_(IHS|GHS)_",
repositories=("lsms", "datafirst")),
"Liberia": CountryCatalog("LBR", idno_pattern=r"_(HIES|NHFS)_",
repositories=("lsms", "central")),
```

`idno_pattern` answers **"is this catalog row this country's?"** (identity), NOT
**"is this survey in remit?"**. The two diverge: Liberia's NHFS is in the
pattern because it is the catalog entry backing a wave dir we *hold*, but a
forest-resources survey is not in remit. Do not conflate them.

Note `GET /api/collections` returns HTTP 400 — collection ids cannot be
enumerated via the API. Read them off the `repositoryid` field of search rows.

### Step 2: Add the wave

Expand Down Expand Up @@ -125,14 +199,9 @@ DVC layers config files: `config.local` overrides `config`. This keeps the reade

## Batched DVC Operations

Always use batched `dvc add` + `dvc push` when adding multiple files. The `populate_and_push()` and `push_to_cache_batch()` functions do this automatically. On a cluster scratch filesystem, batched operations process 68 files in ~2.5 minutes vs ~90 minutes sequentially.
Publish blobs with `push_to_cache_batch()` (or `populate_and_push()` / `add_wave()`, which wrap it). These batch the underlying `dvc add` + `dvc push` for you: on a cluster scratch filesystem, batched operations process 68 files in ~2.5 minutes vs ~90 minutes sequentially. They also route through `_run_dvc_with_lock_retry()`, so concurrent writers queue on the global lock with backoff + jitter instead of failing.

If running DVC commands manually, follow CONTRIBUTING.org steps 8-9:
```bash
cd lsms_library/countries
dvc add Ethiopia/2021-22/Data/*.dta
dvc push
```
**Do not run `dvc add` / `dvc push` by hand**, and never `rm` a DVC lock file — see `CLAUDE.md` §"Data Access". Hand-rolled invocations take the global `.dvc/tmp/lock` with no retry, which is how a parallel sweep gets a wave half-pushed.

## Data Loading in Scripts

Expand Down
2 changes: 1 addition & 1 deletion .coder/coverage/blessed.csv
Original file line number Diff line number Diff line change
@@ -1 +1 @@
country,feature,wave
country,feature,wave,blessed_by,date,note
Loading
Loading