Skip to content

feat(coverage): the blocked tier — wanted, known, unobtainable (+ empty out "Countries Without Microdata")#598

Merged
ligon merged 4 commits into
developmentfrom
feat/coverage-blocked-tier
Jul 12, 2026
Merged

feat(coverage): the blocked tier — wanted, known, unobtainable (+ empty out "Countries Without Microdata")#598
ligon merged 4 commits into
developmentfrom
feat/coverage-blocked-tier

Conversation

@ligon

@ligon ligon commented Jul 12, 2026

Copy link
Copy Markdown
Owner

The state between "obtainable" and "does not exist": the data exists, we know where it is, and we cannot get it because the acquisition channel is broken.

  • Nepal (3 waves) — NSO Nepal hosts the microdata, not the WB. Their site is unreachable and no human contact has been reached.
  • Albania 1996 — directory and WB catalog entry (2311, ALB_1996_EWS) both exist; zero data files.

I had this wrong two commits ago, and the mistake is the point

In #593 I graded these n/a, under "Countries Without Microdata". But n/a means "no readiness applies" — it reads as nothing to see here, and it buried a gap we actively want.

That is the exact lumping error this module exists to prevent, committed by its author, inside the fix for a different lumping error. Worth stating plainly rather than quietly correcting.

blocked is visible (it counts against us) but not actionable by us — nobody closes it by trying harder, and an RA who picks it up will spend a day rediscovering that the NSO site is down. It sorts below the real defects and above the settled tiers.

The load-bearing part is the probe, not the label

A blocker is a condition that changes. A broken website heals; a dead contact gets replaced.

"We hope we'll find it someday" is not a plan. A scheduled recheck is.

So load_blocked() refuses a row with:

  • no probe URL — it could never be rechecked, and would silently become the permanent close it explicitly is not;
  • no blocker — an unexplained block is indistinguishable from neglect.

bin/coverage_refresh.sbatch already self-resubmits, is connectivity-gated, and already opens GitHub issues. It re-probes them. No new loop.

Two further bugs this flushed out

Blockedness is a property of the SOURCE, not of a build. I first wired it only into the build-failure branch, so make matrix-coverage reported blocked=0 while four sources were plainly blocked. We do not need to try and fail to build Nepal to know NSO's site is down. Hoisted — it now applies to the cheap coverage-only layer (no builds, no auth).

A blocked source we don't MODEL as a wave still has to appear. Albania/1996 has a directory, Documentation/, and a catalog id — but no _/ and no Data/, so Country.waves never enumerates it and every per-wave loop skipped it in silence. The blocked list is authoritative about sources we want; it does not defer to whether the model happens to know about them. Otherwise the denominator quietly omits exactly the waves we most want and cannot get.

"Countries Without Microdata" is now EMPTY — and that is the finding

Both entries were misclassified live gaps, each hidden behind a label reading "nothing to see here":

was actually
Nepal — "no data" blocked. Data exists; NSO's site is down.
Armenia — "no data" Not missing at all.

discover_waves() defaults to collection='lsms'. Armenia's 18 waves of Integrated Living Conditions Survey (2001–2018) sit under repositoryid='central'invisible to our discovery, not absent from the world. Plausibly the largest actionable acquisition gap in the repo. Filed as #597, which also covers South Africa's General Household Survey 2015–2025 (repositoryid='datafirst', 10 waves).

#597 matters beyond those two countries. We are teaching the matrix to report an honest denominator of waves the WB publishes that we lack (#593, #595) — and that fix is incomplete while discover_waves itself can only see one collection. We would have shipped a denominator that is confidently wrong: "no missing waves" for Armenia, while 18 sit in the catalog.

The ruler was miscalibrated. So was the ruler's own instrument.

Tests

10 new (blocked beats n/a; probe/blocker refusal; un-modelled wave still appears; a genuine build failure still reports broken). 40 passing in test_coverage_matrix.py.

🤖 Generated with Claude Code

https://claude.ai/code/session_01NtARb9se2un1DLFJrrpjHe

ligon and others added 2 commits July 12, 2026 09:33
The state between "obtainable" and "does not exist": the data EXISTS, we KNOW
where it is, and we CANNOT get it because the acquisition channel is broken.

  Nepal (3 waves)  NSO Nepal hosts the microdata, not the WB.  Their site is
                   unreachable and no human contact has been reached.
  Albania 1996     directory + WB catalog entry (2311, ALB_1996_EWS) both exist;
                   zero data files.

