diff --git a/src/populace_dynamics/data/sipp_jobs.py b/src/populace_dynamics/data/sipp_jobs.py index 84076423..c722c9e5 100644 --- a/src/populace_dynamics/data/sipp_jobs.py +++ b/src/populace_dynamics/data/sipp_jobs.py @@ -379,12 +379,24 @@ def read_sipp_job_months( active & ~no_establishment, _EMPSIZE_CODES, ) - bad_size = raw[f"EJB{n}_EMPSIZE"][ + # A garbage *string* on a no-establishment slot would coerce + # to NaN and masquerade as the structural blank, so the raw + # cell is checked too: non-blank raw + NaN coercion refuses, + # keeping the loud-refusal posture symmetric with employer + # slots (review note on #206). + raw_size = raw[f"EJB{n}_EMPSIZE"] + raw_blank = raw_size.isna() | (raw_size.astype(str).str.strip() == "") + bad_size = raw_size[ active & no_establishment - & empsize.notna() - & ~empsize.isin((_MISSING, _MISSING_ID)) - & ~empsize.isin(sorted(_EMPSIZE_CODES)) + & ( + ( + empsize.notna() + & ~empsize.isin((_MISSING, _MISSING_ID)) + & ~empsize.isin(sorted(_EMPSIZE_CODES)) + ) + | (empsize.isna() & ~raw_blank) + ) ] if len(bad_size): raise _domain_error(year, f"EJB{n}_EMPSIZE", bad_size) diff --git a/tests/data/test_sipp_jobs.py b/tests/data/test_sipp_jobs.py index 0ed205e3..091bad1b 100644 --- a/tests/data/test_sipp_jobs.py +++ b/tests/data/test_sipp_jobs.py @@ -200,6 +200,26 @@ def test_corrupt_empsize_on_self_employment_refuses(self, tmp_path): with pytest.raises(ValueError, match="EJB1_EMPSIZE"): sipp_jobs.read_sipp_job_months(2023, path=path) + def test_garbage_string_empsize_on_se_slot_refuses(self, tmp_path): + # A non-numeric string coerces to NaN like a structural + # blank; the raw-cell guard keeps it a loud refusal on + # no-establishment slots too (post-merge review note). + # JBORSE 2 and 3 take the same no-establishment code path. + for jborse in (2, 3): + months = [ + { + "month": 1, + "job1": { + "JBORSE": jborse, + "CLWRK": 8, + "EMPSIZE": "GARBAGE", + }, + } + ] + path = _write_pu_file(tmp_path / f"j{jborse}", 2023, months) + with pytest.raises(ValueError, match="EJB1_EMPSIZE"): + sipp_jobs.read_sipp_job_months(2023, path=path) + def test_corrupt_code_on_active_slot_refuses(self, tmp_path): # Only the -9/-999 sentinels excuse a non-domain value on an # active slot; an unparseable string or a blank must refuse diff --git a/tests/tier_counts.json b/tests/tier_counts.json index 01e321b2..9d39ce3d 100644 --- a/tests/tier_counts.json +++ b/tests/tier_counts.json @@ -1,7 +1,7 @@ { "schema_version": 1, "counts": { - "unit": 650, + "unit": 651, "artifact": 1020, "integration_psid": 802, "reproduction_legacy": 520,