## I had this wrong, and the mistake was instructive

Two commits ago I graded these `n/a` under "Countries Without Microdata"
(Defect 6).  But `n/a` means "no readiness applies" -- it reads as *nothing to
see here*, and it BURIED a gap we actively want.  That is the exact lumping
error this module exists to prevent, committed by its author, in the fix for a
different lumping error.

`blocked` is *visible* (it counts against us) but *not actionable by us* --
nobody closes it by trying harder, and an RA who picks it up will waste a day
rediscovering that the NSO site is down.  It sorts below the real defects and
above the settled tiers.

## The load-bearing part is the probe, not the label

A blocker is a CONDITION THAT CHANGES.  A broken website heals; a dead contact
gets replaced.  **"We hope we'll find it someday" is not a plan; a scheduled
recheck is.**  So `load_blocked()` REFUSES a row with no `probe` URL (it could
never be rechecked, and would silently become the permanent close it explicitly
is not) or no `blocker` (an unexplained block is indistinguishable from
neglect).  `bin/coverage_refresh.sbatch` -- which already self-resubmits, is
connectivity-gated, and already opens GitHub issues -- re-probes them.

## Two further bugs this flushed out

**Blockedness is a property of the SOURCE, not of a build.**  I first wired it
only into the build-failure branch, so `make matrix-coverage` reported
`blocked=0` while four sources were plainly blocked.  We do not need to try and
fail to build Nepal to know NSO's site is down.  Hoisted; it now applies to the
cheap coverage-only layer.

**A blocked source we do not MODEL as a wave still has to appear.**
`Albania/1996` has a directory, Documentation, and a catalog id -- but no `_/`
and no `Data/`, so `Country.waves` never enumerates it and every per-wave loop
skipped it in silence.  The blocked list is AUTHORITATIVE about sources we want;
it does not defer to whether the model happens to know about them.  Otherwise
the denominator quietly omits exactly the waves we most want and cannot get.

## "Countries Without Microdata" is now EMPTY, and that is the point

Both of its entries were misclassified live gaps, each hidden behind a label
that reads "nothing to see here":

  Nepal   -> `blocked` (above).
  Armenia -> NOT MISSING AT ALL.  `discover_waves()` defaults to
             collection='lsms', but Armenia's **18 waves of Integrated Living
             Conditions Survey (2001-2018)** sit under repositoryid='central' --
             invisible to our discovery, not absent from the world.  Plausibly
             the largest actionable acquisition gap in the repo.  Filed as
             **GH #597**, which also covers South Africa's General Household
             Survey 2015-2025 (repositoryid='datafirst', 10 waves).

#597 matters beyond those two countries: we are teaching the matrix to report an
honest denominator of waves the WB publishes that we lack -- and that fix is
INCOMPLETE while `discover_waves` itself can only see one collection.  We would
have built a denominator that is confidently wrong.

The ruler was miscalibrated.  So was the ruler's own instrument.

Tests: 10 new (blocked beats n/a; probe/blocker refusal; un-modelled wave still
appears; a genuine build failure is still `broken`).  49 passing in
test_coverage_matrix.py.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Phase 0 was meant to make the denominator honest.  It did -- and the honest
denominator is far larger than the matrix ever suggested, because the instrument
could not see most of it.

Armenia (18 ILCS waves, repo=central) and South Africa (10 GHS waves,
repo=datafirst) are both IN REMIT (Ethan, 2026-07-12) and both structurally
invisible to discover_waves(collection='lsms').

The queue as now known: ~43 waves to ACQUIRE, ~35 already-downloaded waves to
CONFIGURE, on top of the 456 absent cells.  None of this is new work appearing.
It is work that was always there and could not be counted.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ligon and others added 2 commits July 12, 2026 09:57
…ources

Backlog reconciliation, and a prior-art miss I should own: **GH #552 (June 2026)
already audited WB coverage gaps "tiered by obtainability"** — Tier 1
(WB-direct) / Tier 2 (NSO-redirect) / Tier 3 (licensed, fee-gated) — and even
cites the Nepal->NSO precedent.  I built the `blocked` tier without reading it.
CLAUDE.md tells me to run the prior-art-ledger skill at the start of a
non-trivial task; I skipped it.

The reconciliation was productive in BOTH directions.

## What #552 gives us: `blocker_kind`

#552's tiering is the right axis, and it belongs in the machinery as STRUCTURE
rather than prose, because the kinds route to completely different people:

  nso-redirect  the WB catalogues it but redirects to a national statistics
                office whose channel is broken.  -> chase a human.
  fee-gated     obtainable, for money and permission (Vietnam GSO: USD 200-2000
                + consent).  -> a BUDGET decision, not an engineering one.
                NEVER queue this for an RA.
  unconfirmed   we believe it is blocked but have not proven it; it may be a
                plain obtainable TODO (Albania 1996).  The honest state when you
                have not actually checked.

"The site is down" and "it costs $2,000" are not the same blocker.  Flattening
them is how a money question silently becomes an engineering backlog item that
nobody can ever close.

blocked_sources.csv grows from 4 rows to 8, absorbing #552's Tier 2/3:
Jamaica (JSLC, STATIN redirect), Ecuador (ECV 1995/1998, INEC redirect), and
Vietnam (VLSS/VHLSS x5, GSO, fee-gated).  They are now git-tracked, probed, and
re-checked — not a snapshot in an issue that ages quietly.

## What we give #552: two of its claims are stale, one is wrong in fact

- **"Kyrgyz Republic — wholly-missing"**: NOT missing.  4 waves, 146 DVC
  sidecars (~378 MB) are already downloaded; only the CONFIG is absent.  It is
  one of the 10 `unconfigured` countries (#593).  An add-feature job, not an
  add-wave one — a materially cheaper class of work than #552 assumes.
- **"South Africa: no later WB-LSMS waves"**: true within `collection='lsms'`,
  FALSE in fact.  The General Household Survey 2015-2025 sits under
  `repositoryid='datafirst'`.  Same blind spot hides Armenia's 18 ILCS waves
  under `repositoryid='central'`.  Both now in remit; see GH #597.

  #552's conclusions are correct GIVEN ITS INSTRUMENT, and its instrument could
  only see one collection.  *A negative result from an uncalibrated instrument
  is a result about the instrument.*  That is the third time that exact lesson
  has landed in this series -- twice on me.

Also closed as verified-fixed during the sweep: #571 (Guatemala), #572 (Serbia),
#573 (Panama — its data_scheme.yml landed today in a0ba3e1), #574 (GhanaSPS
food_acquired; its sibling #579 stays open — GhanaSPS.sample() still raises).
And corrected #587, whose stated hypothesis for its largest bloc (Nigeria's 22
`dropped` cells as "false positives") is refuted: the built `t` values are valid
wave labels from the OPPOSITE PP/PH round.  They are real half-missing features,
not a reporting artefact.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…g the bookkeeping

Organizing principle: **the GitHub backlog should hold exactly what the coverage
matrix cannot.**  Any issue that IS a cell is duplicate bookkeeping -- the
Phase-1 sweep re-derives it, sized and sourced, whether or not an issue exists.
Maintaining both is how a backlog becomes a museum.

  the matrix owns   WHICH cells are missing / broken / blocked  (the WHAT)
  the backlog owns  WHY a cell is hard, cross-cutting bugs, and everything the
                    matrix CANNOT SEE

That last clause is not a throwaway.  The grader has known blind spots, and they
are exactly where issues earn their keep: #588 (Uganda earnings is sane COLD and
broken WARM -- the matrix is a cold build and structurally cannot see it) and
#589 (currency labels -- it does not check them).

Track what the instrument cannot see.  Let the instrument track the rest.

## Severity rule: order by HARM, not effort or age

  1 silently wrong    the user gets a wrong number and does not know.  The only
                      UNRECOVERABLE class -- it propagates into published work.
  2 silently missing  the user gets less data than they think.
  3 loudly broken     it raises.  Annoying, harmless.  A crash is a gift.
  4 untidy            noise, dead code, ergonomics.

## Top of the board: #503 (Guyana), and it is not close

`i = [ED, HH]` CONFLATES DISTINCT HOUSEHOLDS.  The source is at (ED, HH, SN)
granularity: COVERN.dta has 1502 unique (ED,HH) vs 1807 unique (ED,HH,SN).  So
roughly 300 real households are silently merged into others.

That is not missing data.  It is WRONG DATA THAT LOOKS FINE -- it builds clean
and passes every sanity check.  Every Guyana number we have produced is suspect,
and nothing in the matrix flags it.  The purest instance of the session's theme:
`sane` is not `blessed`.

## And #537 is that theme INVERTED

`validate_acquisition_source` has ZERO call sites, while its docstring asserts it
is wired into `_finalize_result`.  A validation we believe is running, and is
not: an unevidenced POSITIVE.  Same disease, opposite sign.  A guarantee that
does not execute is worse than no guarantee, because it is trusted.

## Also

- Wave 4 is DECISIONS, not tickets (#575 purchased-value-only; #226; #279;
  #438/#439; Vietnam's USD 200-2000 fee gate).  An RA cannot close these by
  working harder, and leaving a judgement in an engineering queue is how it
  silently becomes a default.
- A large fraction of the ~29 open issues are not defects at all -- they are
  cells, decisions, or acquisitions filed as issues because there was nowhere
  else to put them.  Now there is.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@ligon
ligon force-pushed the feat/coverage-blocked-tier branch from 9e7f955 to 7f80b67 Compare July 12, 2026 17:07
@ligon
ligon merged commit 006cdb3 into development Jul 12, 2026
10 checks passed
@ligon
ligon deleted the feat/coverage-blocked-tier branch July 12, 2026 17:52
ligon added a commit that referenced this pull request Jul 12, 2026
…time

Ethan's rulings on #597: South Africa takes the GHS **and** the IES + LCS
(28 waves).  Each series is now acquired for a STATED reason:

  GHS (21)  roster, housing, education, employment.  NO consumption module --
            it cannot populate `food_acquired`.
  IES (5)   income + expenditure + acquisition diary.
  LCS (2)   living conditions + expenditure diary.
            IES and LCS are what feed the demand-estimation path.

So the ZAF pin becomes r"_(IHS|GHS|IES|LCS)_" -- 29 rows, zero over-match,
against the 320 ZAF rows `datafirst` actually returns.

CAPABILITY PRE-POPULATES `not_asked`
------------------------------------
When SA-GHS lands, `South Africa/food_acquired/2015` grades `absent`, someone
files it as a gap, and a probe (label sweep + sibling differential +
questionnaire) eventually rediscovers a fact the WB catalog told us the day we
acquired it.  That is archaeology to recover knowledge we already had.

`lsms_library/capability.py` records what a *series* measures when it is
acquired.  A `SeriesCapability` names the features it `provides` and `lacks`;
`proposed_absent_verdicts()` turns each `lacks` entry into rows for
`.coder/coverage/absent_verdicts.csv` in the schema `load_verdicts()` already
reads.  The probe sweep then only adjudicates cells inherited WITHOUT a
capability record -- a shrinking set, not a permanent tax.

AND IT MAY NOT CLOSE A CELL ON A CATALOG BLURB
----------------------------------------------
This is the whole discipline.  A capability asserted from a cataloguer's topic
list is NOT C4.  Closing a cell on it would be the Albania mistake with better
paperwork -- an unevidenced negative is unfalsifiable, and therefore permanent
whether or not it is true.

So each record carries a `validation` level, and the verdict is DERIVED from it
rather than chosen by the caller:

  catalog-only             WB topics / abstract        -> `unsure`     (no close)
  data-validated           C1 label sweep negative     -> `unsure`     (no close)
  questionnaire-validated  C4: the questionnaire read  -> `not-asked`  (closes)

`data-validated` deliberately does not close either: a negative label sweep is
exactly as consistent with `asked-not-distributed` as with `not-asked`.  That is
precisely why C4 is mandatory.

Every record shipped here is `catalog-only`, so every one proposes `unsure`.
The 21 GHS `food_acquired` rows it would emit are all `unsure`, `checks_run`
empty, evidence prefixed `[catalog-only; C4 NOT run]`.  Upgrading a series to
`questionnaire-validated` -- one RA, one PDF -- is what converts them into a
permanent `not-asked`: a bounded human step per SERIES, not per cell.

`capability.audit()` enforces the invariant; `tests/test_capability.py` proves a
catalog-only record cannot close a cell *through the real `load_verdicts()`*,
and that the questionnaire upgrade does.

No verdict rows are written -- SA is not acquired.  The mechanism is proven, the
shape is up for review.

Rebased onto development: #596 resolved the Nepal/Niger/Senegal/Nigeria
provenance gaps I had reported, and #598's `blocked` tier supersedes my
data_info.yml edit (its emptied "Countries Without Microdata" map is taken
wholesale).  Census: 79 rows, 54 missing in-remit waves.

Refs #597.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NtARb9se2un1DLFJrrpjHe
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant