From 71b24cfd8318dd0551c94b9f60212d390bc8485c Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 29 Aug 2026 19:30:19 -0400 Subject: [PATCH 1/8] Add NZ and UK source storage conventions (#175) Route new raw and derived artifacts through country-aware publisher prefixes, refuse stale manifest keys, and record NZ period/geography rulings. Preserve legacy US paths and leave UK object copying and manifest migration to a separate credentialed change. --- chronicle/artifacts.py | 176 ++++++++++++++++++++++++++++-- chronicle/core.py | 8 +- chronicle/harness.py | 27 +++-- docs/pe-nz-source-checklist.md | 67 ++++++++++++ docs/storage-architecture.md | 30 +++-- tests/test_chronicle_artifacts.py | 121 ++++++++++++++++++++ 6 files changed, 396 insertions(+), 33 deletions(-) create mode 100644 docs/pe-nz-source-checklist.md diff --git a/chronicle/artifacts.py b/chronicle/artifacts.py index 139663c5..c2b944fa 100644 --- a/chronicle/artifacts.py +++ b/chronicle/artifacts.py @@ -24,6 +24,31 @@ DEFAULT_R2_PREFIX = "raw" DEFAULT_R2_DERIVED_PREFIX = "derived" +# New UK and New Zealand uploads are namespaced by country. US objects predate +# the country segment and deliberately keep their legacy ``raw/{source_id}`` +# and ``derived/{source_id}`` shapes. Publisher directories are the stable +# routing input because they are shared by packages/ and db/data/. +R2_COUNTRY_PUBLISHERS = { + "nz": frozenset({"ird", "mbie", "msd", "stats_nz"}), + "uk": frozenset( + { + "dft", + "dwp", + "hmrc", + "isc", + "mhclg", + "nisra", + "nrs", + "obr", + "ons", + "scotgov", + "slc", + "voa", + "welshgov", + } + ), +} + @dataclass(frozen=True) class ArtifactStorageLocation: @@ -366,7 +391,7 @@ def fetch_source_artifact( filename: str | None = None, upload_r2: bool = False, r2_bucket: str = DEFAULT_R2_RAW_BUCKET, - r2_prefix: str = DEFAULT_R2_PREFIX, + r2_prefix: str | None = None, wrangler_command: str = "npx wrangler", ) -> ArtifactFetchReport: """Fetch/register a source artifact and optionally upload it to R2.""" @@ -385,6 +410,12 @@ def fetch_source_artifact( size_bytes = len(content) manifest_path = output / "manifest.yaml" + resolved_r2_prefix = resolve_r2_prefix( + prefix=r2_prefix, + default_prefix=DEFAULT_R2_PREFIX, + source_id=source_id, + package_path=output, + ) r2_location = ArtifactStorageLocation( provider="r2", bucket=r2_bucket, @@ -394,7 +425,7 @@ def fetch_source_artifact( year=year, sha256=sha256, filename=artifact_filename, - prefix=r2_prefix, + prefix=resolved_r2_prefix, ), ) r2_upload = None @@ -449,7 +480,7 @@ def publish_derived_artifacts( year: int, build_id: str | None = None, r2_bucket: str = DEFAULT_R2_DERIVED_BUCKET, - r2_prefix: str = DEFAULT_R2_DERIVED_PREFIX, + r2_prefix: str | None = None, wrangler_command: str = "npx wrangler", build_artifacts_output: str | Path | None = None, ) -> DerivedArtifactPublishReport: @@ -497,6 +528,11 @@ def publish_derived_artifacts( errors=("missing_build_id",), ) + resolved_r2_prefix = resolve_r2_prefix( + prefix=r2_prefix, + default_prefix=DEFAULT_R2_DERIVED_PREFIX, + source_id=source_id, + ) entries: list[DerivedArtifactUploadEntry] = [] errors: list[str] = [] artifact_paths = sorted(path for path in input_path.rglob("*") if path.is_file()) @@ -515,7 +551,7 @@ def publish_derived_artifacts( year=year, build_id=resolved_build_id, artifact_name=relative_path, - prefix=r2_prefix, + prefix=resolved_r2_prefix, ), ) upload = _upload_r2_object( @@ -560,7 +596,7 @@ def publish_source_artifacts( source_id: str | None = None, package_id: str | None = None, r2_bucket: str = DEFAULT_R2_RAW_BUCKET, - r2_prefix: str = DEFAULT_R2_PREFIX, + r2_prefix: str | None = None, wrangler_command: str = "npx wrangler", ) -> RawArtifactPublishReport: """Upload manifest-declared raw source artifacts and record R2 locations.""" @@ -594,6 +630,17 @@ def publish_source_artifacts( errors.append(f"Manifest files must be a mapping: {manifest_path}") continue + try: + resolved_r2_prefix = resolve_r2_prefix( + prefix=r2_prefix, + default_prefix=DEFAULT_R2_PREFIX, + source_id=str(manifest_source_id), + package_path=manifest_path, + ) + except ValueError as exc: + errors.append(f"Could not resolve R2 prefix for {manifest_path}: {exc}") + continue + updated = False for year, spec in files.items(): entry, updated_spec = _publish_raw_manifest_entry( @@ -603,7 +650,7 @@ def publish_source_artifacts( year, spec, r2_bucket=r2_bucket, - r2_prefix=r2_prefix, + r2_prefix=resolved_r2_prefix, wrangler_command=wrangler_command, ) entries.append(entry) @@ -764,6 +811,81 @@ def bootstrap_r2_buckets( ) +def infer_r2_country( + *, + source_id: str | None = None, + package_path: str | Path | None = None, +) -> str | None: + """Infer an R2 country segment from a package publisher directory. + + ``package_path`` is authoritative when supplied. ``source_id`` is a + fallback for low-level callers that do not have a package path, including + derived-artifact publication. + """ + path_countries: set[str] = set() + if package_path is not None: + path_parts = {part.lower() for part in Path(package_path).parts} + path_countries = { + country + for country, publishers in R2_COUNTRY_PUBLISHERS.items() + if path_parts.intersection(publishers) + } + if len(path_countries) > 1: + raise ValueError( + "package path contains publishers from multiple countries: " + f"{package_path}" + ) + + source_countries: set[str] = set() + normalized_source_id = (source_id or "").lower() + if normalized_source_id: + source_countries = { + country + for country, publishers in R2_COUNTRY_PUBLISHERS.items() + if any( + normalized_source_id == publisher + or normalized_source_id.startswith(f"{publisher}_") + or normalized_source_id.startswith(f"{publisher}-") + for publisher in publishers + ) + } + if len(source_countries) > 1: + raise ValueError(f"source_id maps to multiple R2 countries: {source_id}") + + if path_countries and source_countries and path_countries != source_countries: + raise ValueError( + "package publisher directory and source_id map to different " + f"countries: path={sorted(path_countries)}, " + f"source_id={sorted(source_countries)}" + ) + countries = path_countries or source_countries + return next(iter(countries)) if countries else None + + +def resolve_r2_prefix( + *, + prefix: str | None, + default_prefix: str, + source_id: str | None = None, + package_path: str | Path | None = None, +) -> str: + """Return the country-aware R2 prefix for one source package.""" + resolved = _clean_key_part(prefix or default_prefix) + country = infer_r2_country(source_id=source_id, package_path=package_path) + if country is None: + return resolved + + suffix = resolved.rsplit("/", maxsplit=1)[-1] + if suffix in R2_COUNTRY_PUBLISHERS: + if suffix != country: + raise ValueError( + f"R2 prefix country {suffix!r} disagrees with publisher " + f"country {country!r}" + ) + return resolved + return posixpath.join(resolved, country) + + def build_r2_key( *, source_id: str, @@ -771,15 +893,20 @@ def build_r2_key( year: int | str, sha256: str, filename: str, - prefix: str = DEFAULT_R2_PREFIX, + prefix: str | None = None, ) -> str: """Build the canonical immutable R2 key for a raw source artifact. ``year`` is usually a calendar year but may be a label such as ``source_capture`` for non-year manifest file entries. """ + resolved_prefix = resolve_r2_prefix( + prefix=prefix, + default_prefix=DEFAULT_R2_PREFIX, + source_id=source_id, + ) return posixpath.join( - _clean_key_part(prefix), + resolved_prefix, _clean_key_part(source_id), _clean_key_part(package_id), str(year), @@ -795,11 +922,16 @@ def build_derived_r2_key( year: int, build_id: str, artifact_name: str, - prefix: str = DEFAULT_R2_DERIVED_PREFIX, + prefix: str | None = None, ) -> str: """Build the canonical R2 key for a derived build artifact.""" + resolved_prefix = resolve_r2_prefix( + prefix=prefix, + default_prefix=DEFAULT_R2_DERIVED_PREFIX, + source_id=source_id, + ) return posixpath.join( - _clean_key_part(prefix), + resolved_prefix, _clean_key_part(source_id), _clean_key_part(package_id), str(year), @@ -1002,6 +1134,30 @@ def _publish_raw_manifest_entry( prefix=r2_prefix, ), ) + storage = spec.get("storage") if isinstance(spec.get("storage"), dict) else {} + recorded_r2 = storage.get("r2") if isinstance(storage.get("r2"), dict) else {} + recorded_key = recorded_r2.get("key") + if recorded_key and recorded_key != location.key: + errors.append( + "recorded_r2_key_disagrees_with_country_prefix:" + f"recorded={recorded_key}:expected={location.key}" + ) + return ( + RawArtifactPublishEntry( + manifest_path=str(manifest_path), + source_id=source_id, + package_id=package_id, + year=str(year), + filename=filename, + local_path=str(artifact_path), + sha256=sha256_actual, + size_bytes=size_bytes, + r2_location=None, + upload=None, + errors=tuple(errors), + ), + None, + ) upload = _upload_r2_object( location, artifact_path, diff --git a/chronicle/core.py b/chronicle/core.py index c43f7574..16dcc3cf 100644 --- a/chronicle/core.py +++ b/chronicle/core.py @@ -21,8 +21,12 @@ # (packages/hmrc/vat_firm_targets_2024_25) and academic year AY 2024/25 is # academic_year 2024 (#131). Publishers that label a fiscal year with a single # year keep that label year (US federal FY2024 -> 2024, -# packages/cbo/individual_income_tax_receipts_2026_02). Caution: the EES -# helper _academic_year_end (chronicle/sources/rows.py) names value COLUMNS +# packages/cbo/individual_income_tax_receipts_2026_02). +# New Zealand labels its April-March income tax year by the ending year, so the +# publisher's "2024 tax year" is tax_year 2024 with PeriodCoverage 2023-04-01 +# through 2024-03-31 and the publisher label recorded in source_period_label. +# Caution: the EES helper _academic_year_end (chronicle/sources/rows.py) names +# value COLUMNS # by the academic year's END year ("2024/25" -> 2025); that is source-layout # naming only and must not leak into fact periods. ALLOWED_PERIOD_TYPES = { diff --git a/chronicle/harness.py b/chronicle/harness.py index e45f3604..1d96f093 100644 --- a/chronicle/harness.py +++ b/chronicle/harness.py @@ -336,7 +336,7 @@ def fetch_artifact_file( filename: str | None = None, upload_r2: bool = False, r2_bucket: str = "ledger-raw", - r2_prefix: str = "raw", + r2_prefix: str | None = None, wrangler_command: str = "npx wrangler", ) -> ArtifactFetchReport: """Fetch/register a raw source artifact and optionally upload it to R2.""" @@ -373,7 +373,7 @@ def publish_raw_artifact_files( source_id: str | None = None, package_id: str | None = None, r2_bucket: str = "ledger-raw", - r2_prefix: str = "raw", + r2_prefix: str | None = None, wrangler_command: str = "npx wrangler", ) -> RawArtifactPublishReport: """Publish manifest-declared raw source artifacts to R2.""" @@ -410,7 +410,7 @@ def publish_derived_artifact_files( year: int, build_id: str | None = None, r2_bucket: str = "ledger-derived", - r2_prefix: str = "derived", + r2_prefix: str | None = None, wrangler_command: str = "npx wrangler", build_artifacts_output: str | Path | None = None, ) -> DerivedArtifactPublishReport: @@ -893,8 +893,11 @@ def main(argv: list[str] | None = None) -> int: ) artifact_parser.add_argument( "--r2-prefix", - default="raw", - help="R2 key prefix for raw artifacts.", + help=( + "R2 key-prefix root for raw artifacts. Defaults to country-aware " + "raw/nz or raw/uk for known publisher directories, and raw for " + "legacy US packages." + ), ) artifact_parser.add_argument( "--wrangler-command", @@ -948,8 +951,11 @@ def main(argv: list[str] | None = None) -> int: ) raw_publish_parser.add_argument( "--r2-prefix", - default="raw", - help="R2 key prefix for raw artifacts.", + help=( + "R2 key-prefix root for raw artifacts. Defaults to country-aware " + "raw/nz or raw/uk for known publisher directories, and raw for " + "legacy US packages." + ), ) raw_publish_parser.add_argument( "--wrangler-command", @@ -1014,8 +1020,11 @@ def main(argv: list[str] | None = None) -> int: ) derived_publish_parser.add_argument( "--r2-prefix", - default="derived", - help="R2 key prefix for derived build artifacts.", + help=( + "R2 key-prefix root for derived artifacts. Defaults to " + "country-aware derived/nz or derived/uk for known publishers, " + "and derived for legacy US packages." + ), ) derived_publish_parser.add_argument( "--wrangler-command", diff --git a/docs/pe-nz-source-checklist.md b/docs/pe-nz-source-checklist.md new file mode 100644 index 00000000..ab98924f --- /dev/null +++ b/docs/pe-nz-source-checklist.md @@ -0,0 +1,67 @@ +# PolicyEngine New Zealand source checklist + +This checklist records Chronicle's New Zealand source-ingestion decisions. The +canonical wave-1 inventory and acceptance criteria live in +[issue #176](https://github.com/PolicyEngine/chronicle/issues/176); this file is +the repository execution ledger for that issue, not a separate target design. +Chronicle stores publisher-backed facts only. Microcosm owns target selection, +reconciliation, aging, and activation. + +## Period convention + +IRD labels New Zealand's April-March income tax year by its ending year. +Publisher label "2024 tax year" therefore becomes `tax_year: 2024`, with every +NZ tax-year record set also carrying: + +```yaml +period_coverage: + start_date: 2023-04-01 + end_date: 2024-03-31 + basis: tax + source_period_label: 2024 tax year +``` + +This differs from Chronicle's opening-year treatment of split UK labels such as +FY2024-25. + +## Geography and aggregation rulings + +These decisions implement the rulings recorded in +[issue #175](https://github.com/PolicyEngine/chronicle/issues/175): + +- Territorial authorities use `geography_level: local_authority` and + `geography_vintage: ta_2025`. +- MSD Work & Income regions use `geography_level: statistical_scope`, + `geography_vintage: msd_wi_region`, and stable `nz-wi-...` slug IDs. +- SA2 is deferred to wave 3, issue #178. When admitted, it will be a + first-class `sa2` geography rather than `statistical_scope`. +- Rent quartiles are not representable by Chronicle's current aggregation + vocabulary. Wave 1 records publisher medians as `aggregation: median` and + geometric means as `aggregation: mean`, `concept_relation: approximate`, + with evidence notes that explicitly identify the geometric mean. Quartiles + wait for a deliberate parameterized-percentile contract change. + +## Wave-1 ingestion ledger + +An unchecked row means that package work remains; it does not imply that the +official source is unavailable. Each completed row must pin the publisher +artifact and checksum, validate its source package, pass its country regression +tests, and record a verified `raw/nz/...` R2 URI. + +| Package from #176 | Artifact pinned | Package valid | `raw/nz` verified | Notes | +|---|---:|---:|---:|---| +| `stats_nz/subnational_population_estimates_2025` | [ ] | [ ] | [ ] | | +| `stats_nz/national_population_estimates_2025` | [ ] | [ ] | [ ] | | +| `stats_nz/census_2023_households_by_region` | [ ] | [ ] | [ ] | | +| `stats_nz/census_2023_family_type` | [ ] | [ ] | [ ] | | +| `stats_nz/census_2023_ethnicity_age_region` | [ ] | [ ] | [ ] | | +| `ird/taxable_income_distribution_2025` | [ ] | [ ] | [ ] | | +| `ird/wage_salary_distribution_2025` | [ ] | [ ] | [ ] | | +| `ird/working_for_families_statistics_sept_2025` | [ ] | [ ] | [ ] | | +| `ird/student_loan_statistics_march_2026` | [ ] | [ ] | [ ] | | +| `msd/benefit_fact_sheets_national_march_2026` | [ ] | [ ] | [ ] | | +| `msd/benefit_fact_sheets_supplementary_march_2026` | [ ] | [ ] | [ ] | | +| `msd/nzs_vp_fact_sheet_march_2026` | [ ] | [ ] | [ ] | | +| `msd/annual_report_benefit_expenses_2025` | [ ] | [ ] | [ ] | | +| `mbie/tenancy_bond_rents_tla_2026` | [ ] | [ ] | [ ] | | +| `stats_nz/qes_average_earnings_march_2026` | [ ] | [ ] | [ ] | | diff --git a/docs/storage-architecture.md b/docs/storage-architecture.md index 826ece4d..cf898933 100644 --- a/docs/storage-architecture.md +++ b/docs/storage-architecture.md @@ -46,35 +46,41 @@ Hosted tables mirror accepted build outputs and provide a shared query surface. ## Object Key Conventions -Raw source artifacts use the implemented content-addressed key shape: +New UK and New Zealand source artifacts use country-organized, +content-addressed keys: ```text -raw/{source_id}/{package_id}/{year}/{sha256}/{filename} +raw/{country}/{source_id}/{package_id}/{year}/{sha256}/{filename} ``` -For example: +For example, an IRD artifact uses: ```text -raw/irs_soi/soi-table-1-1/2023/842da11...aca17/23in11si.xls +raw/nz/ird/ird-working-for-families-statistics-sept-2025/2024/{sha256}/working-for-families-statistics---sept-2025.xlsx ``` -Derived build artifacts should use build-scoped keys so different builds can -coexist and be audited: +The implemented country segments are `nz` and `uk`. US objects deliberately +retain the legacy shape `raw/{source_id}/...`; migrating those keys requires a +separate consumer audit. The fetch and raw-publish commands infer the country +from the package publisher directory. Raw publication refuses to replace a +manifest-recorded key that disagrees with the inferred country path. + +New UK and New Zealand derived build artifacts use the same country segment +and build-scoped keys so different builds can coexist and be audited: ```text -derived/{source_id}/{package_id}/{year}/{build_id}/{artifact_name} +derived/{country}/{source_id}/{package_id}/{year}/{build_id}/{artifact_name} ``` Examples: ```text -derived/irs_soi/soi-table-1-1/2023/{build_id}/source_cells.jsonl -derived/bea/bea-nipa-pension-contributions/2022/{build_id}/source_rows.jsonl -derived/irs_soi/soi-table-1-1/2023/{build_id}/ledger.db -derived/irs_soi/soi-table-1-1/2023/{build_id}/reports/build_summary.json -derived/irs_soi/soi-table-1-1/2023/{build_id}/mirror/aggregate_facts.jsonl +derived/uk/ons/ons-mye-2024-uk/2024/{build_id}/source_cells.jsonl +derived/nz/ird/ird-working-for-families-statistics-sept-2025/2024/{build_id}/ledger.db ``` +Legacy US derived keys likewise remain `derived/{source_id}/...`. + Derived artifacts are reproducible and may be replaced by a new build, but a specific `{build_id}` path should be immutable once published. diff --git a/tests/test_chronicle_artifacts.py b/tests/test_chronicle_artifacts.py index 2728965b..f5b3c259 100644 --- a/tests/test_chronicle_artifacts.py +++ b/tests/test_chronicle_artifacts.py @@ -15,6 +15,7 @@ bootstrap_r2_buckets, build_r2_key, fetch_source_artifact, + infer_r2_country, infer_build_id, inventory_source_artifacts, publish_derived_artifacts, @@ -50,6 +51,55 @@ def test_build_derived_r2_key_is_build_scoped(): ) +@pytest.mark.parametrize( + ("source_id", "package_path", "expected_country"), + [ + ("ird", "db/data/ird/wff", "nz"), + ("ons", "db/data/ons/mye", "uk"), + ("irs_soi", "db/data/irs_soi/table_1_1", None), + ], +) +def test_infer_r2_country_uses_publisher_directory( + source_id, package_path, expected_country +): + assert ( + infer_r2_country(source_id=source_id, package_path=package_path) + == expected_country + ) + + +def test_country_aware_r2_keys_preserve_legacy_us_layout(): + nz_key = build_r2_key( + source_id="ird", + package_id="ird-working-for-families-statistics-sept-2025", + year=2024, + sha256="abc123", + filename="wff.xlsx", + ) + uk_key = build_derived_r2_key( + source_id="ons", + package_id="ons-mye-2024-uk", + year=2024, + build_id="ledger.build.v1:abc123", + artifact_name="facts.jsonl", + ) + + assert nz_key.startswith("raw/nz/ird/") + assert uk_key.startswith("derived/uk/ons/") + + +def test_country_aware_r2_prefix_rejects_wrong_country(): + with pytest.raises(ValueError, match="disagrees with publisher country"): + build_r2_key( + source_id="ird", + package_id="wff", + year=2024, + sha256="abc123", + filename="wff.xlsx", + prefix="raw/uk", + ) + + def test_fetch_source_artifact_writes_manifest_and_inventory(tmp_path): source = tmp_path / "source.xls" content = b"chronicle artifact fixture" @@ -160,6 +210,77 @@ def test_publish_source_artifacts_handles_label_year_entries(tmp_path): ) +def test_publish_source_artifacts_uses_country_for_each_manifest(tmp_path): + log = tmp_path / "wrangler.log" + wrangler = tmp_path / "wrangler" + wrangler.write_text(f"#!/bin/sh\nprintf '%s\\n' \"$*\" >> {log}\necho ok\n") + wrangler.chmod(0o755) + for publisher, package_id, year in ( + ("ird", "ird-wff", 2024), + ("ons", "ons-mye", 2024), + ("irs_soi", "soi-table", 2023), + ): + output_dir = tmp_path / "data" / publisher / package_id + source = tmp_path / f"{publisher}.csv" + source.write_bytes(publisher.encode()) + fetch_source_artifact( + str(source), + source_id=publisher, + package_id=package_id, + year=year, + output_dir=output_dir, + ) + + report = publish_source_artifacts(tmp_path / "data", wrangler_command=str(wrangler)) + + assert report.valid + commands = log.read_text() + assert "ledger-raw/raw/nz/ird/ird-wff/2024/" in commands + assert "ledger-raw/raw/uk/ons/ons-mye/2024/" in commands + assert "ledger-raw/raw/irs_soi/soi-table/2023/" in commands + + +def test_publish_source_artifacts_refuses_stale_country_key(tmp_path): + output_dir = tmp_path / "data" / "ird" / "wff" + source = tmp_path / "wff.xlsx" + source.write_bytes(b"official WFF workbook") + fetch_source_artifact( + str(source), + source_id="ird", + package_id="ird-wff", + year=2024, + output_dir=output_dir, + ) + manifest_path = output_dir / "manifest.yaml" + manifest = yaml.safe_load(manifest_path.read_text()) + artifact = manifest["files"][2024] + artifact["storage"] = { + "r2": { + "provider": "r2", + "bucket": "ledger-raw", + "key": ( + f"raw/ird/ird-wff/2024/{artifact['sha256']}/{artifact['filename']}" + ), + } + } + manifest_path.write_text(yaml.safe_dump(manifest, sort_keys=False)) + log = tmp_path / "wrangler.log" + wrangler = tmp_path / "wrangler" + wrangler.write_text(f"#!/bin/sh\nprintf '%s\\n' \"$*\" >> {log}\necho ok\n") + wrangler.chmod(0o755) + + report = publish_source_artifacts(output_dir, wrangler_command=str(wrangler)) + + assert not report.valid + assert report.entries[0].upload is None + assert ( + report.entries[0] + .errors[0] + .startswith("recorded_r2_key_disagrees_with_country_prefix:") + ) + assert not log.exists() + + def test_inventory_source_artifacts_catches_checksum_mismatch(tmp_path): source = tmp_path / "source.xls" source.write_bytes(b"original") From ccc58d702d04ee9a08b4e3434a51759c2aa4acc5 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 29 Aug 2026 19:46:24 -0400 Subject: [PATCH 2/8] Guard country routing before artifact writes (#175) Restrict publisher inference to canonical package roots, preserve cached bytes on rejected routes, and correct the stale quartile ruling using the existing quantile contract. Targeted schema, consumer, artifact, core, and import checks: 83 passed. --- chronicle/artifacts.py | 55 ++++++++++++++++++------------- docs/pe-nz-source-checklist.md | 13 +++++--- tests/test_chronicle_artifacts.py | 45 +++++++++++++++++++++++-- 3 files changed, 84 insertions(+), 29 deletions(-) diff --git a/chronicle/artifacts.py b/chronicle/artifacts.py index c2b944fa..e019da98 100644 --- a/chronicle/artifacts.py +++ b/chronicle/artifacts.py @@ -395,13 +395,19 @@ def fetch_source_artifact( wrangler_command: str = "npx wrangler", ) -> ArtifactFetchReport: """Fetch/register a source artifact and optionally upload it to R2.""" + output = Path(output_dir) + resolved_r2_prefix = resolve_r2_prefix( + prefix=r2_prefix, + default_prefix=DEFAULT_R2_PREFIX, + source_id=source_id, + package_path=output, + ) fetched_at = datetime.now(UTC).replace(microsecond=0).isoformat() content, inferred_filename = _read_artifact(source_url) artifact_filename = filename or inferred_filename if not artifact_filename: raise ValueError("Could not infer artifact filename; pass --filename.") - output = Path(output_dir) output.mkdir(parents=True, exist_ok=True) local_path = output / artifact_filename local_path.write_bytes(content) @@ -410,12 +416,6 @@ def fetch_source_artifact( size_bytes = len(content) manifest_path = output / "manifest.yaml" - resolved_r2_prefix = resolve_r2_prefix( - prefix=r2_prefix, - default_prefix=DEFAULT_R2_PREFIX, - source_id=source_id, - package_path=output, - ) r2_location = ArtifactStorageLocation( provider="r2", bucket=r2_bucket, @@ -426,6 +426,7 @@ def fetch_source_artifact( sha256=sha256, filename=artifact_filename, prefix=resolved_r2_prefix, + package_path=output, ), ) r2_upload = None @@ -822,19 +823,27 @@ def infer_r2_country( fallback for low-level callers that do not have a package path, including derived-artifact publication. """ - path_countries: set[str] = set() + publisher = None if package_path is not None: - path_parts = {part.lower() for part in Path(package_path).parts} - path_countries = { + path_parts = tuple(part.lower() for part in Path(package_path).parts) + # Match the innermost canonical package root, not arbitrary ancestor + # directory names such as /work/ons/chronicle/db/data/irs_soi/.... + for index in range(len(path_parts) - 1, -1, -1): + if path_parts[index : index + 2] == ("db", "data"): + if index + 2 < len(path_parts): + publisher = path_parts[index + 2] + break + elif path_parts[index] == "packages" and index + 1 < len(path_parts): + publisher = path_parts[index + 1] + break + path_country = next( + ( country for country, publishers in R2_COUNTRY_PUBLISHERS.items() - if path_parts.intersection(publishers) - } - if len(path_countries) > 1: - raise ValueError( - "package path contains publishers from multiple countries: " - f"{package_path}" - ) + if publisher in publishers + ), + None, + ) source_countries: set[str] = set() normalized_source_id = (source_id or "").lower() @@ -852,14 +861,13 @@ def infer_r2_country( if len(source_countries) > 1: raise ValueError(f"source_id maps to multiple R2 countries: {source_id}") - if path_countries and source_countries and path_countries != source_countries: + source_country = next(iter(source_countries)) if source_countries else None + if publisher is not None and source_country and source_country != path_country: raise ValueError( "package publisher directory and source_id map to different " - f"countries: path={sorted(path_countries)}, " - f"source_id={sorted(source_countries)}" + f"countries: publisher={publisher!r}, source_id={source_id!r}" ) - countries = path_countries or source_countries - return next(iter(countries)) if countries else None + return path_country if publisher is not None else source_country def resolve_r2_prefix( @@ -894,6 +902,7 @@ def build_r2_key( sha256: str, filename: str, prefix: str | None = None, + package_path: str | Path | None = None, ) -> str: """Build the canonical immutable R2 key for a raw source artifact. @@ -904,6 +913,7 @@ def build_r2_key( prefix=prefix, default_prefix=DEFAULT_R2_PREFIX, source_id=source_id, + package_path=package_path, ) return posixpath.join( resolved_prefix, @@ -1132,6 +1142,7 @@ def _publish_raw_manifest_entry( sha256=sha256_actual or "", filename=filename, prefix=r2_prefix, + package_path=manifest_path, ), ) storage = spec.get("storage") if isinstance(spec.get("storage"), dict) else {} diff --git a/docs/pe-nz-source-checklist.md b/docs/pe-nz-source-checklist.md index ab98924f..c0b362a5 100644 --- a/docs/pe-nz-source-checklist.md +++ b/docs/pe-nz-source-checklist.md @@ -35,11 +35,14 @@ These decisions implement the rulings recorded in `geography_vintage: msd_wi_region`, and stable `nz-wi-...` slug IDs. - SA2 is deferred to wave 3, issue #178. When admitted, it will be a first-class `sa2` geography rather than `statistical_scope`. -- Rent quartiles are not representable by Chronicle's current aggregation - vocabulary. Wave 1 records publisher medians as `aggregation: median` and - geometric means as `aggregation: mean`, `concept_relation: approximate`, - with evidence notes that explicitly identify the geometric mean. Quartiles - wait for a deliberate parameterized-percentile contract change. +- The original rent-quartile blocker in #175 predates Eurostat #168. + Chronicle now supports `aggregation: quantile`; publisher quartile cut-points + can use it with the percentile label or code carried in explicit constraints + and source evidence, following `eurostat/ilc_di01`. No new aggregation name is + needed. This removes a vocabulary blocker without expanding #176's package + scope. Publisher medians use `aggregation: median`; geometric means use + `aggregation: mean`, `concept_relation: approximate`, and evidence notes that + explicitly identify the geometric mean. ## Wave-1 ingestion ledger diff --git a/tests/test_chronicle_artifacts.py b/tests/test_chronicle_artifacts.py index f5b3c259..ccab797d 100644 --- a/tests/test_chronicle_artifacts.py +++ b/tests/test_chronicle_artifacts.py @@ -57,6 +57,10 @@ def test_build_derived_r2_key_is_build_scoped(): ("ird", "db/data/ird/wff", "nz"), ("ons", "db/data/ons/mye", "uk"), ("irs_soi", "db/data/irs_soi/table_1_1", None), + ("irs_soi", "/work/ons/chronicle/db/data/irs_soi/table_1_1", None), + ("ird", "/work/ons/chronicle/db/data/ird/wff", "nz"), + ("ird", "/work/packages/ons/chronicle/db/data/ird/wff", "nz"), + ("ird", "/work/ons/chronicle/packages/ird/wff", "nz"), ], ) def test_infer_r2_country_uses_publisher_directory( @@ -137,6 +141,42 @@ def test_fetch_source_artifact_writes_manifest_and_inventory(tmp_path): } +def test_fetch_rejects_wrong_country_before_reading_or_overwriting_cache( + tmp_path, monkeypatch +): + source = tmp_path / "source.xlsx" + source.write_bytes(b"original official artifact") + output_dir = tmp_path / "db" / "data" / "ird" / "wff" + fetch_source_artifact( + str(source), + source_id="ird", + package_id="ird-wff", + year=2024, + output_dir=output_dir, + ) + artifact_path = output_dir / source.name + manifest_path = output_dir / "manifest.yaml" + original_artifact = artifact_path.read_bytes() + original_manifest = manifest_path.read_bytes() + + def unexpected_read(_source_url): + raise AssertionError("A rejected route must not read the source artifact") + + monkeypatch.setattr("chronicle.artifacts._read_artifact", unexpected_read) + with pytest.raises(ValueError, match="disagrees with publisher country"): + fetch_source_artifact( + str(source), + source_id="ird", + package_id="ird-wff", + year=2024, + output_dir=output_dir, + r2_prefix="raw/uk", + ) + + assert artifact_path.read_bytes() == original_artifact + assert manifest_path.read_bytes() == original_manifest + + def test_publish_source_artifacts_uploads_manifest_entries(tmp_path): output_dir = tmp_path / "data" / "irs_soi" / "table_1_2" source = tmp_path / "source.xls" @@ -211,6 +251,7 @@ def test_publish_source_artifacts_handles_label_year_entries(tmp_path): def test_publish_source_artifacts_uses_country_for_each_manifest(tmp_path): + data_root = tmp_path / "ons" / "chronicle" / "db" / "data" log = tmp_path / "wrangler.log" wrangler = tmp_path / "wrangler" wrangler.write_text(f"#!/bin/sh\nprintf '%s\\n' \"$*\" >> {log}\necho ok\n") @@ -220,7 +261,7 @@ def test_publish_source_artifacts_uses_country_for_each_manifest(tmp_path): ("ons", "ons-mye", 2024), ("irs_soi", "soi-table", 2023), ): - output_dir = tmp_path / "data" / publisher / package_id + output_dir = data_root / publisher / package_id source = tmp_path / f"{publisher}.csv" source.write_bytes(publisher.encode()) fetch_source_artifact( @@ -231,7 +272,7 @@ def test_publish_source_artifacts_uses_country_for_each_manifest(tmp_path): output_dir=output_dir, ) - report = publish_source_artifacts(tmp_path / "data", wrangler_command=str(wrangler)) + report = publish_source_artifacts(data_root, wrangler_command=str(wrangler)) assert report.valid commands = log.read_text() From 544b26e96545f829a6afe98ec7d094b04b98c76b Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 29 Aug 2026 19:52:41 -0400 Subject: [PATCH 3/8] Require publisher and package structure for R2 routing (#175) --- chronicle/artifacts.py | 9 +++++---- tests/test_chronicle_artifacts.py | 30 ++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/chronicle/artifacts.py b/chronicle/artifacts.py index e019da98..d9620761 100644 --- a/chronicle/artifacts.py +++ b/chronicle/artifacts.py @@ -826,14 +826,15 @@ def infer_r2_country( publisher = None if package_path is not None: path_parts = tuple(part.lower() for part in Path(package_path).parts) - # Match the innermost canonical package root, not arbitrary ancestor - # directory names such as /work/ons/chronicle/db/data/irs_soi/.... + # Match the innermost canonical root with both publisher and package + # directories. A package itself may be named "packages"; the following + # manifest filename must not then be mistaken for a publisher. for index in range(len(path_parts) - 1, -1, -1): if path_parts[index : index + 2] == ("db", "data"): - if index + 2 < len(path_parts): + if index + 3 < len(path_parts): publisher = path_parts[index + 2] break - elif path_parts[index] == "packages" and index + 1 < len(path_parts): + elif path_parts[index] == "packages" and index + 2 < len(path_parts): publisher = path_parts[index + 1] break path_country = next( diff --git a/tests/test_chronicle_artifacts.py b/tests/test_chronicle_artifacts.py index ccab797d..e08beabc 100644 --- a/tests/test_chronicle_artifacts.py +++ b/tests/test_chronicle_artifacts.py @@ -61,6 +61,10 @@ def test_build_derived_r2_key_is_build_scoped(): ("ird", "/work/ons/chronicle/db/data/ird/wff", "nz"), ("ird", "/work/packages/ons/chronicle/db/data/ird/wff", "nz"), ("ird", "/work/ons/chronicle/packages/ird/wff", "nz"), + ("ird", "/repo/db/data/ird/packages", "nz"), + ("ird", "/repo/db/data/ird/packages/manifest.yaml", "nz"), + ("ird", "/repo/packages/ird/packages/source_package.yaml", "nz"), + ("irs_soi", "/repo/db/data/irs_soi/packages/manifest.yaml", None), ], ) def test_infer_r2_country_uses_publisher_directory( @@ -322,6 +326,32 @@ def test_publish_source_artifacts_refuses_stale_country_key(tmp_path): assert not log.exists() +def test_publish_source_artifacts_accepts_package_named_packages(tmp_path): + output_dir = tmp_path / "db" / "data" / "ird" / "packages" + source = tmp_path / "wff.xlsx" + source.write_bytes(b"official WFF workbook") + fetched = fetch_source_artifact( + str(source), + source_id="ird", + package_id="packages", + year=2024, + output_dir=output_dir, + ) + wrangler = tmp_path / "wrangler" + wrangler.write_text("#!/bin/sh\necho ok\n") + wrangler.chmod(0o755) + + published = publish_source_artifacts(output_dir, wrangler_command=str(wrangler)) + + assert fetched.valid + assert published.valid + assert published.counts["uploaded_count"] == 1 + manifest = yaml.safe_load((output_dir / "manifest.yaml").read_text()) + assert manifest["files"][2024]["storage"]["r2"]["key"].startswith( + "raw/nz/ird/packages/2024/" + ) + + def test_inventory_source_artifacts_catches_checksum_mismatch(tmp_path): source = tmp_path / "source.xls" source.write_bytes(b"original") From c9d6eae12fb15343009a2cf66bb1b5407795fbb0 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 29 Aug 2026 19:59:01 -0400 Subject: [PATCH 4/8] Add official NZ WFF and IWTC tax-year 2024 source facts (#176) --- chronicle/source_package.py | 3 + .../manifest.yaml | 12 + ...g-for-families-statistics---sept-2025.xlsx | Bin 0 -> 71211 bytes docs/pe-nz-source-checklist.md | 2 +- .../source_package.yaml | 1331 +++++++++++++++++ tests/test_chronicle_bundle.py | 27 +- tests/test_nz_targets.py | 317 ++++ 7 files changed, 1679 insertions(+), 13 deletions(-) create mode 100644 db/data/ird/working_for_families_statistics_sept_2025/manifest.yaml create mode 100644 db/data/ird/working_for_families_statistics_sept_2025/working-for-families-statistics---sept-2025.xlsx create mode 100644 packages/ird/working_for_families_statistics_sept_2025/source_package.yaml create mode 100644 tests/test_nz_targets.py diff --git a/chronicle/source_package.py b/chronicle/source_package.py index 993b4d9f..e6ebc232 100644 --- a/chronicle/source_package.py +++ b/chronicle/source_package.py @@ -94,6 +94,9 @@ "hmrc/salary_sacrifice_reform_2029_headcounts" ), "ici-fact-book-table-30": Path("ici/fact_book_table_30"), + "ird-working-for-families-statistics-sept-2025": Path( + "ird/working_for_families_statistics_sept_2025" + ), "isc-annual-census-2023": Path("isc/annual_census_2023"), "isc-annual-census-2024": Path("isc/annual_census_2024"), "mhclg-council-tax-levels-england-2026-27": Path( diff --git a/db/data/ird/working_for_families_statistics_sept_2025/manifest.yaml b/db/data/ird/working_for_families_statistics_sept_2025/manifest.yaml new file mode 100644 index 00000000..89c069a6 --- /dev/null +++ b/db/data/ird/working_for_families_statistics_sept_2025/manifest.yaml @@ -0,0 +1,12 @@ +source_id: ird +package_id: ird-working-for-families-statistics-sept-2025 +dataset: ird_working_for_families_statistics_sept_2025 +source_page: https://www.ird.govt.nz/about-us/tax-statistics/working-for-families-statistics/working-for-families-statistics-datasets +table: Working for Families statistics - September 2025 +files: + 2024: + filename: working-for-families-statistics---sept-2025.xlsx + source_url: https://www.ird.govt.nz/-/media/project/ir/home/documents/about-us/tax-statistics---current/social-policy/wff-stats/working-for-families-statistics---sept-2025.xlsx?modified=20251111195236 + sha256: 95ae66f4d44f3f47ea3daa006328b22f061a163cf7e31b487342cde649390833 + size_bytes: 71211 + fetched_at: '2026-08-29T22:46:45+00:00' diff --git a/db/data/ird/working_for_families_statistics_sept_2025/working-for-families-statistics---sept-2025.xlsx b/db/data/ird/working_for_families_statistics_sept_2025/working-for-families-statistics---sept-2025.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..3b286ea097bc2674f228ddc1b23e886214fd03ca GIT binary patch literal 71211 zcmeEtV}m6@v*u}Y+O}=mwl!_rwx(^{wrzJ$+cu}|X>Y$+-QD{K_HKR1s?3b6b0RV# zo~TpLQIG}!MFD^VAOQdXA>c?6p`k0>=-h3r z2?{`gDe?f{LjV8xuWW%ybseh>1{5Fs3x1657!3>d5e&)KSsR8`@qFA1)%Y4CL?1#* zo4afBaXEA8jMWqapYX)h=Pak#t7I25^`DdxUUe~6tm5cLD5!a5rrOoVr!!lvFxT%)1c?)tfj=+nF;M+*&~-HH+XXZORtZ;qd&I(AYvN#$bOP zxjw?m)$o#X|AqyiHPS1@N8b#u*!=BOY&e8QU72!6^Qt+-_WljjMp^^)* z=!ggEa2^3Rr~$uGN*cNAGq>x1i%S#!OHm58_Zr`#nM}w8<;2QJT_pFEbW}cRi#6$8o z!HDuV@C~51I;tJQ27Hzbf>{R!IFif(VHo7<{0t=xFi!EmK56Ulg$y5toGV&Ej4&!1 zyx{g1NBkd;Y?nzhdjdFxViv+W{z3o%Utb^qh5s!94dBceG6Der4j=#k%y$CPb2PDb zqNn>u`~MTqf8})ZKXtEKn34ivL;&n|?s5ljcCOX%hdFOVLV|(CzXBGFUD;X2gGrX2 zE@dl4(f?4b8YPS$W@0j>+f*6UVib_6-#aKehR7w%*Hpj!-uHDL)Rfsp!En^LrxoMN z%*mC_nG={uH7xZp=8TMFy;izw6%tv?|D_Qpo+^(8ji#gB39gj^Gz2XfR&|oxJJ(Hd6wRq&2ZcwF zF-uGer9@~+HH8K9cIrkdQO!qc)Omp@E{F?mbN%oVdJi6Zd{;B4mDxD+_h&Spx+{qq z+74%9=-eCOA$31Cu`Wdo+Hf`DluBi-LoTw0-Y6@4W|0maQYdz)Y$4Vt;A?oIxi^Xh zw{M&MpAiOYKD&4H9bKDH008{Agu6Apo1LSTp`D%8Kj6JeRmX0h1I0(r;wvcmnZ=cl zK%}@Zm}Fh*vdnPR^$Dsy+@gxiVVR9qhV{#*79`)9ORGLxRR5s%!0DRz`SpFFq|5s9pjil9FLwU2w#H;h|o88Hb5Z^Y;Z5SX=Rk?D$ zBN9g(71gSY3mMnSz=YITm=*RYsE}*iyqs#pS(%$|gt#=ntJxJYo(D|0O07%3oMQQh z)gzopLxyEuE!B28P&K7ofK{2dwwo(q1{}ea$JApBM6-gjNGg1F+(Qp|BRh!zHq;K{ zJ7mZRu5Dnkn>uS(oKSc&ABee`_-pnDD0gs|L;rC%yqxeI?s!gH_+CIVj!)dusXwbH z81diKIY^1h6)Di^Hv%pD-D55E6KdVYjcjMc%Z`~Q#`p?a73Jc_1S>VUYDU;$W-n&K z6`h7-)~XKV3ptvTeS#D0aH*Dpb1iy-F%TZ(5pxy?kVfQS#FdQk6iYVbYH-}E%U9K@ z$JItD|AD!zkM83HLBoKaJ~eX-#5pe)VV7Ua4ci_Hf)fsFzKxle=@n(h*}Io=F1YSu zO--&S$xT|zGL0R_GIZ@Usqp)f^c!~5agq~~>7I)PPBF%^J14V#2yC=btvL28(?wff z_&N|N68nDi2BA8lpa#~sG^d93XKC@B?rqJpZoyK6id*USY<~ql<8M6hvkb1g8I@Cp zv}W;Esxzh&u5aG9pv)-$b{)-uc(M+KVuz%8*sR^YRZ=Gq&)pr|#E;@v5lDA8h8;D} ze?k5^`&NF0iLEW{Y}surm;jo%*l6a9wBd^pb-^It&nt<85Ou&a>>)qPX}GFxpuhfH zcF9b>;miuA3ezrHJ*#l4*lYcM^;1IZn9D;G$>{R7pW7XJw_8BrVlIMBK{Q?1dyE$;!Jg5CiB+1aNd2|KJYsukaz*dw#{?QJn4YKV;8lOp}3h6>bc&c}Sl3 zf%h2gKNuRl#gO;LHp8P}$ZB&v^`ZkMaB0-fD%M280LutveC5vx<@+ey8)l`6c0x}; z8@YCBktz$+J_9)b>=-pjB9N_uMEJ@%Zjd8elaG(J2C+N`GD@0h0`4%zu4=vg2Q@1l4kfxLmor zCDW$3y_9DjoRdV$@8Hjz-0DSMoZJ7pR}Q@bwq4gPnI=e#^!vYNgep3%Ug>YlCHzKY zEC3|XcSiUZ?EbHL;lD#L(0A?sE&E^l=t`Eg8f1VCxeodW-F@39yX=6^c&KAMbvf5p zKPZzlH-wPhTK3~d)Mya8^<-$rATc@X|)cOx{o|lL*;X+5Ypg{$TQJw53z0dT;HIFigmFQJfa*Yq?VMUfx8q zKststVTXy5^)^ZrvmiG_j>oJ7lq zJ|VczzQ!ND+A65^U-0A9V|ggcZWv5|L?9iUgdMR^8IKD4h8^V zeEZnH!O6+o#KhT&{+}ntf1p%W!q3=D283Tb)Q|iczU1I+#>glQ>%n4MsZ@V1w-|-% z_F&FqD&H83UjlL7(gOqu3N?b>1eHI9d0zlD_0iX&eUC&5QR;Cj2~N3D zc{hj75U|S%oWXXx?TuF|L+R>UDtgA5N__mY!vEgIudfEmX$e_c2T2;qrbfyMdUc8h zRVEcB3Ob2NYVd~1D%hzc6e?I*q{M0^DjlWBMCE%a>txu6D4FC7>jc3H5P^vRsUI@Q z50};t0j2=S;QW82pRccfHYzdwmcW9rA$lef`QmP>MT^r{CY5br8$q+uw+QKN+)Z*J z7>KVA9mJ7o!U^;wU-;Pd#XqFZ2gl#%1YyR+?evFQw`QqFS+WDx8maIh{y^eN$}2NK znBo#p+thZF@`@ufCRl&)qu#)U3@a2xq74Pk`2!{I8`31 z^z1YGs!$HHhY$M4nP-(YOe?=HMwj6#XHI zUf(LSG`mjwM=%(1v#Z`;IT1){QD&AQL4w@5dEd-D?B$`Z1>DbPM35DxAu$Xr&sR~u zezrtiQQih*W>8z@6^P$F7(CsNI@@iHv;qS1^u0wl5IPbV>9uAzW33^;cUYD!@NSl? z)30%EJkyF$r4KmFGNgs&h0bgltrsMKV5nQLWH<=nP~h8qcP6HDS!r$eSllvg(4i{1 zNz}e$0Y^cqi9&CAgsVqs^_g(IjQ}E4JmO@lee&kLA|Yi!(=L-iC_Q`Gd+Re;76Tpq ztm7*)jl>AfwPB}Ep6X*eA>npdiod?o&hQ27oIjPiwV~!zoM(pY<-ib=_ec}b#32SV z6a9cN4XEG5N6oK_~Zor1?3*+@=wJ`UCRu&ixRMq zjV3qS9KVIAYAYR-r?4|9Q_+_Hs`Wh2RCCgD_Dg8S2N&Fzro!5UN%tL6FrmcIqijj- zs^=~6kfmL;qngX8BwHiSD)MZ#6PFoB3yUuM#p^~PmS94^A!gB0o_~!TPAN?OqZVO% z&L#coxih&)Ukg)J3*NZ5_NC{eod1y$yXfr`-%#Sh$`|1ZV5jo8K4HwgZU7t22SFNq z(k6BKsbK7ON3_W^rl+b<5e0mlf(+5vI$lof%n2)YB{8ik=!l;u7Zq4;)#Vopy;B## z8a0&R#f+Iwpf8j!%k{_@m}BAMv}{+O5D-SlN>;l$0dy*e-d)G^Z7Zab?Wc1UHpm(L z@!Rc^o*Ut3jbguaNA0E3Qm?2%C5}8V5`-kj$V$1o%lpJ|OT!JrFI{M49ifK(uKf@} z<9r5sXK5zYAoyGf=;u?x{nPSJJg-aydeVLt$FO>{)B#Z97IwAq-*<<=kDxf3;xddK zNb%^Q2hKv;ztBOhUSkFItzNkj%#q+{t7ilqI+q`B!2d@OBehYtw_ z!bu}KS*8Bm{uJ0t8U2{Ogpv_4~l773JO7-)5T|s}}Xr=u;c&`21`uA??t7UJYcIWfsOD?zf{q;r9 z+#2rc=)2e-~7LYId>GZHrzNUyCi_mJg$A3 z>t&qvlJ~-#OCj^9k@k&s6M~q7!&ksD8GeOgtLoq%IrHZ`wI8lgqKwHExViml79WeZ z-F8c59@zj};IMttT4vV_Li5D+BBF=G zi_PM}b?)c;`*W(@ce~M<>h=DVgC0r$)d&1=_ldT%J%9}xv~!nnU&0$)WA)|>DVy4X zpu^Rx*+hy~aD};LY@eS=cP7aQq`|Jk(1GZ`%=xiQPPvztiu>~RL71FqJKI6ztf~&L zZ+n-oG+*md8pGI;u^99MC~gzJF)+tE>D+e7x9OBE)(7$M9x%QsNLmDKcI}b_s7Vd2 zEB`D9?XdCss3C`%O`W-OQ_1f(-|y)2cJ6mXK~p=d2_ z5R6W3#R<@)Y|lbnW4bc>(qbZc(;h>L2F^j0+{-1@SKh-@_bqfGgnyw6EZ`59R65?jz9Sx~i~ zME;M+ddjEIi)Ehtk#D6l*bRgXb3cs(Pj?F>pRNm2UL9QgUSVFF*}&ag7s&qPFrd4E{w;30BLQbv^-FQea= z>Fac$#k8qbHx>}b&-KWSH8K~1; z1lnC1R+&cVLaw_F%Uh6|_pO{IqT3Jq&}N%SUge+T4dR?4==sa5?W-*tDH?FRy!8oC ze*7%LrCDt&in{fc&aR}1Jx#Y7NubEu@hgn>$&3#102zzUZvOqd$`g!)+ZQOsY(l$z zpmQ~;A74>6?XJ;^5%45qCmNHhcf8G@$k`UT&_GeACRokel^>7E2t1OrnwAB)qysXI zi3$3Tq=kU~Khj(mb)PCxrxUGZ^0% zBw;SgB>*yw15}VUr65GL!TMVqg_uX1*oB(f@Sy}%`(ol&hnEP_tD3h(^0CEIvP0jd ztCS79&GxL|c3X%!=N!VTnZhWu(7%8_2_ai^E_6&#lx}FeD324QHUzK3d6e!#3S?bl zTL!5B1CzUZF5E_$zVqy48ZGLCKcW&onIQTzfni|@rzo%d0=8E$;!=BQ4#-B4Ms9QV*hf?01YVEr zLVFTIn-w%AmDrw06cK?(5jhdz@xz>d_+hG!OsN#9HWc@GC0>DH zJ0X>U8C0ll(2At;qx+3*p;csh4O1CxZKdW2Q=D^vEm}HP{)CT)Ds##i!-KDYaZ7BS zCD)`zbAgY(VB`-eT3^vA`Ct55kbZgS+3EE}QoG3>|zR>o4d1@n9@Wc8obm@kM^*T|+gN43!$v*ha7_gmHkPyWHg<+}~L# zm5+icDi^>utzt^9oLj2gVkmGt+)*Kh6;Yz!knM~Tmz8h-o%T`EkuzX_$(R4Z7KdUa zQsi#JSsYqUmB4{P4RfKam>#cHmnIT6lDETCVjW5h6})Ywl{^GnsoJ5tQc>jh zKo)ARlS6uP^b>okT{0M60}J&$jN6?2AlBGBcRh?0HDJha{KYBN{&{Q848mFoeh zA8{~Y`WYn%xuVC#2v^1)YxM+01a!nab9;xJNnfc`=zy^m7AspVjT7`?OFnNBSrS~t zCdsB3bC9&_s8`>Pp+AjrruE2=AKpE;5NL*oSfr7ux_K2Q(7@G`Tcrg89x+GREYYwP z2nM+0e10auxouXEf32XG0?Q~Z%_F7 zO-#?=>YbDBu1H5-QffCBr)0%9oEv1#Z2z3-b!jXvv29r|WM_^DN*nn=R}A$J_4So_ z6EQOs%&8xpGIq|Q0Bd+2N%S^1Wy$=UWw?-uT)O7Avu&hHCSWOszsW;GtEZtAwnRFc@i9wEk`sa;IJ>EJ zE|qAZ?m=W10vWc)>Lo=vw`q>(po-TYPguocKR;Oxogm}{62)mS`o}&$i9=xNI1CJ` ziuxL8R(2bPdDwXE8dP&eHg@5p_8G(+*v1rsrT)CY4U*7?vWz9t6_sS(!1vby?2dU6 zcYRgqlI)LN1~yE}GiY8;`clZHQ{3)o!aiWz!9N$K;cKC(q#rYb5Z z9t6jh4M+-nAhW(x7(p%zx|EfTx^N=l$e}z5n+>If__xl`ANJ?T30GO@9N=e5z^zF<)lB^Z06oBFytwL`f)#ICeE*| zG@<+^k!KW^z{)H$q1n&aVOGfHMy8w?*j#pMo9dx;3KLEiLE6|8LrXQTD6Pv(IpPkDFLANYV?8`f{tp2V ziIj%PK}q(Z{Ut8Kn#2hZhH5rUx=B2BLvXXt@8VOxs0S;pa*{CL`t4z_K+9`9^gEt$ zmF|8tB!tbegpE#JQ>k?+c&lWbB zfKQ{IfzWg}>a-~d-LeJ&n~3>H?m znY!#?>Z+l>O~Q>q!Z=2D(^%RZluGp0#qV3b_}O*Y!i$#^E*`ZTcxh{~6s&J!;v}y; zk1^1h2&a}!(idv1z1WyAck1+j4rOoPpR3A@g9jxlk&cUTKBBn~x2}17L%|UU*(0+< z6x-y3M#zI~htaPJ$4aQd$(vHf_gMG(iZD2vVL=KE!*Lfw6LId1?G*>?=>vQ;G*8|g zn4J|%RP^GPflRn3ffXnnq<{s->Jbche0h%y?wlR`9H9Tk*c$2ai=VN<o%)L;f)Ge__p=O4t#BB;Sqz1@_YIOVH_N24%Kzp=4 z?|WnBE$75?VaEiKrMaFjNe+QM{g~cSn4jVsF~C38@z8)6q>zf(Z)_ffsAj`12jBq1 z@N$;yd8_`W@;8Yq=t_jhnP@^y`hicgQlI5#MVp;VqNrvwNNFgD%TRKHPxgf~b}53i zvY$+2@ld~A-b6$=m(AA%ML2gG{CDRK;X2Tgs1#J$gUqRElIwNsjFI=wDzbm zwG~&F7c@WbYI<~pS3N!BKWdpc;~;SX$)OhF<1#1`9r)QyR@}p1>~e&;OAYg^$)AZIb}Mk5XiXxFqBsayn@g5>k9L|K$37#{rb(PfAdwOgQiL8j^?+kyB$-|>{uJ? zH*n+V-Va1)rQy+xp?M=M?rdpRmi4Ag;Zj;N9>LvtmOm+i-|$?zXlVgAG#1kklIFKY zRzha`E84P<0>;qCxi=4S_2++<1`~MA$yh6g)&HPP3KD+FFnS9p)@V+uPx}}r4`QT_ zx?oi2rZ3@*z_w_fM2v=xjw+Vv_+1WPodHp#orE&U)6Fr_r(n5=&@KrDzk!#LBoMYcDL%z!N@RE?qC zmAnlWw|aI!+~ZnTfNMgU4hI1@LIcnoFT%4X37wFcaWkmIfYO2)ey3Ttd!Fb3J^N?( zN*0(QfyFBgupl9H%cf!%S>T-+G`nb&7`owa%;gRzH3WgAXbbboa{U}d34u6Vol+8u z_V~YlJZIR1|MI6J2?iSlxCzBsJY*A3SXceDnSc!f6y=x^Hg*i)QIMpFV@UeJkp#$o z{=t|^We)t&o4h26!U-{DXmC(=}d^OB`OEI%6juY9k24*A+%#3`ZDD zviN73Ll{hD__Tk>Hy#>oUPW2P8C6D}pU{bjh;8oA4}t;B4A-XZhlMSXrUEvwP?Vg9 zrK1Qy4A|72>2ZOKMex#Np`t2+mn`g$Y;xE=Zoqj_lVhc^P)23BFAuwvtP-(5>}wFU zhCWtCW*_3VG`L);)P;m1`)}Ac9=><`u#?6<{@6vi!vWlyyL z3%e6o!94*rLUyBGz-eJVHNsKF=JK`#_w6P#{LN5j>1Ge2PPO+`*f_R$m9PRK)@3Gh zmbX(f7BgIMlE;Y~nRXWW( zq^`L)HII{!f(R^_HEilH#3BS-N3QYn4)fTj%o(JsC(A_UuI2U>aRK&6ydQz(T%CPe zCPF&MdtJh)mr1D@2-83TL#?ef_Cxm5b-!!8MYuqrlxC+}&&~N0v#*2c8^t)tL9Gyr zBfA^jSy| zI>B21e)2RQ5jpSuc4_8Kh)ZRknAA~(mQj~F$k|ZDb`%^#AUX}GLZw=GDt$|%d6jUg={C{d>hR3WFh zxGT@M>liT=)FF8-bqJYGVv`c`W9FuoiKm(-0``Ln|3E4bG)LkTh);!iw7|GL%-|qn z^#9h@r0kZ3%%HYe35+P5v4G*qG!Ab#S`LX{B!BkvI zL^P%hK0 zxFy+3c`6;kWY)Ejk3qV*>wQ4UfK6B;Xn=m+dxQE7{>Jd%D7mV+9wYcfgWWDC_qGT3r5 z$1U{X-aNAztENW%~FmkgTZ+eul9_%qV(9m-NN0 z?(&~~H;Autp^T$;sl>Ap8i&JbIWe2ZcYl?G4iHJmA2Xwyfz|23#shBNt4;C3RV5hV zwBOb>tGV5#W4NPC=<)0PiL*G99Ns{+zhtTmLD8d(fBwxxXf(w_=HXxp_SB^lB6m@( zRy1?LOzt1vE~Pv~in$gZyB@-TIf#$@l@v~#KXTXZ#D6J+e~6<91Y1uMr|e`iY}0s? zo+e6fh-P)v1Jy4zpvZGc6iEPpOSU2%CkUvhDNAfK>)9!CP|=-}F@ypPHel6uGIjMR zhMPH4#1m$T$$IYVZmgBrBmBVn?d(UPyPSvmrs7Ff#TXcAVIu8~YzSvXFi~_S44cli zmhzag;sO-a*ma@*6%k%6e1R0RkTMUs^dJ_Z&RT*8RQ|rM*~$I9F1LtAO~{u5S%7l^ ztsv6kL~>THwN2b1^+`5V#eA59z$0dZvVOIcW#pdx-t9lKpI0^o>&ljv#dS^LpqW|5 z7{?Ug^;!swg9K;D*0cj8Ei918grU1q66LDj5tWN&AyJm#aq_u2N5&Ca1{YKNO6VP^ zkgUN(zRnKA^uDhWB5?OSR}oq+=A5r5{YEx)q0KFOVkV$dNVM%wPl%xk?O$H(r+JkS z3R;1L!7M_=viC_we*$7b#rd7w;6;uQgNWZK{J|U4s?z0h!ZL_ z7Au-X{ls#HaP`z$J>xixZp{5Fp!t;JFiTin9#cT4x`b#d>SDrUtmmSPV{po5WlAn2 z!#eCxqfO3>34myAFiuDc=eE`fesQ!JM`60@hpXmt!1XZTy*zv*J8i<~m83(Bd|idg zx@oc>*gHJB78qC`S`)?jX}+Suz6Fd*d@=){FdIHY&DJW2uHqZ!!}XbBnrmBh3~rNp zhg*WR**f#}_qyI>R3CSEH>FM-=5cJ>CtL8yHJ^6iAd2ec>ou;=LX38)YKGb-HM!+n zYQv`ZlCO1*lEu7{(M<_%z~S~Tlskq-%KI~Pbnu*l$L;tIwT!s=D8m+}P$Tz{M4_g& z&Hq8LRyB@~t`~a#NOD==_1v~qRWbu)*p3{01Y8e`dx$x=F*k|=p&|7uJ40bEE;FWZ zvZ}BBY)F=v)SF7iG?T97%reuqrIkHP;~t}*FuDJKjb{X)diWKSRx;2$%kC54jdN$ZW40dU$S1OC~PuQb)yip z>QvuTH3bUbo4>|b2Slt64#8yJ!7Ol+t1e3U zyC(ymgDJ>Wr_D)ec*I0X#rhcF`<3rOFztK!$A8*)Ze>ed z<~;zR3M5#=PypfA*923bcp_U^ku9)|HDlPL!VM9gnp*-5S-J_6=*>!|b-1WAeB;~kRF9o= zsJ2mn4R@Iv*HeNZaQo*O)d*FPONVy#0%!sMw5c=O7uWD<(wXBcayDW6Q^|<UU|B5hUG-Bfk-aMS{yAfjiO?1<^KqA-G zNJMcT@Ena*4onYfGv?iSx{@FA{he`XRt-;lad*-SFB?QE?qV$^>5M$ebTk23lx!5UX<`D^ZeadkFGKNq;!phr2284%@`GqLU}6&`~=`* z=FXbQoTSG;r*c=QUMkTHrjZ&DVEsku(x@5S>q{q|$WTXTH_^outraXk5t53I&p!!Y z(%t&ASQ~(f)d|@;eQD}o9a&xBgq@ykxtrN+alYBV;flPWQ+e0GyreprND_Ij&AwMZ zP=%{BShdojz+SStQsFR4R9)c+@z(mYyHTZ@E+nb){>ir&8?|IxFg&@Y$?H!PoV=h-$BdqHh07e4)iPb+#I3^&{b=sU0tfx86#WUlO2~Md zDm)elPwM{l29orf*1NV$SEI%<`e1Hl_N7ZZ`jLt$9Y+nS`kYTSf^DuFv!k zYp83;PP4~1l^tUx*%*sL`l zRjX%7Rn(3>WD!@BNnS0xy#mmV)#*T;yMZ$Zmkyp^x0ks_MzgalSDkz-O+hB1FRg48 zOI$nZSDl>Rik^)wb;&3p;-02Q3(*Z=`Y_eRIfhFEDhj~5L0ShN5UWSATb{GU8+1a` zMB@v$!Tpbd^{vy3ZliXq<9^NqzTD(hhlg_{vL82q?5X5eH(xK;pkG++L(o>{jtT%t)guSzd4>qpDOv!K0jIp&Rz#cFTafSYJohh$imgtVKJC+0NOR{r79+K-=rI$o6n!dQ~GhyGGd zXm~<6O9U8-!0-LDb;tM3=P zb6C%v&wc*;6MVn!9?dqs+gG-`-aGQ0-j8n@%7-?r@B3b_@8@l(Ug2y7T7r7}$5wdg zz#`^GMm;`gEKBPC3~=%L7q3Ko8{ct6aF@DJfjR)GSwSf9UqXyQ!S51Z_>Z$t+W5lA z7z3b|`7A(q(8^}kRwI+3uYS{@HL?9rC>#M=6te#RUcwyEpUV`m^vJjbI^TJ#kj7>7 zli76J=I@EWmrs;WD=K`pJB3VkeKjU?t(VUBXHqlW`6nlGFYgJL6QREB1&;fkR5CHO z%U$kLvy#D|NE3n70D ztggTG<&f1O>vF{MX{_j)NoksyeUfOnAElF@rcP=UW}s=c3H$WRg@lG)&-)Pgs56F_ z$aY}&{xAb-`9W@unE}1t;JcTlfNh^|_M#bp@4I(k2JkiUm(KY)j~{4`w)*5RjART)Swk2Iu|ZO{)u0WE|~t;{^iHSQ2y$4=eeB%l@?{NYP)$c zf*Pq&O8QDDDF+3vxS{D)O}`+f<@acDS+dX%wU`(Y+-{TA3=!v0Eg6HO$cIpN^3s*d zz+YYy1%0PvanD>j0ApCBt!acYR-kY>eET%902*q$odg%DxHQpl!DliBQ?Ra+O=uKi z6b+4lJpf+a;!NWo)DB?;_ZZ;r27?o=*`FKfW+vpJTMOVM4by<8XD|{j%!|*~sGR0U zZth2(>%qk*g1Eg+cmRK(SJUnfuN$!1s4dA-7y0{XhHG6j(5P;Ik?k`emu-YjzKi5- zs7@g7aR~i|83!|L0U-&{Of6EyMP1)uNAtXiP(Y+lg-Uh4k72IXqP%sYiCru60I5Xy zLM>#@Blqywdr&(?Y=QUoS&&$yN5pOP&@o|+`9v|K1Yglh4T9^UCZpui%8(htUXT)X zK!g@;O4$#A;+W}{4NSn>5GZC8SRasK)q`Xjs<}#%z$0`RHcD}v&@z603T0seXafU9 zalsJ7pg2V#k#S*2uTeDYNV!(jr0RRAT@Vjlbxd+NlBkKvtFiEidhh9L6h*vSh`=J0 zgfoL7do-}-a->=b1i?%}aNGsf9W}G@9S&&6DFR(pA7ry@IX&hN9dy6KWa!OdOPdo9 z(u*bQQa^n@HUeE|2H$63q->cmNSG7DQ5B^7XjX!}vBygNl}ze)y5OQBfM4 zaGUk)xWk+hn~0pLD8!0bagdbrs+!8q{4R;@?_8kRECl6gk*&Rt=vC-K9gskldJ>|L zE;Qx2h_nO&@{K}LDeGTgtk=6tegZ$viAyGev6v1k+bB#sJVLCIo2c%LV|M<7? zhSOFVFRSe;ibPhI#egoVx#LMDmC^JoFo0HL{T?YapavH;6Tu6QM;I=)*XBtNYbm&R z5Hv*VCD|vAS5c|pl$tGQ1%%@Tb}go>V4V28$0S3GIQ83C(_(xSF#Ge~j7U95tpTl@ zTnLQ)`$75`}he4of9D zWFfxRY&Ae`a5JW!g)6S#{@j7g&Yt1pn(c)=|?37T%0{@ z==bjp@!hbITFV+RRcC#|yJ!oFxFcd^_vbMfGtLDf&O{w15YJ{tnm5=j#K`x7&|LLI zL2wzT7AO*ET<6XKkBt-sKU1pCEVrf=jwu+2n*9t&Bj`e#_O+=lFI*g^bk1a`&i4pf zLfW6W_s!Tp1JG=%+Ay$rDtzmacqNbOdgsvk675Kom1Lqsba zQE2O#W&*UOQ*}&~e-6vqi&Qy9CrFmofkX?DJPIc2^-4G0{nz0L5Q~Ku$}Ym5R9i-c zo7kmdDU_IMCg61rLW-&lWunM_m+2@b@)wNY+!e4uOR#*N zSa47W}Kqr zn_OCDf*y?THi^mD4^!i&}VCTGc(q!iK*DT~~=f zDwyi@oiV!FS78t$Om5b-5sfRvX_9kGk2uub0i2p)-2JVW_vPuSV%@=F#13X3=F<=k zyD41fF-%zMN_O4$1)!HwoN}_iBz|xJc1&iZi z9x6cTh|glU1>+La)dPgQWq1s>qpEbjQG*tRcQ}g7L`I8bII7--arF@f4u-0h$n8y3 zl1()nRdfQVIw4#gi^~|K$!3W^JWx~L6RFH}W_awuYY+~<89P3{Ds$vajM*njZv%jI z@MC`rgTTv?W8CbTU;u++kY#%;BTE7`fF@(Ry_r4jYunJSV$xYvXC^fq2@B&lg+23h zlmr>KAfIZzJf`bwUhBeN1kp}$pnc6Gg-+fg+BqCjb5R|CkyN$^>FmjF^Jgirg8tig z^g!4kZ|q>1m@EV07empSpU!csz{U-+lluHk#FMjctH`Ep*jyUniZPjEg}b~Yn zOw9U>oTpeD#14Roy5ooHGe^nIjjEJlxCXC+4vXD}8j61WfgoVjCxSq^?MnaCU4RA; z?o1bVT11w~b~%y!R)rd5xI{o>YhKN- zm}LG$Xf@|}KchRL&H9D^=9CNuZf`{Vl-Cv2>tJ z&>DJ}GfGYd)nEVqyfrvYuSF&%4Lj}9qldq1;Mr2xpm}C5Lsy8>D5rgC*D5iaP z;!x}j@s~*ws1z2Uz{zI@2T6e6$)_2wxQ9{vw}DYkWnNcr+%b2+FzF(S)ROyfk`d5vEzu=Q49Gp*o2C!(yOTyFd{Xq!52VIpSHmu3 zBjO9_(!_Ni*T*yRaV`(EpgiggD2k4kj%52X-HDh(P9wunD-aL*`nu z7^jgfN7%E@tw%(SA;X88S&l+= z+EfCA7fUtt8WzrxQD`O{P@I9EHc{v(jo@RLlS(6VGSlS@!oxGUSt@7tTv zh*Y16+I3*>F0yM#oCznfaJqzh{~N`Qxw!%I7`P!Gx$2v2pv?ZTs8KKxN6kk-13NVl z6(>`(L>-_9#Me--&hrL@Y^St3Vl_2_+M-kL#9$9f#8HW%o<82eQjg+8{oC=H4fSYj zju6n!K?CnZSH_qMgTAMaISorqRQ(X-l|GADmHmsbr2F#3X<#RqL>vAaYu^}MS<{7i zW81cEvt!$~ZQfWnwrzEcPSUY$ci1sINvETpyze*j%~~__bN-xltIk>H?ECDhU3(Yy zvvxvg<0CiP4!WBiWma7E1HZVvuXi*R z$qf(|1of_0M*oVIj@K7eK=x;a^O4SKijXZoSfktnu?h5Z@FoPbW?^Z3c-oCzB4#}v zCjk(cRi~cIUXfTaNMKrEQ90l+b-7+esSZ4ff#k~{Vh*tpZKn%Ag_@+aKc`b{A`~5B zHOu;{;Go-n!V)%SOd<#^;>up`i(v}V%HuOoV8cgQ1B+->t#zK#C~5b1!fD`C6Szs%^EQkAV}TECRE#LQ`YL_?@e2e#Tc2qo8gU`XK1%ZElnwGr{1651GyYY*lWp)RjLkoma!JM?sfopK3}Wa)-o)Q=GPG+N@* z@J|BSQZbu#CMG`EREx;TjL(Er(KiT5v~;vR%RHLj#M|ELqwB9MIQXUqk?tZctCk> zk)sMx)o#Mtp)bNF4MH`u=sY87I@Ys4U*IGVHO7@%!s-bD>=;8<`Vdu<6*1^#s!hX8G&rLrdWdvUdR-M&le$Rh zZhDW5Iml&gTx%(FRtU=(!0c+SjLLh{cslQyvenKgM}QlJTfIAYiE`br=NBa~4t?8z zE8=)z5R>=Sg6z9sko+dPs@vJ8QM*>EaIR^Ogm=;?x_==YUiE6$9jG7W4#$-WFb1$S z;BH98v?it<%08QV!NMLh+`zGOb7OnWN(<;G$z@!MKh2O6PolLWHk$*WOzfF0n;=sO}?#{j1(iYq<0@0auTzRrvc%M zsM&1maNT zkp*~2wMjqIO5cdylJ!d?6f&lJATCw7SOXhBN3MGtkW#FAJi!%DzcP7~=#BTVa*v&k ziHJ+LK@f4>xXrMm)gD<&Q0%tSTg(M{rXan*LHFsr1|sOVdiy-Y(RQRCH@i=J1g?K# z0ugJI)NLD6Y0#`)#&9;BkI_o9JCC?Rp=QV|L6I#GOYZujT*4Q)gWQzru2dJR>Lrm& zp3GslU;R*Z7*i=4Z7Gugrc)>ycN{&?Vwa&tf3SRaaf?2s#!qJ%Z-5Hw-vs7HX%S*LDKZY!w zm2nS^@2V{0rQ~;h26xY@R|ER2nB+FVXt5V@-UqCw>^u7DEB0;o>Uz?gnS7h?MGs6r zt&xvtKkeTm8?avo=8nL8FjisF?oVvx&yfJBoqUqxMV*EvB060$_@fd?z$|rX2$YG= z4~jcU_xwEfH}3i20$jb=oc-7;50&3vN+NCO(@?v~`6tFFqkodiL~xW*d&IUjlZXBM z;PuKC+v7E*N0Xk@886k)t73B{!bBh(gBTWJBS)XF^l-6^B&WgiNoxRMVo4M9L~27O z_{-!0c=aAb@1=1@nWbmZLrT0CFNYoC$J(eL5ZD@Zy7VMnEI1}chcd>ETKICQaReZb zq0&D_%Ie5YflZXmyn+Xk2nIfj&bVN<2(7FYPWGq9D_e1#GuKWVq(Z?w^(EePI#u)!czdS(bYNB#w{ci0Dxw z$axC4k^4eZmIFU?35f6Q-<{>AC(#o8yy}54LeB^E2&1yxKOc#XF593~9TyPZ5yVW( zdg1Gj%gvH$W(;dC?Kfo+RC)ex9%Upb=MSSWlag5cMa$i4vHjJv!-s{{_L{LVJhqki z>s&>++!VES3Ke>F@4nYt{U)`o8FrVcwMhfR-mgFk#p!vi+2xpBrLsCd z3yOY4ZPFg{3c`1FJIUdfO9h5D^ol&F8Q!-Yr63x4onT%(PqTh#zzD>rJ^`V5O2NQtScLUDdzPEHJ7)ozt@~u9 zW&@~khytTLiy{G|0{v80QJ~lm`{A`T$hgsdchhN($h^hWtb0=O)yS+YOnDhu#mW+S zQ3*Q)Q-s-x8@#2j1LI6tPJBWB~}wG919=m*C>KLpMDaZVlT7C#Tu)AKDeewzgi z+-~+_1Jt~ht4&bkr-r})=@i-lWs8z)ynJKaNGzRy@BouzG&BtRX)W{v2hDs(GmB6< z0Y4_XGLnM^nzQhAbnQ2qvlcb^ga+hU$M`T&)Q7FjsLIm8tt1pn!%LV|DYkgSXU!Gf zZ=x0M1ch12^;Fb>5m>nMbM4ES309HluLT!0WpUh(&h*M-A?}XFNiO7m_yj<8vtA8c z2+fH!^)@J8Dl^Dcd+SP(lAL%Uz&Ot^0GI~n*$TjJmFr;#R%!v}0HnWfyHJ5T*|%0y z4YC!0I{^T#tf)nt0~EjXAUNqbSs@KDNqb_LDZXMsHquQV)38m2`BSTH>ooHAve4GA z7QRf!Yn}Pp=x0v`&95dATG(_=BXlJR0PF}dZ$2H}n8mopY~#|o6N4D7^J^92))Ze>pNOtgR4Bb%zY zXS%@dOiB2rEj@XTen@Dn5sntnUz+)mYvob}+nvF~J`@?noF^PxtD_GX&YK2Y#hy=* z!NC5#)s zZ+slJVBMZu5F)$>y8$3RHVz06*e>)bM_t^{Y?g`#ojk3>$%CeMbtJH(0Qh*0gAhzq z>Eg`2AQJ8zKJe~sO7aU}L(Lf>09S+R7syccolD+vcPygj6&_^$xaoptFs!HzJK1!>QVv7ss}RhLLJM zadxCU1BNaeI~)#v9NG};2039BX)?r7WG!$K;_d~}>AGb~1#;Tz3i}C-8Sh-Q6$H*> z8SY1)qAwrFC7aHBgD7z%tbcw+D2abAHH)}6swqv=A1@?Wo`8=^GlS@2{}W0N;=T?3 z@1VxBwcEYCDks(g`d{ST0O59YGYheIYAsRr6$g7Jpa=el{XCmUFg6vr5!KGRPzN|) z+l(F%P!A>_Xx&D#JX%N8%_T}>&9!IkfXXlE?<01K8p{+0HNCr0{nSZ}@s;#S0N4); zfhrA%vwzzf;D^vzd??CJ4)xu@5l1E$bk;JFF^%??LJrDD5lzSmjslFkZ0*x9sNQ-4 zVubDFI)!uv{LJJW>c1a_sQk5J7|c|FvW&n6nuu%{)!C?|rp%&pPHq_$_o;Sgp-Xpc z=iw(uyR!@vrQt3Z-et5GEmu((c_^5(rH0x_XilINlEo3=8(WS+%o{OL&W>Qv0OAFR zXhW8xZ|_VF6d>z$<1OOH*CziUsLvX&K$PN`)oPC{n>9BJQ$o*_5mW>Y55vpo5V%j4 z00$XQc1>ojRmEqZ*Y5Sf?A%+w7M^%Ce_hT5!MZmdd)2LamAH8mNWF50V-dILfWZx@ zK~8IZz-wV&5s6CTZTK^YRwKiN$2#SJRh<$jU8&^5(f5qqhpH?nW@(9rzUkbeL)kqY z3_QS?j5vmbrSAb&F^VKRK#^?yBlY-|S3WW6!27`1fXFVaC^>Km16uQZP&q@`RM z4hhPT$z(bt*=`$tLRq*^6-@}EdW5lGP;~NmmwL^;EZNa$W{h%oKUaI`YIs1s+;>*Z zmlC=J5;Fvy@~_}6X^&)g=XmG50!`rC!$J5^b{L`B&7aFB*9xbX9!^kI4W6bVb9DL6 zpKB**6@%rP0$TP03-)NShPynFSj(#DYSfqsas3&tLw)aFn^eXixXyiW7r^%E{UF7j zl+c;-RcAgNkAq%-*dFWiAE|NE7-7?hu{9DPmNW!7P*VSrMj|eb28_9(L5uwqdUL;Q z9pf~A07)VZ)vr#x<6F#{DKU9`9B+W7{N!;7J4X?T;9Bmdq)C_}vO6!#=NDwPJs4u;9b6mO0Ernk z)iQA2cWE15<$Pf1aX6)Vco2)vBE<+`>r9K8c+ zFGSV&*t$O0B#AIU1M^4#T*Cv|(=O^9q_Y2XEv{lpz!naFVYG`!&zRiG!BnjyN3Qu4 z>cL+U0RHv#r-rMT!i>-EVL{rJOUwi-q1yq~Bp`lLX0yUo>^W;kP}R+tHMTUZlLS9W z-7757pg}3(H?Rn!jXklmJspU}-9)XHDlkgRupGoYl_gQ{Fa>@hBa@vBVy@9pagu=J z+mz@~oLE>#ELHVwOUlxZuRT|#QZcyh%nwL3riEQe=LN zXTX}N%CcEW*xH=!j*4L;2uE@^GJ@s4FjeQAT1_&Jqi4&rw7Yh1uJH#VwcXBxZ!HXt zZi}=Hc}cCat_C!LFXCZ8g*Zp&w(2;4P-sKC4z16?ROM#&StcD{KPx|>8JM#|pLS-~ zFOoCGDxD{zQ;evvCeVa14Ow0PB~6G~B~)N0_IhTp8P7I+Qis7_b*)|RnM zZAj9{CqOS;uH)^}uaC>m!s75hgEC8jW5f6ofGHZsT&=pe+(5E&FlN0BTY~lJyYY1> zW@Fu%La7_k0k?0N(d5mU<00|w&HA*WjI;OyVk-EkN43)3Vq%5AC?Z~`&}fO?f9?U? z(aMfRD{)6t@5o)6SP%@7jzusFW_{(;tYds6Dpf76OVkulvlmdNYZ*w&5L0_WUn-79 zOG2pr>Mj2ksfV{9xeJDT#BwPH_#F-N%GbdKT{Qkuuo=Qnp^_CGOqNd00WM=YxgbCm zA+13IfNWtW&FH-G2QM$72x)?1IPvSZ70^3K%$m5b&oSc#B+z#zAu@P6H{Gu(l4N9{ z1_i`A(M_!VqPKitP0eGY0yV@PH97!9t|z{|Lk3O8y!eFCpP+pQ%KHp?h|Mx9`$=tNYt!40+q=IBMAv{F1d0-@d zEc%0+8I;9uScX$h_|q2}0ZMkv_S3D2Z_VIUX>re3HLX6M(<9!X$WZ#Qf(w|k$q}#H zRj>1uKw$e2kby_i?2u)6wiR?~=C+uTG<>1!g$--;?MVwbdyWjGbo!G3q@=3qV^68{ zqMVDSj6`DA5~T+)E;~n4{-In^USJ)3I*lPWmO6CX^p`oBl5?q*W$0IySze5$&n7?4 zIQ2A~Tu-RLB`NhmKao~vlNO}wEYDl~WwX(d+9jqyb^FlIl4g<>0c!EkrX8rf`-yfS zufoWvw4*CmyPn)Evmi@U9m=|&gK?R)|WKu`%bL9}Wp>LJ+jR_Q9oyIuXnd=DW++L}qq zo5};3?aF(;r#G)U#hgNnQ=eQ(Gg+=qDv4li54z3if~GNu0l32lVxzy_AVHunTy@yOQoBR}tn(-i zpLTOmX@z1iOdgp)-EmGnTPV?jd;++F5`UxtQl{F?je^odHmbzzAjdF$oA_uo?`Y@9Bt|(!K=WD`0(~7i#2rPxLYXC^`3?iN-9obG-YfzjCTi_t z_o?){orNXd{3%c1ywXFyWRJ7lOpT%re& z=tcFBEYO6byXZld+1#ogyGyVr&86H3L`y#P7wTQ|B*ADrWHyFrs1| z0^zr~Q@4=Hd%42^MW;<(Q{JIgB4sw|c0eKdu9$2Z_7e7PBow(q<`fH<`bo(@jii?D zH=%A@NQL0iameM$r*}`iI!S@l&*y+VS@xQ3C08C!N~{pg7utrF>jiVH?l?d~qZR@E z-3h>NmA6Z$k5pvyDM$$;{DRNQyx7AOplheDN=!)YBX=D5Q@8o>JgYz=y-Jc`8 zVkiriXbdgYzGA?^z0&wT-3|=tSRmA=BS>lE0}drL3M7WPNYh*8u+vmzlZ;&YT+mfy zXZ<1ymyw~_^C8Qa>|3%;6jBhnSm$~m?`R4@>#K5o)cFyb@|hFmj1rdh*hxSx$0E9C z*F2iU6H0TJa@a+hDH0Zyp*n*kZN6y->BB%#c{JuhuF}IxtceGKu46&gA@;(&1%ExK zpi%QavpwZU7ucJuBDSZ48x%*+!nPXqid1y0JE~&MySDJk<>=Dhe9^L_>}P(Mu}Sh% z$dgN=M}r&))cal2tp@)CB;8KhWLjElyW#m^T1Pt$N!_I~R}c8Ly;&r-lOW4D+_SX% z(}oQtiA9cG=+jl`z(VInM8#?9;yXC_jaB%(1HlNNg)`-gvqnt!* zm6Dm0NTo&i7zxWG6hl=t%Z&T`N!x^NgljzOfu=73!!4cj_%aj1vIjS0W?1U*tT-9Y zl!;R)mdWq2Wm|XY?O0Rf9iZtAmo}}316nc|_`c1Dbiai@fhLE0>GRg2|%e&XHe* zCW6!?y&>HCRt!gPrZ#5#)eCjx=pL05tS2)CXH_n{oXXZb#TnrbWoWsRhx-onyfbL! zJqd_FWf(Slo4!@HB9LXPE6dq7@<$nYbsvWQY!QIEL0JayEkhDaQ?^SBnfs%Kwm4U$ z+h`+bwqitsN`17g&jgBD@}}^ja;>5qtCI^X>K$Se=x+<*B;T)^5|YiUsd7nj==GK4 zP|mxx6UlY#&tzx*gYq@Q_QtGUil!JIv*XvEHn<)LCp8#=+`FlD!JTEuKAVSI$uE_Y z!S&EgRUaN&=Q<+W6@ey-cJ6|2$un@I+dvjQs*G^c5L-l~-EBlTV~?(8(&VK|!q}b@ zNIO8XynH%2KDWCTFB>HQw<0%s%#4y!@C)4%<$!6+IbOU6J^ce@{oxzj@tlxon&6$g zoWJaLj8|DUruEhM8a`ZFd%Kna(`1$wS{ucBoF54M)L20VXn?6dHKRyob%#b54G>7} zdMCu`897YMdi+itD11xg7q<}o>BFq8Y;-+t-4H&AsNo-u|5JL&>q;HQ9~l-zXe z)d4WLP{-qDS!G(zPu#F_DWM_@>y!uxN0mYY^ozt6gTo}sQubLAlU?DBEZ49VO@BFv zNKY@igg@3Xz&ZUDuQ?A|f=U;knC%?n0FtgY#pR1ZvW+QZd;EaXVCT9-gr36>s6pfy zfloK`PJp3Qo~bHMt-z)!@v103U7Zc6&lR=(2FJS=B2q15hYgf0p#@GImnoXyL!BR^ zw;=ozJ#%jEf1b~de=>j^rgrt`P}{t;Lu`fem-9t z$Ce26^(}5^DO3VsuQv2`Sui>gw)Ekw*QSM55eAx zM#%VQNh6-43`g+HW|BSI`3V71n&y?AaY^7vTND30}G?(xT()Dgyb{Z-E~zK_ax zB=E4xViw5y9SUc#&g5-w$B%u>3G5g$9^n@7w4*>E3?NM!;W+8wxDoM~4p~TJumy@d z)j)`ydA_xnisN^DQAGJY(bx3$4IPJjFdJEYelIhS-MwDTL+qqtw7kc(#;CR7b$111 z3-t*GKE2df#*jcf$q6jxu`J#9^Z4gOOgL8yhnWECHTd}nguT(2BL>kQaM^7ph0m4x z?tRy)cuy(B2&VO9$aF-BtkCBq`yo=konNdmfL(9=(DcWDu z-lQGk>ttBH^bcH>-=jC>g#nlV2n>>M}$j@rZzfMNv94t%Frj7p#%WY&4EwRq@EX$kQ4MoOre~>zgt=QUY!zERciw)>FbHk-_67*DT*kuxGhCSFC**$7@Q&z4FopWB~ zdSxOssFWF!7IX&$+*$(phip{IY--E2qA{E`Z@&$$NhBcCM zhs5X`3=J*?+@#9L)8WM^lj~7a3NoDat9W42_$kYOC8(-f2;U8m@>%Y|i~=#SLBS9o z`!=G1(CEEOMstJ4$kheRw8t=5kpW0y??}&>Y_7)M%xNJo-@&0gI%J*82&<4&i&eKI zn^ZkF-NJgHC05UgyY2j6akNj0o9G|q?2d~R5{$`2>^QcUSWYX-hOZnv^i>)uZB3ZH z@k6%WYw)$1`Y-u6=kS~?-@p0+jOo3>Ith$Td>Zmg6MPJTt`GJ*2RG2^b zNQx2Ip+gYYPGYc5)n6l@yY@7%Bwf7uTTp2I){K5Vbz~38BOdEnGHN!HtJzEaCTHGP zjBNfv$w07g^Bi~A2%TtRKmqFP-39_3ye~10>qJq!mGymm&&pu?$Op%9zFhB9K{sE4 z1CRlSw|+Yr?TY!%xAnG7uOvYJ%aBYHLMbM6<^jze{~tnKPk_MD0|R$G?wVJBtXE zl0~Xb)_}?<7lJ5Caga(i7^r=sJcDXfp4hTfhQ)^aQ#>;lVSwO)lgy_E)y}Z$QKmIFUEU!Rd36&@ z&=b{oI{5)p{Ebe!Su17~$Cn>XupG8ci_=P@R?>WaDQ=HW>Xwtjrv}Y#$EGKso-kfa zSpd8$%8;}#BYCa1S&>q7ONZ3Q;d1={mT-L%PhY_N|0N(OKd-5U!|Q*`mkJdc>;VVO z7gj(&4!$qrH%W>RPH|Qp#)z6wR*Bjy%9~Ok2dPYB5#sW(y#&v;@>z@XZ>`n8k9VU9 z9>W%Rjf0yO*y2Ptmg_$r-1X}Ie-cZVG*3(!>+{o;LY7HTgUJueV%V_Ki|Vktq=cVa zU;n*Po<#JIyb(TSpOWD=hkZV~@^2Mg)aPHI{0A@A_hZ)_d6tUQEGY3YDI}b*G3qmC z`>ubq+Gl#6VU^sS6i7k-E2B}hR_y51nx3fv;>nzSuT76y zJ#PGf9tVC{n;j>#(9Wl{zpmQ6ICopL61R$UNneb=A?;H$|8bl7d=r6yIfk{vIcO%Y zsf(lCWi(@Uy@jLQZFF^ZeH8DrnYGWZ&*E(fn8R3Ko`QP$&1tMJFF}TRO+40@J9w`y zqvE&Fg*~4jhE^gBs7LPEez{#H^LarPjDY=DywFOF0rl8DTglsHavwi5!4cR3D@bLo z&`N><^@KfJX~5%GgyFTEwcoXe$lpXybtq)AkItK7IJiAWt!CG^@$`Ak<*hF_d{5Fj z+Pyx!4ne)V=K9tSC!nkRrZ5gKk5Qw1Z(%-j>l0^|+4WqGr)y9kuX(fRZ6a|&&p;E8 z&V{%;%)WvYHtb29py$6T#uWcl@n?syN?J^PNc${UMKYq!7X91k8joy03keqW1T&=N zG-tM-8vdE>74p1Ert$sU)*c6zm_WeoKX5JT2yOfquwO(B)(%%7$Gj#SYljQaRi5|8 ze-ZoP>NYw%yFP;F?>@Sl--L2vXc00`(p#LY{Ha-ZBw1w~3z&geN`!C@o!T$vfz^-2 zY&r}Q=3iH%M;TV=O|fCEbK2B6DTV4krT6`#$AaIe!65Z{8#HQCkN>G=o@wy?&)!Rf zG@&Qt6t)6>H}w1a`_mRls`1D3@yX}u&;NC1t(i_nLHSXUQEqOUL2F!Ak)cVGeV#*G zjdoID4%j4rkI-LIAqOw}hl=V?7psw!otc~+TC{atSyD!!G5F!>*2Zb(363R>by-Em z`ak>{|M$t#CP}1AgU^)u;AH=B^Zm<>;9+CwXvy;L^@a)KhyDsSFTOha&S07lONa zgQQ8)OI|Ud%EuuQM~RiAAC&V_qf#cF*0N^x9oPtgJZdc%>VPcc@PFh*1|K4J+uFtB;f}faj2W z0G@cHM-b~7@qxO|pjHF_7;=eIluk+Xs&@mV1Y4t7D|2Po;DDee4u{y=i zWN9+-EfP&*;1 z^>jhV2KihQ#`&z#d1E{Np)ri7NFlg0f+YTBPZGvi6i;|_P`cO4dGDO32=T}IAP>Wi z*t*99fy%UvUvor*M7&>c?35XSg}u+@W(iihY48|6NDrNN*}xfY)0`K|Wf$e*b(%)1opDXZL}nr`+o zTaiZ+^AYEe9b)z-Vn)Kj9|s%CECuhcBRIxAK2L$d9jeFUSBD->Q}0iWjelPg=N5v4 z-cFo8m-^Kk}r_2BN-xSG10a8c2i+#eKd$%nN*UkdoCR%XncpL zq>v1mcL(oUI*oTfNC-_3(ydLA9$g~11+zeBmwbCCk%43q*4Oi!Y)sWInn7O}{lCPdWNh(M0f>FgqzEVfASA?Xg%gYfUD3nw*b`hLe&*A+~I z0Lc{f8%oYV$2<3i%a{)l%f#=%uJF*Y#%Dvhe8aTn;D-K|5rbjHU-r_QA>4?0ENFaT`I5EQg{Mt3{uEtX8>8B zZ`lbZ^R3kiKirn^4Tf*ooCWzhyZWV|;Tk@~H3KjYp;}{VDw_B*yUx2L5SWYoEg}^! z(({%a{Zyuol9XGJZQ5$S8AP;84~5m$3HMzvkO2JZ5vsOqMfz3d!F1soHuKH6D=d6sPk}-|GLN;nRy^6aHaKT zIofR)zRafl&8(LIo+GFJH=4a7?MY`yIk);(%zV#KDWOpPEI1>NP&|*see>qeO|&ma zh%I~uOA2Y@+;3Z~Q{pmQ{p_=ZPBJv+Vd3=TsHL(r^HV=8AmovvaJOo>NW)Q9JGtj4 zqpS!acaYe#XT~KqOgp4nD_f*ZoShga=`P7YfY@gPdR2S zuN#LKm2oL{f^eyIWQ((oPk_Wz@!N1{|Gk8H)DLd!@HGr)|NF4*KXofG!`-8tG0qL^ zSGmqXZ@A;;0Ph(2iZVE=ZA%11Nn;-6tWE%QLw0#Uc_)R$BT8W*n#&Iv@at!zx zgMK=6Z6!{<%kd?Cp17I}VMbSUgpBk`HL6~{BB2+E?UO8jW}a6igPZ%By14gcvvcF# zn%TAu9<=RozMV>A!k)HBIW9MJ_{Xb#+a*dL{7g(@_nV-1v1q{=St=}O*)FjF`g9pM zFC71e_E@n=wPZs$t~%Tog-oD!#@$e3^wdry`ziTTkI{*^q1+bCnYa(Ai(N4 z;O9@Cu0L|;07F=WWLc_(!M4sQ_plfbY^kCthZyL4{Z3s*QY~>S?CUalu@k=sBt<1H zzB5d#2RLa{+)ZO^44y*8=lFA?zIzhP3I=0?LT|~Fzy%VBE#}uPW~Z0t#NbEEwJr?1 zv4*Y9+rtL>OdGRi**er&^;Rqb5@ba3(SElUo6B?+BHI` zebM5gJ#~H{MdpYwyg4utH*S%fvqqfJBM_Br3dxZM>b+|x>1lf>+{d4fsLr#6(ENki zQ~ZUJiqF>CIIoudGo@3_OHXQSUX}o=toW$oO|>`ZTOvu4eP~TYuTb59Qw~;I$}S!O zuit_!&rtSG1!Cagclj6V?*n?Y^dqcUNjAyqeSQN!Y|x9Lj zHOr$hSM9Y#$k0_I=FGA;Ghiom*x2qR(P?^nz8x_4YO+0l$0&1fC(xtlw=Dv`3sN>ro&t^ zG%VKR%@j-~^&9d|o_!nGg24_FxO=nK{!c1ODZmtxfsN!aT7ye`_pnjpdr`M6HwXap zaMVpEXV!q1J-0e(d3Lok2aa+eIUV~_EE^F)rN&QSRtu-A?~b7OfEGibIHcWQ(a}@B z57rj>Lx3cqQqfa9g@Fo06qXo3&COIy7e~FwTrT5l7+n${lkZ){R6a35>vyI53w5L( z-8b`_V6fMN8Hq5eGwJ{{2WccD8ribYMYG$+g?w${x zx8{kI|2aqi4Bvc+#CjeE1}Hoq?gQmY>jod@9de`y%ZB6M^*hv5i4dG6oi{m8)RD%=aIN_hcg{s0j76{f3k}c_50s!mQ|J)X7gP7 zL61=#qQ_J8f_lwpe_@d(L(Y@GS0 zjr$~G`wuyifKhFMV~Z$fWSe>yE)ib-(qgppoquED^bwIbzO3@V_$ zK2_i}#Yrb}4E+rhaTg3MuM$qCQfW8oCdRZ{Fo(YP27gau+mj+v~{BYQTbg@7Q{a zdX-|(k0Ytw?R9PXYnC zKF60GM&mTk&2=l(am*8+wV)K zhgA!<#NPO0mH9@cKZ3fvH-lD0Y`<^6XX|x%{r=KphH&jipxM~o=6`iu!A7lrTD*ho z*h_g=bt zHa9T4e?e3b_?THZ*z4(M^H*(`jV-))5W^hIh2sDuNsjf&z#7cOX+qYO_y=x#dKx{N zE(Z-mMV(kByt8v{=jt)%N;LcvAcx<6JMD!UE6Psruh!-2VlXfvs>PNmQ}WX*GvB)- zx|u!DrQdnMrVbF8*gaElUFlN8OjzC2jk_2ohL1@5#|;%IXYO+19EQy;a{S!FDDiA0 zP_Bs65-&MbIh9g+wZF_0B`zODO4>+4;Nu_=FM^wa7ec($@qdA^bAs-$mtPxJo{mP)m3y#VZ%~L|>$=7eph%+w#N+cW^1MN9P zIST>C_sjKhdw84>^KVm{6x87Qk;I{9h(XHnqL>NM9A$)1<$hL(8$K)|jg7GM1gx1a zDZ=qJMxSbg*8w_s7xV8}SnoNJ*@O@D&MX%gQvW@*7OU!NPD*6goK21u5$ z#kZB5_LM<=a?A$Lx$Mc)7Nl<3XW-u4TQi40om!RxHZBiZ!fuER;vIRfr#O|q5*r;z z@Mct~nD|=}c9CE+ltx6H&7$8o+V6aP$j0UpX#Tw$LYCHx01M0V@ngl)s7@-0h+ET+s)DFM9X9>v1`ec z$V;0Yn=&SFzJ;=i4UrRD#(pEL4R3vip)zAgV>FX&9*Qws zG-uY``^T@kD`cb*L;CL@Z^izo%#-9+6BVGR>6l!+BS_hNoGT!Vwi^#-~~61Y)syU>M5^AcL7Hoa=zB{U!0s60TL-ic z-}}%Ru|gYPwvWPnCGHE$9x5{8?tsNjIS9IXQ_iECgUi}66^L`Y&NF=46|xiytb=Em zWnyfFlHQ%E(|#qvenSalPeCI*n?bU$No>Q!Vo8`|QTiCCzSwq&G-OJZ=oiT!{=%J` zxucF=L5rE*(pc;r$##J05F#MtJyVEf{YCa`qT@dH57|kzgfv<(1dPV-m%QA`xWl6f z6ns=Bw0FUzvMfW-P@Qj&nZdCd6ClMuP2n$L5k`***RYE8Vp^{fEwcuq`RUs6Xf$W) zCs*KmkT&s(Iu)4nh*$>T5}086{+K9x=`HGLgTc@pA)}UV=gI0yy!p;!Qq@dsv`q0Z zib7&O5Jq6z85qA)j6(}NUC9-A6+ZNB2ibnO#!c0?&)L0Rc0kKy73PQh3^2QdHdo*m zXS72c)5WHy>KSvFZUvzd#O+sxR{9|^TB77YblAJxw4a)zq3;?(L^i4PzlkowJKc>l zaqEeo8SF#azhmjWnFO8Z<0Nz7!Z9@ds&=eDumYb@A!l<#v;u6@Uwm@-Oomd*K2||D zG4Y;ftK1f}6p;qZ%qNZYcH~8*vcg@gw6(X>GiV%c&jt}U zf4I!t3;i4`iX$=?6ry{Bb#N3-XG;PXp{j%N#gjBX+AptI1Gof3V4oW(Xqj61u!Jt^NRW#QQ%Tm6vBJGN-Ju;WTK2G}7OPNV5*KW=SNFs&u~Sw1 zgi$^$xQd_w+`3V)Ssk=~j2An}Y!dk0ItofP{(Ko%+{iICEHOye*A7|cLt#}hwCL$N zjD8o0p5Z|=-H$YpPg(i(IVYd$9f`q{g9&XA<0XO5P0NL6ziuoki}xK&(S;^Km9&TWJGdx439ClmN_uvI0!gBYHN@Q|f?MMtR$G2)8yR%nSP6rx|l)zqwy|r9n#09!pyKD|k zgr4TupB)&m&(ZZoC@r!#u^p#V{H;}JF+03e=+KrC#7q8j82JJGL^jMC#5b(CL93OJiaoixz3V-nsAm|uOl>h@rs~h0R^hoJeNnqyu=aK* zpVA?>4mZeP#?o*2&CVa1df_&52!~@^ERQkEtsuI=avAmfWn}vorh$@lHlJLtDlk=j zmUoIay}wu0VqarRE>qp<`XS6Mscm|HF5=?RPwD07+>FG%|8W4cko6{v$#+|+RH#ki;2^M0FqlUqMoQW8f%lwgFh=D!b!OcW zGQ4hlJ5sbHEft)fdrT(^8o#CGOv!3$vq$8@$DF58?20CE)1H(|VWaR83h7&Chj ztjM%zyxApj>H&;UCwE4=V1#ZT|4Q_&50cISgN~%UI=#W1WI&sj){$(dl%MjfC1X6i zLBtl?7_s>LX1S}YpTJ3XY@?BMg1{!AC z9rY2=pV=x$SbFwX7LO?H=^jt*PixOuv$OPgupCEQu&?gbz)2`ywY42qD+D|~9b8w! z^0IwAz$M08_-mCHi}^tpwC_l!aL#bJ#eUK8xAmd z2>mtfzmg+D5&_QkUU`srtP-|g#BA-hS;=n{lFCZ${qjDBz*^yc)JdJ^F0ebXuI44P z?X?7D(d)LoQxFja;``*cPL8E?^xCcLs!$+X8Sx z9r2v83!+ELHV@RpL@#(*xvH9|iNGC?&+f1&Z7LEm6qTffs@b)Gp6V%-C(G{bZf`AIR3 z&hh7prK{w9^`}krZJ&aJ%zW{|w_(I3J>ylOdZHlU$YHvf*t3NwQWfbyLPWn%-2LXN--#Sc1~mxOsQj@v$#WJzOtU5Rm; z$d=An@u$9A;gD7udUNPWVQvVgNcS`1LN+f{;qzixK=0^rUfxKfc2o*2JQ*qEuoIK* zE6lx$2Fvnij6uK%;B2h^+^7S|C$$#f`eTB_S5VA^j=_s|($3LwOtFe*@Wecj^QXMe z&iyBYVqz26_CMs>Vz<7>r3lBr##=giDa&EyuJl3C+FW#KJ{mGgb*edi_(zy(`=-6S zF*Rr5MopI<9_v33Lw}M40s7?i@juaRKnnO&pl2Z00QC9xq#uQVOv^R^$DeBVUb}$v zdv5cOG;HF>+6Wph=YS5j7*!9}h>~c~@E!N>Hh(Cffq7T7>6f@Ym73iC%TtIWEGJlb zC;$Kgx_>`~_%}lLKiS-crj^|~JK9Hg?sZI@y}}77MUcNU15HeDuqmtUY}1Z`(P;NzaK9N1cWYZ1_cGWVZnxtIUhW z+%@}BISro5s-3+JA{35GJF3*f5&FvrIc7$N>!0e73|fE}Odto;H0@7ALR)m`QOrjT zjJk)N=6}dRrqzLA7t${cz=C>qDb}3QA07^%ttU@4zj% z&Gsx+G%##KB&O!6ZI&$8j&_|!a&%K?!fsRj)Qba`!A&n_VxUfpQ1!w0?WNaThKzr3 zmS?&Aeh9|N8d;5fyRIO}uBXmwc$Pdft#sOUx!3M?*TTG`F%i8hkjRRzjKY4QntQ&x z;mDd8H~tXCD6l>t#Bf2)Eh(^_Qs3+}0CGJ}pM%a^e%3HPd(>l#i4v zaiCFm(O<$-mA0SP7l}4CuR`3MZ0zeIIvyd_*vuX;ugsEc{&r3nPrYe`88b@?rrRng z{|t63iwT%q6jx*!Sq#wVJudo+_yg6gmVA6R#$2Nhdjbis=%lcDTy>;D>9e3(f6geC zG|xPK5SA=-XF>Xhk(tdh+=zSSisWY=eWuG0`pp^eJr6o0Rl0?86}c5f#>#NBozi*} z&%5HrfNDi!1f85lj_En@yhxCCOeU2|Wt-C@7evfCP5##uG*wNCg^OlL3Ps5HDf9!d zjMVhN8ro82ZeE*CQ)NDD*e}(WZ0JI53}}wxU9@YJv|;8q}BU3 z#-lFT>ep7Cv*%~AKu8L7_Bz8Z+C)cB4lV3KGWQv#y0l$L?=F@^}URrYH18S&X5k>S} z<9pOqGjT|y@j@$EvUq%uJ^u55R&VR1PwxsP@~9$dP3AL1-H+q&H(%<_{C#q)xH@L7VUc8MI33`9gtQhTGrLg=ZN<8FKvbt;ngI%MFf1za!r zObZdtKWotxfxSrDe8|q?^JN>aQ9yzczo*F^%m_&MZjUw$BXlb%u>9Zodk~DJ*CKs^`+_MXXq_*ir5UMS#p#*@TeC~){k0TDc(OVzXi8CI;rm#Cd+>ER6_Ce#dkta$TjqvQ9& zdG<5BHc6=-AkblG-)}T9mdigK$=K1MM(!>6HDcE@hAo6O$I(}DDYV4{dc+@*txsZ- zX~8Wdn}`(a2pY#GtIqmbg?m0L_Gj(+JUX=Tk7(^ z%&o;+PMG}aDd*s+K8?KkU)r2x&qw04qdb+iBQN%6GGDhwne7Xc%H{klra@b7bGf~& zlC!fX8#Hfi(heUm_4Z&M@qEv{kAC`Fym_;h_<;RgO~e%XnH_t{49?yM}K??UO~75S^Rk|Gf5%IJQvVkLPoFN zAL&@iX(BVZhiQ&>Hd?Q+;m#2sbVxXQ*GXuOI{>S2HOM#%l(Wle4?me{HGZO8N}2V(lSbHtzpq zM*5a3ICYKZs0^pMF?I=meD?*J^<4A09OVB8FYUp%e#x!~=}>A{1#=k}LY2)QC3y&4 zxU#2hVILCBm+C);{V$Dc*sPS~)J(!)ek(YJBoR2D-v?@KLKW)nBZ+QtII0 zt`rH`sKfpi;UOjCU;t}PN~yONFLrs<#+#=2@1>`S)-kp<*v z(=(ruKNc-RwajsbAkJ>L)SecIMu<(+vgT+ebX=nD4?o*QR2=W@vpZDk9%o(?H;pds zJ&t(?pW+AFX&qiPD;WfSoG30`7L1Fr`;g2Pa2&&q$)%lvIE`YSMad?_?h2+zt67is z^-#|jq>9u(imRfJMxH6?W0AM1e3=ZDd*A0by0g;tDKJIY2jU4E2=c( zeSRMS|Lq>o5!zx-sEb)BdB03<%H^NAB{&)F3&_ku3<OBzvG3sbBarK#mcOXZha9(ciG5o8tH#^c!H@XmB){J&3q=Ks*N@< zLoi*@h@O;epA*sbjzWmQSUqEs>01qzjtp2sg@bT5gzGcE1Ep@xac`R5!)Q(b|EBcg zkK}-l`7t4=bw!a6CcnLD#wNTF#&#j^(B|x0 zUfp6z1aRMifID<_AAbFcFTKp;I)McnNjfA;x9RC=-XkMf-ky zu>(Vg*S3jn8X-*j9taXy{aoFezUT1HQ_&&=fS<2Otmw9Z?L_YwGx5M6GrNW$3r(+Y z;JXamAqS`ksF{qKfMpZUzO-XIJKF{MwB>TulJ_)EufIL;3xkee(9;rA4kTL)h!Pmg zyYhqzGvC+sisJL0ErmFdomaq*?gvEd&K0dZSkJY);~MxlcgpUgGa%w|XPjouq+s1n z&puc)r?Nrsfva8gnb(Nnx}0{_i3L1p%mYXHVN)WDAVuO4g!f41B-T96tGnhxzy?fD z8CLc94V%b0W2!t4!d)4!o%bI6{(r@TBqss`EoVG2G68=txd|jt=`<_x-uGZ zRd>bVMZ!Nld+eQBste3G-$g9m@*ANtU$bAYdNQsq2OotbH{Is3>*^s@2aTszhkzOe zRL`s+7i@E`Dj75NblJTn+PvmZgZ7$FKWnhGfMAQns!UIZz?+og1kG%qS}bhyodBqI z-au^TyL1lBAroNRr;?P)6`NdnqSmqPC|J6V9Yyy&+(5}UmNSy>;XP|Iq0RgJGJ}p| zizA&cLkU&ceqVk5RlNV7xe^XaY1zj&XS4nO7WIEAdNH!IwsUm)Yeg$c60q&3M-cgi z^a9|3gkMeAUveG^N`~=W$TRwgzg}$bl`U^BM1a)C^>*I#;zvw3t0tbgL5kBP4kx<) zykoT#T3gBfv;qap%L4VoBauLaF}p~a+=<><)WBC}Xt>t&+t7UBl46d6gU`HzTy&2p zZfvd`Mo;>VN5{7BBh*>My%7lQaF zgZn;(S!YG_-Ol)c*JNq%SxJ%h2cWaOh^1LwpDK|lk?mmmId#F0LhK}y03|`<%Jp&m zwks-Cvom7RzRr9XBptXzU~f;m&B5;e`Z8I6(v|9xq;}|#3lrokm#V=sK0f5|wVs9^ zRJ3iSsW2t1kaE}RN0s$?Oy>7FKX09^6VcwK9U=Y5Ut}%1>hM)zqgGg0NZOGoH49Uj zLVatkyC^lg*Q|>(X?Y7am^Oq68+am)e5wVs_XC1jM7RCv+-K|8(LHxk5~UWM$!ey; zLOlWv`;7Ks+&9hSw;AEt|UBM44OKn(qH?HWJ*C75O<6TzyUG*N~> z^8AXhpcQDPgQ&r`bu)jrYo;-AV5_-A9Il-7Oo*0>P@N+c{Z?5uTu9X$?_%#pd3f+- z>9to&KVYW2dsLO{@Vi5499Bbpo_!PT);w;qj(x*4aHH0bGvPbyY-{?UHG>kxm%4jmW1M^wQ~lBI<0?K@Sbom>`U~%+)@>!tdf~ zYr)WE!%nwagyxM$!PFdVcx`PnT5AKDMT~(m^HEVoXS+&y{7j_!bMoLFJG$frQAfl7 zR{8 zhD{Rwu(b^KX&sxVzR;^H`68|B(Us&_{%HTxtv)A0@sAgkf@(~GbWj66lsWulBQ+2GoN#O`^Z78&M85QKsA9vVK)$EFy4di5nr&eEp1yTGP96S6%u|wtF`lz6zQ^p~Ey#NW#L2*#a43nYW^~FT#w?#R z4?%>_p_ugywO8v_WJORLQ<-TYwNX|)Hm3HoE9I(Umitnig;-CXdq8emHlEK?qQgK! zVn%R;sTOAw8(2OpP-r8OL3;C$p;U+guazHQb3Gd%fin{$LKo1NWRs@$r+DFtag!#4 zP)7tZL$)z2!&6q(e;9=fqR^oMAX1r>GOy9MR_A0GbfZ07wo+EmSOcP_)T*zN=W2`_ z5=g2{7d>D#53STLla-3hW(+|8;+PQq)dWg!#{sRF%+soxd6Pb%L$0Qo*6_o&QdgR* z6cr>|6xnLOr87%ZdTge1=#?D6*mQM+wiNZIW=`0RuVI1{)v z7BoN15N^=z@Qo2!%d^swUm@fV)k_SBWlF-u8gZ6Kd=Ib=@`Ue^;bET3;?t5&TP}q0 zDDxDO;lGewzM(`UFc(FL4w-1#FXVI1p}I6Ax1Wmroyi3?$nZ$p$9pwONMYTw5tkqk zXr2_sI;G?{iiA+KlvlO*F^Y8KKc+Dug^n%{)2GCfUoALln6QL>HDfQM>*;TNXK#Pb zvL9~g#@@mx9+n=mPYcR^G;%J5Nt~td&fVrHT%j7T-R6ia(2f4M*2sLeaOph8_+%x+ z|8S$Uv~48lD`s)74VHxBHTQZNN%WeLy@UnVYPc{v7BA}Ja02g&!#{Du78iJF^F86nSEYx4=prGo zJ!z`v`GlF^qsLG61_=knbq97(==TsGn4;NXqjyq6DYFR`5{0uveDMxb34waCNtFRb z@6Yj~wbICClL(eFLMqy0vL@vc5tST>zu32f8tKfFAO< z5$jIUi8nwgA8z)~FlT$*c}SW4_^^GM87v;G#`>6SJ!Z|cm&&I1xlkKE&__G^k`ZNo zij^H|%v>PG`-V{s&J6m@PwNKcr-1S?BK2oqI@Q@-g>NRPVjUHwBJEo7gcOlkC%dqL z8T^EhE{ypw$FPatpsug6&^CU|wd2uey!_^DqY1#^=Q3+UFtM~88p2og^1o$o&{ zj2l|VNAB2~%Cw_Nx8Yoy>a_Qf(-0ueGUx?Dt}U~aJLmui)lbhEos=%3Fu^Vrfw#~> zA^?Rf6{y@iCPp?3%z@v+{VdLSKy1Q5!X48OuE=9s9e-jX6d6NFFZ=b%MCOJ_5*U+y z10o<6kKcSdthu>HBET%Hjyy=ol1s>c59X^=1v$1QYx~0}^|e9NE0l~u>OFrbXICQn zJ;kopD~!zGLJsc&I|roi6SD-Ko;$yXmOFzd3oWL`ptWL)I|=&2>qBX4U2-VtI(Rdn z7Mluo?RUY4CiNxSjp$o(m0NR9yi3XVu+K_Kl%EX`&YC0R@53^R5P4UjkK^VF8?E%P zUg!oW`g&u*3G49ecLJnqeZIFxB}tSH9E;c+x{@-aH)DdgB^6FE+G^|Rvvl`&R1QYn zt-^UFrGxA;m%gky>pXT%Z2Crnuz^fne62%}4)+E1sLV=2&p}ksvn;se&;cRt${Hj2 zI0p%b_|5P8QYc*lU&<20)hG{N?Kwmk{d|NEY5I1=qM-@&l|fV9_(sEkm=M{vNQQ+O zC&x1vmx_r(G+~u-oF`LS=MA_@k`J;hIN`Unsh6sc%9pT(AeEW}emvvjR^NSGglT(x zDj$r~H9h^lU3JIgY4x!Lbk}@NZ%-Ab#2umMm)5=!Fni-;ovjV75~u`4Go)HU^kRTG zv}sh)#T4k|hg4`~4N59LZ^{d&iaifVi<+i0!9=STQ~=B4$MP<4{pbGKKA~fXF>T62i%<2Yd5w1Xj8bjd$}XOz+}2b15#Y2A-xep8RD!^xZeVpvifZIPu-Y4lBFn;!2kxv{pGqqb8uV+V8E%wB7? zB2Z;j3jp~Pz9cA93LlBEJ8X1YbiO^I=D|xE#g}fas4yJGhaDNhmnDU4wv>-&g=2*y z6n1$&si=7-hhT&5IFk=CGYr(&ai6?T;boB`G$)iD-@{z%K`V{P*%f^b@7MltKd$70 zBPxS3aVs0jhz*lvl|}E6V?DbkeG4Lhj4IjTKBs+$mN zEmrxNc3Eug!0(bDbVWsBPk#zT5?8-uR<6e07!kZnNQdnbjfM&>eqLUk?MUl%(37ewiw3B}(;>2IR+H&ObV zDE*&6l>S=L{w7KQU;q%`yo~@LS)S&|7a9OSgX>=srN3(2F0{^_)>{$3wlY59EA~#C z<|7IDZ;F3+*O_#wm>%xgKfj)Nqrig8NR1~Ssf@yfQ7ysr@VwsV z=8z^GWZpg2mq20zB-CdRhYg%JSCj2KBxyGUvdXG7Aazet>CT|(z8GdZdO{CIc}MH8 zVMq}Q$@u$%1O%EETTHuI=uIAL^vhNnYt+`O6z))$(%9#`z&H4>SSh6*R}+n79qNKc+i&0ir2a%1MhX&C>Q)Zv)J&z2Dn+F; z;P8BiGIq>viB99U)J&m?*1WG=iP#4{WE=Mc8K^c$Y@nWAOUZt`IiZ-z(N*h>>!W&f z&2U)4fkPHvqQ;Li@yCuWWLP?fN)j=iYpl#Jha^CcT8-`SJw%wY$eA{GByyjx+)&Z+ zLx;U)5^78|B*tBo(7`%3S0j6A2MSD1JZx1X8 zz#`yqfMWcBX``M4x#(=bb6Qk>N6j;Rr`-*~V?3Wtb=BhLXuR*$6nv2u6px{SOB9Y(8|dxW$LukD9^4w=lJIxd}o7n{ZM|raDOPU0kjNbg^xnGpOd*a zkot%J2iAMb9-Ds4K|{9lZ++rFw(<6Sh<6|O zz3h%%4GCl(EScj;N7RW$W3M7rn2^6p5pW)SpI?_xbG8`7!KG%)(Z+fL;<9uD~`Sl*VUmk zvBmR77Nr0|#1^AHAxel&UZ{;VcOc%~-~yDLUjYT2+*@5QXJ?@|OZA;4u(p3FBfa`4u~d`6EJpDj#r%3Gtpe%A=Bz`G+tLjV^}efBy7ZO= zaGoY6aAF$pik8Dc81DtTA>E-#yUF%mltznH8*N`-IqMzvdRDM>{}i{rHwchogSF~V z6;Ll#viw#3m}E5-vGQG=HrlTh?Hd!{KQh~YriXmVr1=22UMKb7V&O+XK6m6XxK;r& zWhpK?>gzzdmFO?UsGdhT&k6H%Ta^5LG+7xGpjBFc)nS|Rp;ZRgOfgQ-tb}?8aVgk7}1x zI9a6}YDe-%pO%D#gdu5_0Ju_@AQ*7k`z>ItgE8P7$Q`F{g;|4)GU%!qdI{1I@H2In zt8dV$ng{DQGS2Vrrqo?-z`FW*#wxO$xXVV)EiVOYRKAVd9Q|$DZ}=nO|Bug-*z4!d z+^+|2*f;>!npKS-2w{P^Es+6Pg-6DwQujK70HlZ9z-NO>_D1<~xf7_>j!i9iU7Jux z9t4MDyn4c5z#S1?aT{?Qa5!^1J_#z}FiUiEO}?I0M1{ef6#88-*s3F*D{ zDr{k6s0i@EfH`|GY>70LU?7-{m6^yigdgnf_P~q^ak$_pWAo)oi+<^64`&meqvZ52 zNLCr9$08%0k^62hg|E^wMu4mYb2-N{qPpj72sK*f6x^sL0uVnZ-Ky5U&>9Zz(Agk) zB7O!Ym07PZDXUo0R|pYV`N=*G`PetFKiVgDJHK#G=*CGx^u^4M-ABs)<7c*qyEEFJ z5>8mowM;4BQ>S^{V}3%;8+khQ_{C+S&#&7rIZ<&@GJ;z%Voq>85{^evC1#T2QZi#K z(7tIb)`4^q2$lu}a79><>wMjq1Ftf*G>nBxoF73{?6-1<5q@3LJwSszF`erD7jNAe zJcFn;bv%QcLfRh0f?na3@L|tx5Ds$0gFG)22yAzCg->=(=qqtDnpT8#wF9&R!w`i( zMl)$x1&xQ%z+gl2uhqq@gt4>5YJPxYKL-H?<4}pRgoAgA=NJtIf*s^N3pH^j?}zR< zUjd3zL0!kr+H1O8Zn_|h?2`u*1Z zQ_)Qo;QDAv?2HbUT?Ok1;nQ!@e(w<_w3%lqpBY#MYAppr9`NW!f;Hra_k2BjYboJ>x?r&r;7|vmcPiIdZViUNK6T zEGy|OU0xceBd^kkFMIt%jE~T}hT#Bwl-ix}$b5{_RRjIoLX0pcnGk?8*m;_VZDBhM6DMyEEjTGs7xTYx9xniT!5gqg-mEnKeW)FI~N+ytn8{xl9#( z&{#n1SC9X_f^(kN!|(LNe102$godDSWZ*f#p_+Y9X1rmp$U8NNT4*bcR)sM~8f33^ z4C=9c;tyF}z?hEug>2sZIUT2igw^X3D^R-Y4&v`7SdmB58*>zkowF$j#$`0|iGyu! zDbOjEa9uK~a_um>^&D}02fsir|D%Lof_`2>g-8rNH7Mio#maD>t^kl|F)&{f|f!N-_Ec=wE#I>vn;o?o5Nl_tCS9U zu+>v`Sj>syPug$7`3!bAed<7Dw!$S6`*2(>6JV!x=iZL3_uJX~WN+TO?qe@$LUj8h zc&X#F-b#0>gv<2F%0N)*>v_4Y_ptb?|H00N$55-nL$x`rD&)-y+T*d?6s3kHTL=q# z|E|JB1|8--G^)yakcMj+5gmR|Mw$!kh4}LlL!ao@b>gk3thiI|bG~buCAiSEP?K>qHDp1&I?j95w*%LCr!OB~?I45*bjO z{HLO6B?L%vAi}UqIvY5Z^GJiN5j|Vt$>y!<-VO|gtAsNK?9Sps;ODaPq}#=gwdrm- zb866$g`;bs>t;^HWnf!p-SNAIs$5E_M~PvtE!Sz5Cc>ZXt1G?@H_cVeclC#>m;ttb z`9S=Y@DAVr2>@`U2LJ&3&l0k~iea7pVG6gQt!1|-isJig;Y;6Rkyn|dx$WBt**S@N zPI6tPNT!mv3I&sXP*bnWroL9?%WHZ<-zyr^38uT)17aoGj5qg85}=Ybjc=G5DlReB)EcKi2) zcN~yPE0J5X>Coyo^s6!ZowShiRZoDlEyY{4ocw8@yLcN48vPY=%(h2K_=$3hjK0)I5~;_F*MT_)9v@4Lx{Jx8vrh?zWC}A1- zGh@O&-A+YXc49?2Gul$mB>+z-M*zC^Pn!EQH%?PI9;D~2^Vdt7Oj_0tz*MevxYj{T z3d2_*7Gs%6%=_I81<|_tazojce|+p}Ft}>3z8gtUfOq4mw45b$f$P`ZJlb{=Cw2x^pEWc$fOf+;Q#@|w|PM*B% z2!KO4+HzXA=Go0?p%T?GF(JnP?(5hc*9Q_aIf=3qrml1%q~dqmFNMmSRPndONdscA zUlG0@)d>CP*=9$MF!J@clzn|UN5PW7irIr#{qW(YrlBT2qz1!!QNk0=8up-YnW&ZiP0NMwIs*d_|=7QxUUM)>*B(`MVOx5W5yrYBU%qnbH>JFx-<0~$n+@l|L3n723DLQaTSDL+#e$HKb zT(}`^G?f@5%R8(lb?&WN(3>)%-Kw~}> zcUyz6ShHclPeVha5zEM%K?F$%mT@zoWTSz*(-`wrJP|%2`(Yb5#LvjFR{(-^I&e>d!!^H zUw;$f9H8v$=1Ol-CrOyGR66u#4I0BAa+YgTB^e}1MqHl{v`sY^O)Vg-fLcO{v7lKV zDZiiGM4=Ib+^kVFvNLPN766$?7z6h7%IR;34<&O)Y5pr4Wta&Vn6os~#;Dhjwf8r^ zf6kyh%13>)9N`%QF6Tg3h!O&sYi#L3$mz(W~{lZU5n~W*UB(qtS!4X3K z$s8=el(^M%XE%;~)>8Vs2gI2|LXUe;!seyP;k9ISSOP$qm_>vcB0Y^te)}Qx+sJ4l z4sXur15(dXWj7~M&o)W^hu-*zF9m(rN?-?$0fm#vj7ef8Iubf70jYF%4d_zwevLNM zg%x%R_I7;;GEmG6qD;544Ak~|okr6TtqLM67Rf=$mIF-jw$4mt9V#uHe*6TT+5iqB zX#e<1ku!h{v!-waJoU};OliTPm0!}-lyV zMk`F+m(+b?(jPzbx2{q9k`|CE6w6c$;8G5e=rjGbkx`@MVowKQtRYlc&^0RD-KE1L z^D&RvMc{Z@*wlfy9kGkX$_B8LyMX2(3AE36qDojuV)j>{XZunnkI*e%EsGOzVx$VZtnc zU4bCp^=lv$P>)W6@YT*w1$oi%IfBL}pi5 zHABOIAyHl_ACjb)CB&pPrhSnHlAp}5{}7stB!W!kCZ1`&U!{Yh0!R@me$P z)e*2vdW{Sf{SQZ9`7qoFoSd<FH(gU}J~%uU7CCgCZ|@ZVX%n`Kw^CD@a#qvtUU1 zX|`JqrN$nA9Bo;-yvQwJaUM3b*EJ~ILOe0Y_YXHC+Zr1Lf1OJ3`yp6!bRv$=&jg%) z$WkXkNfCJekUkqbnP~u$g+j9cQSesR^^do*VwA1!o--J7@enUXNUxxKDc)CmG%{~V zt7)9kug;z>oQ&NcJ^R3OT?|EdFT3v5!ypmmBzY{M0z<98i zi=FGfq2X*?U5d97jUGR5i67BoD43hz>EEw_Uv|INl$KXZh%IVvcKa^2pSgKfOUPMm zKXtTRdnMt9b-C@$URD%XTp+jub-VpYZ#Kw~B?lzfe(C7-Y@Kk2BsmC1!kgjyg#TBt zf`g$cVf}kgHuGJ~PV~>cnVX%Xm6N%N$-nzr{&n#md@U0u|6!%!e;w5QZKdHlH}Gwx zL1#2`zQ*HkMirq!h%KeH+>+}?PF-9(Zu=^b%bS0BWtFo)h87z^-PjHoa-bB>tnSXk z`wNf1jvdNYG8k=Yr1o6ZZ5=&3dw~Gg$W(=55KT1U-5ll((QKI0q-gC_DdXotBMd3m z*A*CZR!JM%AscWzsM2LnNIg8Z{IF9^)|Y8%cN}hR;#YdY!EY$!NUp_k%DEK4p}?gy zEWJQgH52+4o3*dTQIbS`jy&;{y^uwfY3;*m->5%C7HEDa-T$(bpVwJ_|NoUX|6wox zziN~D-_gd}85nA`6v3kQ#?xDu&)?7iWg{7aCOg4kx%#S^j-8`ii2L_MgGtjm~q(|jo-SGfhIwCm?4tohpistrxqKF9K;UxU)NSe)Fb--NiMQ3&MF&odL$ z(=mTTfpe)CI)Tbc#`Ja8tpBby%=wUs{onB-_+Q1%U&-NLc}{R#-*?;~2ww-h2d#J! zlZ7Y-9%>8XFSC9CsOXLpK1!Om6u-Qksb>ks4H#|FU!Hp9ETI1*Xf9m-Aq=l#?Tv58 zh3ix5XZ}>JliQ3m{3{^gYqf!qn>iWznuqlt~H1wO<;AReTz}DVnvNlxmO!4v@sY; zSR2O*ISiyKM(RVLe!-A$ryHtSL3DV+?b+W`ys)ja$k>_hPk0bx^9hsB8-zoK=NFd= zj133lnb0OtmrdgieQ`(H;l6xv-<5`WdbxW1ODT$cbA#Z%BVpy+D3tBC!#?YFau zqmqfU^Iwjz|LK~6*3iNh%2eD$+?3pe9B2-ZpD~FbEqa!%6W{t!&T;KoR! z7o(R@&ic`3#>BQj%?gIr%9zfhY=2jVD1;9p9$^Qcx2M02Ko{QvflHpU*3s*w(H-J?;7|KF7ztGx^z>O zz#+iq#K1fN@V3@~EynOibu+QJv`){5F^5B90me>{_~*Ou$ISG2~r4q>}Ddzz^aJQ} zMfy1q>Jb;Trv%{c+4(G#@xm{vdbW*UCDhumyn=X>VA}n8t{R?9x8)XrN=%!dD62n- zP4DDZ^{wgluD(`RIWp-c-&0%ips0aX@P6mvy(sW=wvu!XL;RBK#Ep27`H60*5?NWv zw1+5P44M)g^Dw~7JE5?qBjn)GOl;vtk8*)seh>TW^E*EO_k%oG(+ZBE?{7A~gB#&L z2Dia?^X1>M{qHw)wvvq120cO-`YT@al?K--(ikCEV^%T_R(oyMAMTW4D?|;(xxuQp zbFTUbqtfEwjiad2C)1pR#c~FTFkW*rCx`Aa;Xu_k#dRavEBNgx-Y8;(6{P5f^eEh* zR!Uyqc&=;BKSd10Fa|=jmIba!B?5~p{OS(tj-`&>fO`vgN@^;ayG=@Ru$q!|^%smo z$oVvN+*V}#41~6vo^CwXMdAZ^q+STEKaj3zGdc}jWr6R#)zy{(h!E-+Q z+^KjQ@(4V2WYG)XKaft`ny zUIX}gt3bDK-{~r7RovP!pS@bBIYG-`-?T`A{M)sM;gGA+oEQ@8M z&a*PcJBb2&hyfC0mT%WC$r2?KmW-%6fz@8qZE;TA-9FUNM({FO%Zr1=Y%9!pl4s1n znvw5)dBk!o8YNZBsn4KDj{r6AU0**ppKHq9X3B6hg~H0#B3|+qY>hQ+?f{Cd?fy6v z%hk726wl>OE>$#8%i&if)dWjwpcvVb=v1B|UDEDOD0R;2cCi?1l8HHAwmcY=?#d3! z^}&m(R&f7J=OZGECIsgSpArs-ApLfTwMTrUF9C!-?$7;TjoG|qWQmbX&Gb}PlN{g$ z(<$OS(~XJzAMKrWR8`&9@R9BY=|;MwyCkF$k(Tb(1JVuBAl)q@Dcv9?-Q95LkPb=T zBhURV$b&cj{KmMOG2onWSiimI+Vjl4_MB_ZH5>MdUFp)_4l&r3)P=VM*VxO!2d%$_ zah25eIcl}%F7fXA&qCt9Rm6)OKAoAcRe^U^yHLWI&~G^^QkFssSP$HnbyB(MbCm=E zm0^5N^w6C>eewNT+BBbQ^p&o&xxozAlDO8V>bxrIi{6Y%OAEODfOpQ}VP3U^2(XAw zIoFPo3Jm}=;2jbRh7HscVR@sMq?lC01!WxmVxJX=MWGd$C#u>}(Rmpyz@$vm2yVe$z>L`X`sSdD^$+vdS_Hr#t#AP=Nr#S-Z^R==zTIK5&CW_{If zFOi1`Y@w@s*kb8SPSiWGKh89(Tm5X|!eHq61ka!nRNHHeD_XobQ(P^nCp$lwnJWh4 z=+#OMy-E@Orbn|PUim2~;v+r^V3v%Bb)@UT%8|@%AXAjF%u>diHa81nCe}T7#hG~g zp|PSyS^>qbvFcsO>jdzMaAfaevej|Ds|?Hh%C4gLR9!Aji$!(hJk5H*JbeY!1PN;S zMFJG z_AGS@Vc6NGm^(&cCMjwmGKNFG)$w9R?LGc)%JoyrPEw0CFE) ztN*eF)L?%Z0aZ&2CR2c+<&9^EB!dEsI*sJ^Yw?Fy+nQ=`XE@N^%nxLccJ(>l zNWHIDY*SHt1FG~%iwefB`L+YDb5L>eYImB%LEC##Rju8;R%!@wQZ{XJ-3s6yLhSA| z<1@rBJ46yH_jog)B;3SDAN|?8PeEF%-F?7Mj|G7izDv}ZiuQ+PAIBYJIVHWAtmPjg zn0n{Vha}ErAn|Kyx;k7;8wS6_C+Xd_n6b5UtWhAs!n&q2E_BY;-t;r9%FV>fU81hg z)5@uEax+a$O)hp`aK6|EuF#2cmue^Dk4>)Rv38gBB^@4i2OX-2$Nnr@$!C+@eO;SH zBj;rq(|=A2ulOtr&77zb`J+zWJFG+)xjRT=OJ;s;6`xT;uEko~1cD*Qgp#+Lvo)S0fHb%6dd{gQ&NSS6*2DlDln606+34!-bfZNo-qJP5tMp0Bo%YtTm`-;xD;jj z$^3&@vyR!@EC&Szg$m{TdD0AvX>PZBgJbV#1?HkYt=sO2D%{?FlgtUT)HrqeG?#zL z3jeyr^Xz0sKKV}-1+*o;gwh-?TCrNhlO?7~!yL-MA2r$->+kuDJLkn^Et43(1o+F# zg0}lH>+)Mdn`8+9EqXIl=x-t#EtfL)>gAAoN`pQUdqNTWap$-iw%5_i?S;8UCbbxI zlZ+%87C#aVt7El{+~bfog+A<87aUn(JxQ;E!mb-2rZrrx4oQ5mT)oKvv8+YQf?pAQ zbvr8zA{BlDJ`|Cn12w+bs6dYCq1LxJ#D3LNz^)%e33ric#(lXPPb0^=NY#AKm|wZj zax^OHVO^9u6l`V11pKaqK2s3ijuxL6gh zl2O5L+&H}|4v($qgM=8|=&15?d#4`?*3tUHVs74{7Tz;9SwV4Od&o1~J}+m~c|`1w zpX&P*Dx^`2bn@pE(9_HlX1teNl&WN!#z&vH`3`MUL+>&Q^f>L!n?A$1#`Z=!3?t*2 zCc2RJ*DOg@tZDG?Ye9sAI<2YP!c%&?cI>$bh@73NGz?e9N>kdT}Y9E*L=FTw1)R?5Gi6&>D~` zDs}ux@JJu`@Iu2TEnaXT?hQ8qJ;QC2rrYU^z5s=lwM&?*_EOH~3D*+bRjOXD>K^u+ zw!96(SmE`^B-@p%QbpXt;_{W=%+!JBKSTqP@5nlpxv=*$kyIo-87U+j4bK@4ry z?mfqV9aIgm=cy4@pcZA#G@nTUq3`s_d6WXW`zetI{H}>O3=iiX{fWac1Th>16cDov zo=k*Tmk9+QNDV8x?EA$#ui`bXQ*frk;m8ko8ERQF%maqn0ahtSM*y2xto5m6xRvP9 zNQ1l?jK<}+7nWOEK&+fC;slh{Q6E`YRa^}YnbZy<;V*>3Uu;JRl3!?C$je1$Yl!RC zBp^*oP+({b8hUeu2IqY0sQ7p`QEq8AY40Z37ZPu? z6Z^n!w3uyHSWXVY-jt9k_!-T0=k~ib2}cGh&k)Wm6&K8G7YKr^1verfq{AMQ@ixN2 zx=WA>C$n#1wO>|`KJG+b(Bl~yLi36^QwwXUY_b7$v{}bMu3i@*2wq6nQS%v+;^gtH zKJJE3xr~a2j}rqessfpupOhPDRPv7=7H@Em4f3ox*L=*w{yI^ z%H3aH+MG!m5+!X^eC}=tBkkW=&kd=9`DuhJ-Ocqu`g1w5MF&Aa;xd2Q+wT__cq*@B z9e~C|7<8&CT8OziG!2h(Jw9xo!FGpA+vUeCd^%n_mK~RUj~7Zq*8qNQM4L8}TLh{l z=TIm{nnt03&-<;%)ow6#-LL+IfWCwD&=e6SF4$+eN0NLnB;^rcCgsSmy25tf`PHX! zikptu%K?9{YF3~ zcBqJ&K5!^20Io_+X(L4|LQlJEK{OsF!ov?MV24a9i{8&%m=;RGo~N^3ls9$=S62kh z@v*Pr&WhUju*d%A*0vyr=vKeeHGOnCO*;s|N2B#ffvVN4h#B(H3Q_^KP1~&F^-UqL z=4lgZE^Q@l%>KDMXRxU_`5$!~1l=68^a-DI7gYPJibqVl4n%kyfp)r*#eY7{KJWw~ zfVFoJdqF3oHl=1&#_zC8N8v0fcjT@64jJ4!&5FsG*%kqEtH8x8Wnox^tRk{Tg&IRT zm^o;&WuXw0u=Nfx08u-s*dUr*oz_x;7{x-qC!2hOl;a0gFItvX?nSE>DKrR19-jnm z13@i98b95*5_wW~1Fjg#+AmPC#UgCo<48YD_pV+|p95eL3YXsV={na?2aPD4h@O=a z=TN~ujqi9F$kfWIkOa#(#)4_7-M$ZU=h&!WkJY@6gR*0tuMVnnfZ^V=8g;WNzMkB& zNSn|$4=;EB5MqY9T$iHaD`f$$Zug4BdB@*ulx_YfPv~S%wz+h2-%YvU1(Pe+;s|5k zvY2at@-a+vJn3pbF#C~0xCX4EG16)2i~wSFKr#*od?}i0tR0qlN09vZH7?fII<$555<~lgiAxLVpzONkJ0D zT{xGdt$8}^P7G!c5e~?KPKqs}!4%JHMaIp8f82o)FDy@DxF$S{_?7AnhU*&u5ngy< zL-u)1FdI@AF$YB^Ax0ipow5Zr_C;urBjNr}b`)13;Hr&K;Sod7;i$2#S(Y^+oM?Az z!*`>^eA98d?Y-0_9b8Y6@ltfyuL%=|;uA^ZBtMxbY3O{yepVzkI{8n*64f#aiHzFM z4-u^tQw4BIopS5dh^y~>MJ?dYNA9Y_4icJjis*lou@SP^jfT=bF#ZBfQnt~h2i=FY z9rA$b|l zK()EAhbVmMJ;#?51;EDW^W z9}|sWmDAQd`C_zqpON-a2kS{|$i)s=&N16ifD2w{)k;RGX-c7-9-q#*AKScar~0y4 zKP-w)%PGG!5veG)@|mQ7qgiXjgyN_f$32$^(cQzUGsC=SyoA#|s#NX>_JnwY6h<+j z_}fs)VE9Og-?SF`Y6OLYnk0=@F4bm9Lu`4_sAZ_E*QJ!5G2+4}ex%@)2(52Klk+)T zEF5zdXFRL#v0utl@}exLDj21oB?ZRxP{ML(T_dxC(iUAm<4B8Xpu2$kr=4HEAB5od zdveK5l!{Qq?`NNhn<$R)vs*g(F=u5m0v1@_BMds^#r1j3Y`J#iZW0IICK4fu<7c|@ zON?D9($}A{t@T*i7q1;GHWtRVm!~)3WIFJXn;YuNtR7RcdULLLvcdSyJ#yDN zs%kO77t71t57MfcS^UK_~wvI${O7O?d}k)Jls zPS|vxl?RfS2%wbu%)ub`Ig(VGeUMN@G!6#yK1f7QRd=HaxvbTN)eeR7Gwkd0OE*P^ z3$@1&dxA)kl;Qk?Q7-FE_(ni0YO4uCJC02){UeN=Q)~UNSt1e@kSQ8dmWv)t9v*K{ zS==EHn#d$E7wy`%moIkx{JteR)s`;IXN*~#az6Q5%{htBbsR$2VB>Z)-On2-JzPOE z(f2|Ed85F|=i30k=-B7J@m=0NPiN*rYzab7j@jYHq92r~j;?*BXo23?K7GP;2E!+v zh%pG=#Z_Z1hD8aV^!T!s*BmA*=Dd{3FXiS6U(d}u0-nzbz3BbO!1FbcXVm#=e$p7< z#M*TL3Az5t6FXFn3cX%2kE@h~O<05AkaZy-2cJw5L7y_8^e_&L&OwK?KTFA{$s&9Q zECL@Pr=DQ;wkcOMn{ss~9qL6BRxUVC!8D%AFX2{p+UZhCfM$xzoZ>t^)+Qnv8TCL+ z%|V>x@PyIBg@mDGSFERH+CQqeYAiOxcbSnPO5(X=3UeSQ%f7!XaC|F^6}xQ<9dt;I zwF+@;+Kha${}$KlVWQ%m!-qU0v0oXE>ZI}eE&*Hbq ze$S)wcD;xdn4h;xoZT9D%%m$di(^h&q>=$!jC-oZHv-()zwQbk>lN5>75t_UR2klC zM}o5-Vh`vK$@*ycIZ;a&^=P7ySL%Q=NBjFNc5?x|Li+}de|1;Ep-ZYHJxeH zOgUOqK*)kqmSq(5K#{TjR0Q*?-Q2M&`K+rypt$=PsamQWue&*D!;ziCz7CS$O4bbr zyE9^aCz~yFE0{In2z!u?U`o9wo3^i=UoWuP(9X3t5(`-Ap0R$LRP}6yr+7tD3?6_b znJ<*#2dSz@h;9H^>ZR6{uk(sIk9hUq@>8|zgCTT%4J02;(8zxPh0y=*W4T#E@E`kF zqP66knKAu-b~UpX;-e#G(XpUL;|#z$%}0BiZ?{|U0xyqKLOhy$TImWUBi)iuj=nn% z5%G-8y@oRfkNc7e5$y5+byr?-wBSJ0V~f#J^??aaWD`U4&9?wCC4 zFA4FQX4jEKJ#$L{s8q5W;*nCmUBX6b@S%BDN17{Aff6|?ClE6Bke2=pmg~-xwu!;U1?h?s14lKth7xYS4%3Gwy*6RuB%?2M};awV$p0~xB_5$je3Z` z5y6W(m=Ms{9tAyvU$Z zv`|G}Ly-*Y13)%Uc*Pa)Z(M;N^T!VuiRX!6ggv92QjCb~DeCM9MkNTP4w9QNjPdwB zPUgn;2CY)RH+muK3}zciMaKXYtL88=#^Nc%anVXj%V)n=Zd%@?sx^eO?fp`lg};_} zbc9>8wAb)C=_Lt~weUCZhIHbJ26~(TWxI10t zY&BIy%|T|>njPe8B42B1Keu}TOcZ-$DH>cfA<0I}b!rAl?&`%lp@Y00oJBGly~Jz+ zMVQ;>L7DPtBPNe>p|>_(wf^L+-p#XVzPo%1cqAA&*p=SyiSFxnp~w@b_Ik7^p(Z-&Ke0uSnH@=9}($1xiKX!nvYT zp7Tz2&JoFFR4$jCRvhi#HhCSc@6H&5&RdcUezDd5NPT^q`D;$}`6~6vS>HRswziDV z{VBru>B}7m&=b56&#^ELOqcCqE~?)tVt;qcQ%0DAXExsPWn25+k*GGELT<2*h^Ge~ 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 literal 0 HcmV?d00001 diff --git a/docs/pe-nz-source-checklist.md b/docs/pe-nz-source-checklist.md index c0b362a5..5ee95f95 100644 --- a/docs/pe-nz-source-checklist.md +++ b/docs/pe-nz-source-checklist.md @@ -60,7 +60,7 @@ tests, and record a verified `raw/nz/...` R2 URI. | `stats_nz/census_2023_ethnicity_age_region` | [ ] | [ ] | [ ] | | | `ird/taxable_income_distribution_2025` | [ ] | [ ] | [ ] | | | `ird/wage_salary_distribution_2025` | [ ] | [ ] | [ ] | | -| `ird/working_for_families_statistics_sept_2025` | [ ] | [ ] | [ ] | | +| `ird/working_for_families_statistics_sept_2025` | [x] | [x] | [ ] | TY2024: 330 administrative facts; count/entitlement, children, family size, and full published income table. | | `ird/student_loan_statistics_march_2026` | [ ] | [ ] | [ ] | | | `msd/benefit_fact_sheets_national_march_2026` | [ ] | [ ] | [ ] | | | `msd/benefit_fact_sheets_supplementary_march_2026` | [ ] | [ ] | [ ] | | diff --git a/packages/ird/working_for_families_statistics_sept_2025/source_package.yaml b/packages/ird/working_for_families_statistics_sept_2025/source_package.yaml new file mode 100644 index 00000000..a116defe --- /dev/null +++ b/packages/ird/working_for_families_statistics_sept_2025/source_package.yaml @@ -0,0 +1,1331 @@ +# Inland Revenue's September 2025 workbook reports administrative Working for +# Families entitlements for the 2024 New Zealand income tax year. This first NZ +# package keeps the non-derived 2024 WFF/IWTC statistics in the +# workbook: recipient families and aggregate entitlement by current credit, +# supported children and family size, and the full published family-income +# distribution. The publisher's average-entitlement table is algebraically +# derivable from the count and amount tables, so it is parsed but not emitted as +# duplicate facts. CTC/PTC are obsolete and blank in 2024. The income table's +# explicit "Rounding adjustment" row is not a population stratum; it is parsed, +# excluded from facts, and pinned in package tests. Numeric income-band labels +# are preserved as labels, not silently interpreted as interval boundaries. +schema_version: ledger.source_package.v1 +package_id: ird-working-for-families-statistics-sept-2025 +label: Inland Revenue Working for Families administrative statistics, tax year 2024 +artifact: + source_name: ird + source_table: Working for Families statistics - September 2025 + resource_package: db + resource_directory: data/ird/working_for_families_statistics_sept_2025 + manifest: manifest.yaml + vintage: september_2025_release + extracted_at: '2026-08-29' + extraction_method: xlsx used-range cell parse of the two publisher data worksheets + parser: xlsx_used_range + artifact_year: 2024 + sheets: + - Working for families data + - Working for families income +record_sets: +- record_set_id: ird_wff_statistics.ty2024.recipient_families + provenance_class: administrative + record_set_spec_id: ird_wff_statistics.recipient_families.v1 + source_record_id_prefix: ird_wff_statistics.ty2024.recipient_families + sheet_name: Working for families data + period_type: tax_year + period: 2024 + period_coverage: &tax_year_coverage + basis: tax + source_period_label: '2024 Tax Year / 2023-24 Tax Year' + start_date: '2023-04-01' + end_date: '2024-03-31' + notes: >- + Inland Revenue describes these as entitlements during an income tax year. + The workbook displays 2024 on the aggregate sheet and 2023-24 on the + income-distribution sheet. These are administrative claims with non-zero + entitlement, not an estimate of all eligible families or cash paid during + the financial year. + geography_id: NZ + geography_level: country + geography_name: New Zealand + geography_vintage: current + entity: family + entity_role: wff_entitled_family + domain: working_for_families + groupby_dimension: ird_wff_statistics.credit_component + shared_filters: + wff_entitlement_status: nonzero + shared_constraints: + - variable: wff_entitlement_status + operator: == + value: nonzero + label: Non-zero Working for Families entitlement + rows: + - value_id: all + label: 2024 tax year + ordinal: 0 + row_number: 27 + expected_row_header_column: A + expected_row_header: '2024-03-01T00:00:00' + guard_cells: + - column: A + row: 3 + expected_value: Tax Year + label: table year header + - column: A + row: 1 + expected_value: Working for Families Tax Credits - number of WFF claims with non-zero entitlement (000s) + label: table title + table_record_kind: total + measures: + - measure_id: ftc_recipient_families + label: Families with non-zero Family Tax Credit entitlement + ordinal: 0 + column: B + source_column_id: FTC + expected_column_header_row: 3 + expected_column_header: FTC + concept: ird_wff_statistics.recipient_families + source_concept: ird_wff_statistics.recipient_families + concept_relation: source_label + concept_authority: ird + concept_evidence_url: https://www.ird.govt.nz/about-us/tax-statistics/working-for-families-statistics/working-for-families-statistics-datasets + concept_evidence_notes: >- + Workbook table "number of WFF claims with non-zero entitlement (000s)", + FTC column, 2024 tax year. Inland Revenue calls the total families and + explains that a family may receive more than one credit. + unit: count + aggregation: sum + value_scale: 1000 + expected_cell_type: number + filters: {wff_credit_component: ftc} + constraints: + - {variable: wff_credit_component, operator: '==', value: ftc, label: Family Tax Credit} + - measure_id: mftc_recipient_families + label: Families with non-zero Minimum Family Tax Credit entitlement + ordinal: 1 + column: D + source_column_id: MFTC + expected_column_header_row: 3 + expected_column_header: MFTC + concept: ird_wff_statistics.recipient_families + source_concept: ird_wff_statistics.recipient_families + concept_relation: source_label + concept_authority: ird + concept_evidence_url: https://www.ird.govt.nz/about-us/tax-statistics/working-for-families-statistics/working-for-families-statistics-datasets + concept_evidence_notes: Workbook non-zero entitlement table, MFTC column, 2024 tax year. + unit: count + aggregation: sum + value_scale: 1000 + expected_cell_type: number + filters: {wff_credit_component: mftc} + constraints: + - {variable: wff_credit_component, operator: '==', value: mftc, label: Minimum Family Tax Credit} + - measure_id: iwtc_recipient_families + label: Families with non-zero In-work Tax Credit entitlement + ordinal: 2 + column: F + source_column_id: IWTC + expected_column_header_row: 3 + expected_column_header: IWTC + concept: ird_wff_statistics.recipient_families + source_concept: ird_wff_statistics.recipient_families + concept_relation: source_label + concept_authority: ird + concept_evidence_url: https://www.ird.govt.nz/about-us/tax-statistics/working-for-families-statistics/working-for-families-statistics-datasets + concept_evidence_notes: Workbook non-zero entitlement table, IWTC column, 2024 tax year. + unit: count + aggregation: sum + value_scale: 1000 + expected_cell_type: number + filters: {wff_credit_component: iwtc} + constraints: + - {variable: wff_credit_component, operator: '==', value: iwtc, label: In-work Tax Credit} + - measure_id: bstc_recipient_families + label: Families with non-zero Best Start Tax Credit entitlement + ordinal: 3 + column: G + source_column_id: BSTC + expected_column_header_row: 3 + expected_column_header: BSTC + concept: ird_wff_statistics.recipient_families + source_concept: ird_wff_statistics.recipient_families + concept_relation: source_label + concept_authority: ird + concept_evidence_url: https://www.ird.govt.nz/about-us/tax-statistics/working-for-families-statistics/working-for-families-statistics-datasets + concept_evidence_notes: Workbook non-zero entitlement table, BSTC column, 2024 tax year. + unit: count + aggregation: sum + value_scale: 1000 + expected_cell_type: number + filters: {wff_credit_component: bstc} + constraints: + - {variable: wff_credit_component, operator: '==', value: bstc, label: Best Start Tax Credit} + - measure_id: wff_recipient_families + label: Families receiving at least one Working for Families credit + ordinal: 4 + column: H + source_column_id: total_number_of_families + expected_column_header_row: 3 + expected_column_header: Total number of families* + concept: ird_wff_statistics.recipient_families + source_concept: ird_wff_statistics.recipient_families + concept_relation: source_label + concept_authority: ird + concept_evidence_url: https://www.ird.govt.nz/about-us/tax-statistics/working-for-families-statistics/working-for-families-statistics-datasets + concept_evidence_notes: >- + Workbook total number of families receiving at least one tax credit, + 2024 tax year; families receiving multiple credits are counted once. + unit: count + aggregation: sum + value_scale: 1000 + expected_cell_type: number + filters: {wff_credit_component: any} + constraints: + - {variable: wff_credit_component, operator: '==', value: any, label: At least one Working for Families credit} + +- record_set_id: ird_wff_statistics.ty2024.aggregate_entitlements + provenance_class: administrative + record_set_spec_id: ird_wff_statistics.aggregate_entitlements.v1 + source_record_id_prefix: ird_wff_statistics.ty2024.aggregate_entitlements + sheet_name: Working for families data + period_type: tax_year + period: 2024 + period_coverage: *tax_year_coverage + geography_id: NZ + geography_level: country + geography_name: New Zealand + geography_vintage: current + entity: family + entity_role: wff_entitled_family + domain: working_for_families + groupby_dimension: ird_wff_statistics.credit_component + rows: + - value_id: all + label: 2024 tax year + ordinal: 0 + row_number: 87 + expected_row_header_column: A + expected_row_header: '2024-03-01T00:00:00' + guard_cells: + - column: A + row: 63 + expected_value: Tax Year + label: table year header + - column: A + row: 61 + expected_value: Working for Families Tax Credits - aggregate entitlements ($ million) + label: table title + table_record_kind: total + measures: + - measure_id: ftc_entitlement_nzd + label: Aggregate Family Tax Credit entitlement + ordinal: 0 + column: B + source_column_id: FTC + expected_column_header_row: 63 + expected_column_header: FTC + concept: ird_wff_statistics.aggregate_entitlement + source_concept: ird_wff_statistics.aggregate_entitlement + concept_relation: source_label + concept_authority: ird + concept_evidence_url: https://www.ird.govt.nz/about-us/tax-statistics/working-for-families-statistics/working-for-families-statistics-datasets + concept_evidence_notes: Workbook aggregate entitlements ($ million), FTC column, 2024 tax year. + unit: nzd + aggregation: sum + value_scale: 1000000 + filters: {wff_credit_component: ftc} + constraints: + - {variable: wff_credit_component, operator: '==', value: ftc, label: Family Tax Credit} + - measure_id: mftc_entitlement_nzd + label: Aggregate Minimum Family Tax Credit entitlement + ordinal: 1 + column: D + source_column_id: MFTC + expected_column_header_row: 63 + expected_column_header: MFTC + concept: ird_wff_statistics.aggregate_entitlement + source_concept: ird_wff_statistics.aggregate_entitlement + concept_relation: source_label + concept_authority: ird + concept_evidence_url: https://www.ird.govt.nz/about-us/tax-statistics/working-for-families-statistics/working-for-families-statistics-datasets + concept_evidence_notes: Workbook aggregate entitlements ($ million), MFTC column, 2024 tax year. + unit: nzd + aggregation: sum + value_scale: 1000000 + filters: {wff_credit_component: mftc} + constraints: + - {variable: wff_credit_component, operator: '==', value: mftc, label: Minimum Family Tax Credit} + - measure_id: iwtc_entitlement_nzd + label: Aggregate In-work Tax Credit entitlement + ordinal: 2 + column: F + source_column_id: IWTC + expected_column_header_row: 63 + expected_column_header: IWTC + concept: ird_wff_statistics.aggregate_entitlement + source_concept: ird_wff_statistics.aggregate_entitlement + concept_relation: source_label + concept_authority: ird + concept_evidence_url: https://www.ird.govt.nz/about-us/tax-statistics/working-for-families-statistics/working-for-families-statistics-datasets + concept_evidence_notes: Workbook aggregate entitlements ($ million), IWTC column, 2024 tax year. + unit: nzd + aggregation: sum + value_scale: 1000000 + filters: {wff_credit_component: iwtc} + constraints: + - {variable: wff_credit_component, operator: '==', value: iwtc, label: In-work Tax Credit} + - measure_id: bstc_entitlement_nzd + label: Aggregate Best Start Tax Credit entitlement + ordinal: 3 + column: G + source_column_id: BSTC + expected_column_header_row: 63 + expected_column_header: BSTC + concept: ird_wff_statistics.aggregate_entitlement + source_concept: ird_wff_statistics.aggregate_entitlement + concept_relation: source_label + concept_authority: ird + concept_evidence_url: https://www.ird.govt.nz/about-us/tax-statistics/working-for-families-statistics/working-for-families-statistics-datasets + concept_evidence_notes: Workbook aggregate entitlements ($ million), BSTC column, 2024 tax year. + unit: nzd + aggregation: sum + value_scale: 1000000 + filters: {wff_credit_component: bstc} + constraints: + - {variable: wff_credit_component, operator: '==', value: bstc, label: Best Start Tax Credit} + - measure_id: wff_entitlement_nzd + label: Aggregate Working for Families entitlement + ordinal: 4 + column: H + source_column_id: total + expected_column_header_row: 63 + expected_column_header: Total + concept: ird_wff_statistics.aggregate_entitlement + source_concept: ird_wff_statistics.aggregate_entitlement + concept_relation: source_label + concept_authority: ird + concept_evidence_url: https://www.ird.govt.nz/about-us/tax-statistics/working-for-families-statistics/working-for-families-statistics-datasets + concept_evidence_notes: Workbook aggregate entitlements ($ million), Total column, 2024 tax year. + unit: nzd + aggregation: sum + value_scale: 1000000 + filters: {wff_credit_component: any} + constraints: + - {variable: wff_credit_component, operator: '==', value: any, label: All Working for Families credits} + +- record_set_id: ird_wff_statistics.ty2024.supported_children + provenance_class: administrative + record_set_spec_id: ird_wff_statistics.supported_children.v1 + source_record_id_prefix: ird_wff_statistics.ty2024.supported_children + sheet_name: Working for families data + period_type: tax_year + period: 2024 + period_coverage: *tax_year_coverage + geography_id: NZ + geography_level: country + geography_name: New Zealand + geography_vintage: current + entity: person + entity_role: wff_supported_child + domain: working_for_families + groupby_dimension: ird_wff_statistics.supported_children + rows: + - value_id: all + label: 2024 tax year + ordinal: 0 + row_number: 99 + expected_row_header_column: A + expected_row_header: '2024-03-01T00:00:00' + guard_cells: + - column: A + row: 90 + expected_value: Working for Families Tax Credits - number of children supported + label: table title + table_record_kind: total + measures: + - measure_id: wff_supported_children + label: Children supported by Working for Families + ordinal: 0 + column: B + source_column_id: total_number_of_children + expected_column_header_row: 92 + expected_column_header: Total number of children (000's) + concept: ird_wff_statistics.supported_children + source_concept: ird_wff_statistics.supported_children + concept_relation: source_label + concept_authority: ird + concept_evidence_url: https://www.ird.govt.nz/about-us/tax-statistics/working-for-families-statistics/working-for-families-statistics-technical-information + concept_evidence_notes: >- + Workbook total number of children supported, 2024 tax year. Inland Revenue + states that child counts are adjusted for known shared care, may retain + unknown shared-care double counting, and use the maximum family child count + at the start or end of the year. + unit: count + aggregation: sum + value_scale: 1000 + +- record_set_id: ird_wff_statistics.ty2024.recipient_families_by_child_count + provenance_class: administrative + record_set_spec_id: ird_wff_statistics.recipient_families_by_child_count.v1 + source_record_id_prefix: ird_wff_statistics.ty2024.recipient_families_by_child_count + sheet_name: Working for families data + period_type: tax_year + period: 2024 + period_coverage: *tax_year_coverage + geography_id: NZ + geography_level: country + geography_name: New Zealand + geography_vintage: current + entity: family + entity_role: wff_entitled_family + domain: working_for_families + groupby_dimension: ird_wff_statistics.supported_child_count + rows: + - value_id: all + label: 2024 tax year + ordinal: 0 + row_number: 99 + expected_row_header_column: A + expected_row_header: '2024-03-01T00:00:00' + guard_cells: + - column: C + row: 92 + expected_value: Number children per family (count of families 000's) + label: family-size table header + table_record_kind: total + measures: + - measure_id: wff_families_1_child + label: Working for Families families with 1 supported child + ordinal: 0 + column: C + source_column_id: 1_child + expected_column_header_row: 93 + expected_column_header: 1 Child + concept: ird_wff_statistics.recipient_families + source_concept: ird_wff_statistics.recipient_families + concept_relation: source_label + concept_authority: ird + concept_evidence_url: https://www.ird.govt.nz/about-us/tax-statistics/working-for-families-statistics/working-for-families-statistics-technical-information + concept_evidence_notes: Workbook number of families by supported child count, 2024 tax year. + unit: count + aggregation: sum + value_scale: 1000 + constraints: + - {variable: supported_child_count, operator: '==', value: 1, unit: children, label: One supported child} + - measure_id: wff_families_2_children + label: Working for Families families with 2 supported children + ordinal: 1 + column: D + source_column_id: 2_children + expected_column_header_row: 93 + expected_column_header: 2 Children + concept: ird_wff_statistics.recipient_families + source_concept: ird_wff_statistics.recipient_families + concept_relation: source_label + concept_authority: ird + concept_evidence_url: https://www.ird.govt.nz/about-us/tax-statistics/working-for-families-statistics/working-for-families-statistics-technical-information + concept_evidence_notes: Workbook number of families by supported child count, 2024 tax year. + unit: count + aggregation: sum + value_scale: 1000 + constraints: + - {variable: supported_child_count, operator: '==', value: 2, unit: children, label: Two supported children} + - measure_id: wff_families_3_children + label: Working for Families families with 3 supported children + ordinal: 2 + column: E + source_column_id: 3_children + expected_column_header_row: 93 + expected_column_header: 3 Children + concept: ird_wff_statistics.recipient_families + source_concept: ird_wff_statistics.recipient_families + concept_relation: source_label + concept_authority: ird + concept_evidence_url: https://www.ird.govt.nz/about-us/tax-statistics/working-for-families-statistics/working-for-families-statistics-technical-information + concept_evidence_notes: Workbook number of families by supported child count, 2024 tax year. + unit: count + aggregation: sum + value_scale: 1000 + constraints: + - {variable: supported_child_count, operator: '==', value: 3, unit: children, label: Three supported children} + - measure_id: wff_families_4_children + label: Working for Families families with 4 supported children + ordinal: 3 + column: F + source_column_id: 4_children + expected_column_header_row: 93 + expected_column_header: 4 Children + concept: ird_wff_statistics.recipient_families + source_concept: ird_wff_statistics.recipient_families + concept_relation: source_label + concept_authority: ird + concept_evidence_url: https://www.ird.govt.nz/about-us/tax-statistics/working-for-families-statistics/working-for-families-statistics-technical-information + concept_evidence_notes: Workbook number of families by supported child count, 2024 tax year. + unit: count + aggregation: sum + value_scale: 1000 + constraints: + - {variable: supported_child_count, operator: '==', value: 4, unit: children, label: Four supported children} + - measure_id: wff_families_5_children + label: Working for Families families with 5 supported children + ordinal: 4 + column: G + source_column_id: 5_children + expected_column_header_row: 93 + expected_column_header: 5 Children + concept: ird_wff_statistics.recipient_families + source_concept: ird_wff_statistics.recipient_families + concept_relation: source_label + concept_authority: ird + concept_evidence_url: https://www.ird.govt.nz/about-us/tax-statistics/working-for-families-statistics/working-for-families-statistics-technical-information + concept_evidence_notes: Workbook number of families by supported child count, 2024 tax year. + unit: count + aggregation: sum + value_scale: 1000 + constraints: + - {variable: supported_child_count, operator: '==', value: 5, unit: children, label: Five supported children} + - measure_id: wff_families_6_children + label: Working for Families families with 6 supported children + ordinal: 5 + column: H + source_column_id: 6_children + expected_column_header_row: 93 + expected_column_header: 6 Children + concept: ird_wff_statistics.recipient_families + source_concept: ird_wff_statistics.recipient_families + concept_relation: source_label + concept_authority: ird + concept_evidence_url: https://www.ird.govt.nz/about-us/tax-statistics/working-for-families-statistics/working-for-families-statistics-technical-information + concept_evidence_notes: Workbook number of families by supported child count, 2024 tax year. + unit: count + aggregation: sum + value_scale: 1000 + constraints: + - {variable: supported_child_count, operator: '==', value: 6, unit: children, label: Six supported children} + - measure_id: wff_families_7_plus_children + label: Working for Families families with 7 or more supported children + ordinal: 6 + column: I + source_column_id: 7_plus_children + expected_column_header_row: 93 + expected_column_header: 7+ Children + concept: ird_wff_statistics.recipient_families + source_concept: ird_wff_statistics.recipient_families + concept_relation: source_label + concept_authority: ird + concept_evidence_url: https://www.ird.govt.nz/about-us/tax-statistics/working-for-families-statistics/working-for-families-statistics-technical-information + concept_evidence_notes: Workbook number of families by supported child count, 2024 tax year. + unit: count + aggregation: sum + value_scale: 1000 + constraints: + - {variable: supported_child_count, operator: '>=', value: 7, unit: children, label: Seven or more supported children} + +- record_set_id: ird_wff_statistics.ty2024.income_distribution + provenance_class: administrative + record_set_spec_id: ird_wff_statistics.income_distribution.v1 + source_record_id_prefix: ird_wff_statistics.ty2024.income_distribution + sheet_name: Working for families income + period_type: tax_year + period: 2024 + period_coverage: *tax_year_coverage + geography_id: NZ + geography_level: country + geography_name: New Zealand + geography_vintage: current + entity: family + entity_role: wff_entitled_family + domain: working_for_families + groupby_dimension: ird_wff_statistics.family_income_band_source_label + rows: + - value_id: income_label_5000 + label: 'Published family income band 5000' + ordinal: 0 + row_number: 12 + expected_row_header_column: AO + expected_row_header: 5000 + table_record_kind: detail + filters: + family_scheme_income_band_source_label: 5000 + constraints: + - variable: family_scheme_income_band_source_label + operator: == + value: 5000 + label: Published Working for Families family-income band label + guard_cells: &income_table_guards + - column: AO + row: 10 + expected_value: 2023-24 Tax Year + label: income table tax-year label + - column: AO + row: 11 + expected_value: Family income band ($)* + label: income table band header + - column: AO + row: 52 + expected_value: '* The tables only include information for families eligible for Working for Families tax credits in that tax year. Families not eligible are excluded.' + label: income table population footnote + - value_id: income_label_10000 + label: 'Published family income band 10000' + ordinal: 1 + row_number: 13 + expected_row_header_column: AO + expected_row_header: 10000 + table_record_kind: detail + filters: + family_scheme_income_band_source_label: 10000 + constraints: + - variable: family_scheme_income_band_source_label + operator: == + value: 10000 + label: Published Working for Families family-income band label + guard_cells: *income_table_guards + - value_id: income_label_15000 + label: 'Published family income band 15000' + ordinal: 2 + row_number: 14 + expected_row_header_column: AO + expected_row_header: 15000 + table_record_kind: detail + filters: + family_scheme_income_band_source_label: 15000 + constraints: + - variable: family_scheme_income_band_source_label + operator: == + value: 15000 + label: Published Working for Families family-income band label + guard_cells: *income_table_guards + - value_id: income_label_20000 + label: 'Published family income band 20000' + ordinal: 3 + row_number: 15 + expected_row_header_column: AO + expected_row_header: 20000 + table_record_kind: detail + filters: + family_scheme_income_band_source_label: 20000 + constraints: + - variable: family_scheme_income_band_source_label + operator: == + value: 20000 + label: Published Working for Families family-income band label + guard_cells: *income_table_guards + - value_id: income_label_25000 + label: 'Published family income band 25000' + ordinal: 4 + row_number: 16 + expected_row_header_column: AO + expected_row_header: 25000 + table_record_kind: detail + filters: + family_scheme_income_band_source_label: 25000 + constraints: + - variable: family_scheme_income_band_source_label + operator: == + value: 25000 + label: Published Working for Families family-income band label + guard_cells: *income_table_guards + - value_id: income_label_30000 + label: 'Published family income band 30000' + ordinal: 5 + row_number: 17 + expected_row_header_column: AO + expected_row_header: 30000 + table_record_kind: detail + filters: + family_scheme_income_band_source_label: 30000 + constraints: + - variable: family_scheme_income_band_source_label + operator: == + value: 30000 + label: Published Working for Families family-income band label + guard_cells: *income_table_guards + - value_id: income_label_35000 + label: 'Published family income band 35000' + ordinal: 6 + row_number: 18 + expected_row_header_column: AO + expected_row_header: 35000 + table_record_kind: detail + filters: + family_scheme_income_band_source_label: 35000 + constraints: + - variable: family_scheme_income_band_source_label + operator: == + value: 35000 + label: Published Working for Families family-income band label + guard_cells: *income_table_guards + - value_id: income_label_40000 + label: 'Published family income band 40000' + ordinal: 7 + row_number: 19 + expected_row_header_column: AO + expected_row_header: 40000 + table_record_kind: detail + filters: + family_scheme_income_band_source_label: 40000 + constraints: + - variable: family_scheme_income_band_source_label + operator: == + value: 40000 + label: Published Working for Families family-income band label + guard_cells: *income_table_guards + - value_id: income_label_45000 + label: 'Published family income band 45000' + ordinal: 8 + row_number: 20 + expected_row_header_column: AO + expected_row_header: 45000 + table_record_kind: detail + filters: + family_scheme_income_band_source_label: 45000 + constraints: + - variable: family_scheme_income_band_source_label + operator: == + value: 45000 + label: Published Working for Families family-income band label + guard_cells: *income_table_guards + - value_id: income_label_50000 + label: 'Published family income band 50000' + ordinal: 9 + row_number: 21 + expected_row_header_column: AO + expected_row_header: 50000 + table_record_kind: detail + filters: + family_scheme_income_band_source_label: 50000 + constraints: + - variable: family_scheme_income_band_source_label + operator: == + value: 50000 + label: Published Working for Families family-income band label + guard_cells: *income_table_guards + - value_id: income_label_55000 + label: 'Published family income band 55000' + ordinal: 10 + row_number: 22 + expected_row_header_column: AO + expected_row_header: 55000 + table_record_kind: detail + filters: + family_scheme_income_band_source_label: 55000 + constraints: + - variable: family_scheme_income_band_source_label + operator: == + value: 55000 + label: Published Working for Families family-income band label + guard_cells: *income_table_guards + - value_id: income_label_60000 + label: 'Published family income band 60000' + ordinal: 11 + row_number: 23 + expected_row_header_column: AO + expected_row_header: 60000 + table_record_kind: detail + filters: + family_scheme_income_band_source_label: 60000 + constraints: + - variable: family_scheme_income_band_source_label + operator: == + value: 60000 + label: Published Working for Families family-income band label + guard_cells: *income_table_guards + - value_id: income_label_65000 + label: 'Published family income band 65000' + ordinal: 12 + row_number: 24 + expected_row_header_column: AO + expected_row_header: 65000 + table_record_kind: detail + filters: + family_scheme_income_band_source_label: 65000 + constraints: + - variable: family_scheme_income_band_source_label + operator: == + value: 65000 + label: Published Working for Families family-income band label + guard_cells: *income_table_guards + - value_id: income_label_70000 + label: 'Published family income band 70000' + ordinal: 13 + row_number: 25 + expected_row_header_column: AO + expected_row_header: 70000 + table_record_kind: detail + filters: + family_scheme_income_band_source_label: 70000 + constraints: + - variable: family_scheme_income_band_source_label + operator: == + value: 70000 + label: Published Working for Families family-income band label + guard_cells: *income_table_guards + - value_id: income_label_75000 + label: 'Published family income band 75000' + ordinal: 14 + row_number: 26 + expected_row_header_column: AO + expected_row_header: 75000 + table_record_kind: detail + filters: + family_scheme_income_band_source_label: 75000 + constraints: + - variable: family_scheme_income_band_source_label + operator: == + value: 75000 + label: Published Working for Families family-income band label + guard_cells: *income_table_guards + - value_id: income_label_80000 + label: 'Published family income band 80000' + ordinal: 15 + row_number: 27 + expected_row_header_column: AO + expected_row_header: 80000 + table_record_kind: detail + filters: + family_scheme_income_band_source_label: 80000 + constraints: + - variable: family_scheme_income_band_source_label + operator: == + value: 80000 + label: Published Working for Families family-income band label + guard_cells: *income_table_guards + - value_id: income_label_85000 + label: 'Published family income band 85000' + ordinal: 16 + row_number: 28 + expected_row_header_column: AO + expected_row_header: 85000 + table_record_kind: detail + filters: + family_scheme_income_band_source_label: 85000 + constraints: + - variable: family_scheme_income_band_source_label + operator: == + value: 85000 + label: Published Working for Families family-income band label + guard_cells: *income_table_guards + - value_id: income_label_90000 + label: 'Published family income band 90000' + ordinal: 17 + row_number: 29 + expected_row_header_column: AO + expected_row_header: 90000 + table_record_kind: detail + filters: + family_scheme_income_band_source_label: 90000 + constraints: + - variable: family_scheme_income_band_source_label + operator: == + value: 90000 + label: Published Working for Families family-income band label + guard_cells: *income_table_guards + - value_id: income_label_95000 + label: 'Published family income band 95000' + ordinal: 18 + row_number: 30 + expected_row_header_column: AO + expected_row_header: 95000 + table_record_kind: detail + filters: + family_scheme_income_band_source_label: 95000 + constraints: + - variable: family_scheme_income_band_source_label + operator: == + value: 95000 + label: Published Working for Families family-income band label + guard_cells: *income_table_guards + - value_id: income_label_100000 + label: 'Published family income band 100000' + ordinal: 19 + row_number: 31 + expected_row_header_column: AO + expected_row_header: 100000 + table_record_kind: detail + filters: + family_scheme_income_band_source_label: 100000 + constraints: + - variable: family_scheme_income_band_source_label + operator: == + value: 100000 + label: Published Working for Families family-income band label + guard_cells: *income_table_guards + - value_id: income_label_105000 + label: 'Published family income band 105000' + ordinal: 20 + row_number: 32 + expected_row_header_column: AO + expected_row_header: 105000 + table_record_kind: detail + filters: + family_scheme_income_band_source_label: 105000 + constraints: + - variable: family_scheme_income_band_source_label + operator: == + value: 105000 + label: Published Working for Families family-income band label + guard_cells: *income_table_guards + - value_id: income_label_110000 + label: 'Published family income band 110000' + ordinal: 21 + row_number: 33 + expected_row_header_column: AO + expected_row_header: 110000 + table_record_kind: detail + filters: + family_scheme_income_band_source_label: 110000 + constraints: + - variable: family_scheme_income_band_source_label + operator: == + value: 110000 + label: Published Working for Families family-income band label + guard_cells: *income_table_guards + - value_id: income_label_115000 + label: 'Published family income band 115000' + ordinal: 22 + row_number: 34 + expected_row_header_column: AO + expected_row_header: 115000 + table_record_kind: detail + filters: + family_scheme_income_band_source_label: 115000 + constraints: + - variable: family_scheme_income_band_source_label + operator: == + value: 115000 + label: Published Working for Families family-income band label + guard_cells: *income_table_guards + - value_id: income_label_120000 + label: 'Published family income band 120000' + ordinal: 23 + row_number: 35 + expected_row_header_column: AO + expected_row_header: 120000 + table_record_kind: detail + filters: + family_scheme_income_band_source_label: 120000 + constraints: + - variable: family_scheme_income_band_source_label + operator: == + value: 120000 + label: Published Working for Families family-income band label + guard_cells: *income_table_guards + - value_id: income_label_125000 + label: 'Published family income band 125000' + ordinal: 24 + row_number: 36 + expected_row_header_column: AO + expected_row_header: 125000 + table_record_kind: detail + filters: + family_scheme_income_band_source_label: 125000 + constraints: + - variable: family_scheme_income_band_source_label + operator: == + value: 125000 + label: Published Working for Families family-income band label + guard_cells: *income_table_guards + - value_id: income_label_130000 + label: 'Published family income band 130000' + ordinal: 25 + row_number: 37 + expected_row_header_column: AO + expected_row_header: 130000 + table_record_kind: detail + filters: + family_scheme_income_band_source_label: 130000 + constraints: + - variable: family_scheme_income_band_source_label + operator: == + value: 130000 + label: Published Working for Families family-income band label + guard_cells: *income_table_guards + - value_id: income_label_135000 + label: 'Published family income band 135000' + ordinal: 26 + row_number: 38 + expected_row_header_column: AO + expected_row_header: 135000 + table_record_kind: detail + filters: + family_scheme_income_band_source_label: 135000 + constraints: + - variable: family_scheme_income_band_source_label + operator: == + value: 135000 + label: Published Working for Families family-income band label + guard_cells: *income_table_guards + - value_id: income_label_140000 + label: 'Published family income band 140000' + ordinal: 27 + row_number: 39 + expected_row_header_column: AO + expected_row_header: 140000 + table_record_kind: detail + filters: + family_scheme_income_band_source_label: 140000 + constraints: + - variable: family_scheme_income_band_source_label + operator: == + value: 140000 + label: Published Working for Families family-income band label + guard_cells: *income_table_guards + - value_id: income_label_145000 + label: 'Published family income band 145000' + ordinal: 28 + row_number: 40 + expected_row_header_column: AO + expected_row_header: 145000 + table_record_kind: detail + filters: + family_scheme_income_band_source_label: 145000 + constraints: + - variable: family_scheme_income_band_source_label + operator: == + value: 145000 + label: Published Working for Families family-income band label + guard_cells: *income_table_guards + - value_id: income_label_150000 + label: 'Published family income band 150000' + ordinal: 29 + row_number: 41 + expected_row_header_column: AO + expected_row_header: 150000 + table_record_kind: detail + filters: + family_scheme_income_band_source_label: 150000 + constraints: + - variable: family_scheme_income_band_source_label + operator: == + value: 150000 + label: Published Working for Families family-income band label + guard_cells: *income_table_guards + - value_id: income_label_155000 + label: 'Published family income band 155000' + ordinal: 30 + row_number: 42 + expected_row_header_column: AO + expected_row_header: 155000 + table_record_kind: detail + filters: + family_scheme_income_band_source_label: 155000 + constraints: + - variable: family_scheme_income_band_source_label + operator: == + value: 155000 + label: Published Working for Families family-income band label + guard_cells: *income_table_guards + - value_id: income_label_160000 + label: 'Published family income band 160000' + ordinal: 31 + row_number: 43 + expected_row_header_column: AO + expected_row_header: 160000 + table_record_kind: detail + filters: + family_scheme_income_band_source_label: 160000 + constraints: + - variable: family_scheme_income_band_source_label + operator: == + value: 160000 + label: Published Working for Families family-income band label + guard_cells: *income_table_guards + - value_id: income_label_165000 + label: 'Published family income band 165000' + ordinal: 32 + row_number: 44 + expected_row_header_column: AO + expected_row_header: 165000 + table_record_kind: detail + filters: + family_scheme_income_band_source_label: 165000 + constraints: + - variable: family_scheme_income_band_source_label + operator: == + value: 165000 + label: Published Working for Families family-income band label + guard_cells: *income_table_guards + - value_id: income_label_170000 + label: 'Published family income band 170000' + ordinal: 33 + row_number: 45 + expected_row_header_column: AO + expected_row_header: 170000 + table_record_kind: detail + filters: + family_scheme_income_band_source_label: 170000 + constraints: + - variable: family_scheme_income_band_source_label + operator: == + value: 170000 + label: Published Working for Families family-income band label + guard_cells: *income_table_guards + - value_id: income_label_175000 + label: 'Published family income band 175000' + ordinal: 34 + row_number: 46 + expected_row_header_column: AO + expected_row_header: 175000 + table_record_kind: detail + filters: + family_scheme_income_band_source_label: 175000 + constraints: + - variable: family_scheme_income_band_source_label + operator: == + value: 175000 + label: Published Working for Families family-income band label + guard_cells: *income_table_guards + - value_id: income_label_180000 + label: 'Published family income band 180000' + ordinal: 35 + row_number: 47 + expected_row_header_column: AO + expected_row_header: 180000 + table_record_kind: detail + filters: + family_scheme_income_band_source_label: 180000 + constraints: + - variable: family_scheme_income_band_source_label + operator: == + value: 180000 + label: Published Working for Families family-income band label + guard_cells: *income_table_guards + - value_id: income_label_180000_plus + label: 'Published family income band 180,000+' + ordinal: 36 + row_number: 48 + expected_row_header_column: AO + expected_row_header: '180,000+' + table_record_kind: detail + filters: + family_scheme_income_band_source_label: '180,000+' + constraints: + - variable: family_scheme_income_band_source_label + operator: == + value: '180,000+' + label: Published Working for Families family-income band label + guard_cells: *income_table_guards + - value_id: income_unknown + label: 'Published family income band Unknown**' + ordinal: 37 + row_number: 49 + expected_row_header_column: AO + expected_row_header: 'Unknown**' + table_record_kind: detail + filters: + family_scheme_income_band_source_label: 'Unknown**' + constraints: + - variable: family_scheme_income_band_source_label + operator: == + value: 'Unknown**' + label: Published Working for Families family-income band label + guard_cells: *income_table_guards + - value_id: all_income_bands + label: 'Published family income band All' + ordinal: 38 + row_number: 51 + expected_row_header_column: AO + expected_row_header: All + table_record_kind: total + filters: + family_scheme_income_band_source_label: All + constraints: + - variable: family_scheme_income_band_source_label + operator: == + value: All + label: Published Working for Families family-income band label + guard_cells: *income_table_guards + measures: + - measure_id: wff_entitlement_nzd + label: Aggregate Working for Families entitlement by published family-income band + ordinal: 0 + column: AP + source_column_id: total_entitlement + expected_column_header_row: 11 + expected_column_header: "Total entitlement \n($million)" + concept: ird_wff_statistics.aggregate_entitlement + source_concept: ird_wff_statistics.aggregate_entitlement + concept_relation: source_label + concept_authority: ird + concept_evidence_url: https://www.ird.govt.nz/about-us/tax-statistics/working-for-families-statistics/working-for-families-statistics-datasets + concept_evidence_notes: >- + Publisher 2023-24 tax-year income table, All Working for Families credits column. + Family-income labels are preserved exactly, including Unknown and All; + numeric labels are not recoded to inferred interval boundaries. + The total includes credit components not separately shown in this table. + unit: nzd + aggregation: sum + value_scale: 1000000 + expected_cell_type: number + filters: {wff_credit_component: any} + constraints: + - {variable: wff_credit_component, operator: '==', value: any, label: All Working for Families credits} + - measure_id: ftc_entitlement_nzd + label: Aggregate Family Tax Credit entitlement by published family-income band + ordinal: 1 + column: AQ + source_column_id: ftc_entitlement + expected_column_header_row: 11 + expected_column_header: "FTC entitlement\n($million)" + concept: ird_wff_statistics.aggregate_entitlement + source_concept: ird_wff_statistics.aggregate_entitlement + concept_relation: source_label + concept_authority: ird + concept_evidence_url: https://www.ird.govt.nz/about-us/tax-statistics/working-for-families-statistics/working-for-families-statistics-datasets + concept_evidence_notes: >- + Publisher 2023-24 tax-year income table, Family Tax Credit column. + Family-income labels are preserved exactly, including Unknown and All; + numeric labels are not recoded to inferred interval boundaries. + The total includes credit components not separately shown in this table. + unit: nzd + aggregation: sum + value_scale: 1000000 + expected_cell_type: number + filters: {wff_credit_component: ftc} + constraints: + - {variable: wff_credit_component, operator: '==', value: ftc, label: Family Tax Credit} + - measure_id: iwtc_entitlement_nzd + label: Aggregate In-work Tax Credit entitlement by published family-income band + ordinal: 2 + column: AR + source_column_id: iwtc_entitlement + expected_column_header_row: 11 + expected_column_header: "IWTC entitlement\n($million)" + concept: ird_wff_statistics.aggregate_entitlement + source_concept: ird_wff_statistics.aggregate_entitlement + concept_relation: source_label + concept_authority: ird + concept_evidence_url: https://www.ird.govt.nz/about-us/tax-statistics/working-for-families-statistics/working-for-families-statistics-datasets + concept_evidence_notes: >- + Publisher 2023-24 tax-year income table, In-work Tax Credit column. + Family-income labels are preserved exactly, including Unknown and All; + numeric labels are not recoded to inferred interval boundaries. + The total includes credit components not separately shown in this table. + unit: nzd + aggregation: sum + value_scale: 1000000 + expected_cell_type: number + filters: {wff_credit_component: iwtc} + constraints: + - {variable: wff_credit_component, operator: '==', value: iwtc, label: In-work Tax Credit} + - measure_id: bstc_entitlement_nzd + label: Aggregate Best Start Tax Credit entitlement by published family-income band + ordinal: 3 + column: AS + source_column_id: bstc_entitlement + expected_column_header_row: 11 + expected_column_header: "BSTC entitlement\n($million)" + concept: ird_wff_statistics.aggregate_entitlement + source_concept: ird_wff_statistics.aggregate_entitlement + concept_relation: source_label + concept_authority: ird + concept_evidence_url: https://www.ird.govt.nz/about-us/tax-statistics/working-for-families-statistics/working-for-families-statistics-datasets + concept_evidence_notes: >- + Publisher 2023-24 tax-year income table, Best Start Tax Credit column. + Family-income labels are preserved exactly, including Unknown and All; + numeric labels are not recoded to inferred interval boundaries. + The total includes credit components not separately shown in this table. + unit: nzd + aggregation: sum + value_scale: 1000000 + expected_cell_type: number + filters: {wff_credit_component: bstc} + constraints: + - {variable: wff_credit_component, operator: '==', value: bstc, label: Best Start Tax Credit} + - measure_id: wff_recipient_families + label: Families with at least one Working for Families credit by published family-income band + ordinal: 4 + column: AT + source_column_id: wff_count + expected_column_header_row: 11 + expected_column_header: "Count of WFF " + concept: ird_wff_statistics.recipient_families + source_concept: ird_wff_statistics.recipient_families + concept_relation: source_label + concept_authority: ird + concept_evidence_url: https://www.ird.govt.nz/about-us/tax-statistics/working-for-families-statistics/working-for-families-statistics-datasets + concept_evidence_notes: >- + Publisher 2023-24 tax-year income table, At least one Working for Families credit column. + Family-income labels are preserved exactly, including Unknown and All; + numeric labels are not recoded to inferred interval boundaries. + The total includes credit components not separately shown in this table. + unit: count + aggregation: sum + value_scale: 1 + expected_cell_type: number + filters: {wff_credit_component: any} + constraints: + - {variable: wff_credit_component, operator: '==', value: any, label: At least one Working for Families credit} + - measure_id: ftc_recipient_families + label: Families with non-zero Family Tax Credit entitlement by published family-income band + ordinal: 5 + column: AU + source_column_id: ftc_count + expected_column_header_row: 11 + expected_column_header: "Count of FTC" + concept: ird_wff_statistics.recipient_families + source_concept: ird_wff_statistics.recipient_families + concept_relation: source_label + concept_authority: ird + concept_evidence_url: https://www.ird.govt.nz/about-us/tax-statistics/working-for-families-statistics/working-for-families-statistics-datasets + concept_evidence_notes: >- + Publisher 2023-24 tax-year income table, Family Tax Credit column. + Family-income labels are preserved exactly, including Unknown and All; + numeric labels are not recoded to inferred interval boundaries. + The total includes credit components not separately shown in this table. + unit: count + aggregation: sum + value_scale: 1 + expected_cell_type: number + filters: {wff_credit_component: ftc} + constraints: + - {variable: wff_credit_component, operator: '==', value: ftc, label: Family Tax Credit} + - measure_id: iwtc_recipient_families + label: Families with non-zero In-work Tax Credit entitlement by published family-income band + ordinal: 6 + column: AV + source_column_id: iwtc_count + expected_column_header_row: 11 + expected_column_header: "Count of IWTC" + concept: ird_wff_statistics.recipient_families + source_concept: ird_wff_statistics.recipient_families + concept_relation: source_label + concept_authority: ird + concept_evidence_url: https://www.ird.govt.nz/about-us/tax-statistics/working-for-families-statistics/working-for-families-statistics-datasets + concept_evidence_notes: >- + Publisher 2023-24 tax-year income table, In-work Tax Credit column. + Family-income labels are preserved exactly, including Unknown and All; + numeric labels are not recoded to inferred interval boundaries. + The total includes credit components not separately shown in this table. + unit: count + aggregation: sum + value_scale: 1 + expected_cell_type: number + filters: {wff_credit_component: iwtc} + constraints: + - {variable: wff_credit_component, operator: '==', value: iwtc, label: In-work Tax Credit} + - measure_id: bstc_recipient_families + label: Families with non-zero Best Start Tax Credit entitlement by published family-income band + ordinal: 7 + column: AW + source_column_id: bstc_count + expected_column_header_row: 11 + expected_column_header: "Count of BSTC" + concept: ird_wff_statistics.recipient_families + source_concept: ird_wff_statistics.recipient_families + concept_relation: source_label + concept_authority: ird + concept_evidence_url: https://www.ird.govt.nz/about-us/tax-statistics/working-for-families-statistics/working-for-families-statistics-datasets + concept_evidence_notes: >- + Publisher 2023-24 tax-year income table, Best Start Tax Credit column. + Family-income labels are preserved exactly, including Unknown and All; + numeric labels are not recoded to inferred interval boundaries. + The total includes credit components not separately shown in this table. + unit: count + aggregation: sum + value_scale: 1 + expected_cell_type: number + filters: {wff_credit_component: bstc} + constraints: + - {variable: wff_credit_component, operator: '==', value: bstc, label: Best Start Tax Credit} diff --git a/tests/test_chronicle_bundle.py b/tests/test_chronicle_bundle.py index 6a00ef84..d49b8b74 100644 --- a/tests/test_chronicle_bundle.py +++ b/tests/test_chronicle_bundle.py @@ -73,16 +73,16 @@ def test_build_bundle_writes_merged_consumer_contract(tmp_path): "aggregate_duplicate_key_count": 0, "entity_count": 12, "error_count": 0, - "fact_count": 171855, - "geography_count": 12536, + "fact_count": 172185, + "geography_count": 12537, "period_count": 192, "semantic_duplicate_key_count": 121, "skipped_source_count": 10, - "source_count": 43, - "source_package_count": 151, + "source_count": 44, + "source_package_count": 152, "warning_count": 1, } - assert len(rows) == 171855 + assert len(rows) == 172185 assert {row["provenance_class"] for row in rows} <= { "administrative", "census", @@ -100,7 +100,7 @@ def test_build_bundle_writes_merged_consumer_contract(tmp_path): ) assert rows[0]["aggregate_fact_key"].startswith("ledger.aggregate_fact.v2:") assert rows[0]["semantic_fact_key"].startswith("ledger.semantic_fact.v2:") - assert source_packages["source_package_count"] == 151 + assert source_packages["source_package_count"] == 152 assert source_packages["skipped_source_count"] == 10 assert sorted(item["source"] for item in source_packages["skipped_sources"]) == [ "census-acs-s0101-congressional-district-age-2024", @@ -114,7 +114,7 @@ def test_build_bundle_writes_merged_consumer_contract(tmp_path): "jct-obbba-revenue-estimates-2025", "jct-tax-expenditures-2024", ] - assert coverage["fact_count"] == 171855 + assert coverage["fact_count"] == 172185 assert coverage["counts"]["by_source"] == { "bea": 445, "bfp_economic_outlook": 5, @@ -135,6 +135,7 @@ def test_build_bundle_writes_merged_consumer_contract(tmp_path): "hhs_acf_tanf": 110, "hmrc": 20551, "ici": 12, + "ird": 330, "irs_soi": 40063, "isc": 2, "jrc_euromod_be": 90, @@ -161,7 +162,8 @@ def test_build_bundle_writes_merged_consumer_contract(tmp_path): "welshgov": 216, } table_counts = coverage["counts"]["by_source_table"] - assert len(table_counts) == 146 + assert len(table_counts) == 147 + assert table_counts["ird:Working for Families statistics - September 2025"] == 330 assert ( table_counts[ "dwp:Universal Credit childcare element statistics to August 2025, Table 1" @@ -673,8 +675,9 @@ def test_build_bundle_writes_merged_consumer_contract(tmp_path): "tax_year:2021": 9, "tax_year:2022": 41237, "tax_year:2023": 63054, - "tax_year:2024": 40, + "tax_year:2024": 370, } + assert coverage["counts"]["by_geography"]["country:NZ"] == 330 assert coverage["counts"]["by_geography"]["country:BE"] == 4888 assert coverage["counts"]["by_geography"]["country:DE"] == 36 assert coverage["counts"]["by_geography"]["country:FR"] == 36 @@ -689,17 +692,17 @@ def test_build_bundle_writes_merged_consumer_contract(tmp_path): ) assert coverage["counts"]["by_geography"]["country:K02000001"] == 4297 assert coverage["counts"]["by_geography"]["country:K03000001"] == 497 - assert len(coverage["counts"]["by_geography"]) == 12536 + assert len(coverage["counts"]["by_geography"]) == 12537 assert coverage["counts"]["by_entity"] == { "benefit_unit": 233, "dwelling": 12733, - "family": 107, + "family": 436, "firm": 1439, "government": 1313, "household": 40724, "institutional_sector": 133, "pension_plan": 2, - "person": 60466, + "person": 60467, "return": 14600, "social_protection_scheme": 36, "tax_unit": 40069, diff --git a/tests/test_nz_targets.py b/tests/test_nz_targets.py new file mode 100644 index 00000000..f9539fed --- /dev/null +++ b/tests/test_nz_targets.py @@ -0,0 +1,317 @@ +"""Publisher-fidelity tests for New Zealand Chronicle source packages.""" + +from __future__ import annotations + +from collections import Counter +from dataclasses import replace +from decimal import Decimal +from functools import lru_cache +import hashlib +from pathlib import Path + +import pytest +import yaml + +from chronicle.core import validate_facts +from chronicle.source_package import ( + SOURCE_PACKAGE_ALIASES, + load_source_package, + validate_source_package, +) +from chronicle.sources import build_source_cell_key, validate_source_cells + + +REPO_ROOT = Path(__file__).resolve().parents[1] +WFF_ALIAS = "ird-working-for-families-statistics-sept-2025" +WFF_DIRECTORY = Path("ird/working_for_families_statistics_sept_2025") +WFF_FILENAME = "working-for-families-statistics---sept-2025.xlsx" +WFF_SHA256 = "95ae66f4d44f3f47ea3daa006328b22f061a163cf7e31b487342cde649390833" +WFF_SOURCE_URL = ( + "https://www.ird.govt.nz/-/media/project/ir/home/documents/about-us/" + "tax-statistics---current/social-policy/wff-stats/" + f"{WFF_FILENAME}?modified=20251111195236" +) +DATA_SHEET = "Working for families data" +INCOME_SHEET = "Working for families income" +RECORD_PREFIX = "ird_wff_statistics.ty2024" + + +@lru_cache +def _package(): + return load_source_package(WFF_ALIAS) + + +@lru_cache +def _cells(): + return _package().build_source_cells(2024) + + +@lru_cache +def _facts(): + return _package().build_facts(2024, cells=_cells()) + + +@lru_cache +def _records(): + return _package().build_source_records(2024, cells=_cells()) + + +def _fact(record_set, row, measure): + record_id = f"{RECORD_PREFIX}.{record_set}.{row}.{measure}" + return next(fact for fact in _facts() if fact.source_record_id == record_id) + + +def test_wff_alias_and_package_shape(): + assert SOURCE_PACKAGE_ALIASES[WFF_ALIAS] == WFF_DIRECTORY + assert _package().package_id == WFF_ALIAS + report = validate_source_package(WFF_ALIAS, year=2024) + assert report.valid + assert not report.warnings + assert report.counts == { + "record_set_count": 5, + "row_count": 43, + "measure_count": 26, + "source_record_count": 330, + "source_region_count": 5, + } + + +def test_wff_official_artifact_is_pinned(): + data_dir = REPO_ROOT / "db" / "data" / WFF_DIRECTORY + manifest = yaml.safe_load((data_dir / "manifest.yaml").read_text()) + artifact = manifest["files"][2024] + path = data_dir / WFF_FILENAME + + assert manifest["source_id"] == "ird" + assert manifest["package_id"] == WFF_ALIAS + assert artifact["filename"] == WFF_FILENAME + assert artifact["source_url"] == WFF_SOURCE_URL + assert artifact["sha256"] == WFF_SHA256 + assert hashlib.sha256(path.read_bytes()).hexdigest() == WFF_SHA256 + assert path.stat().st_size == artifact["size_bytes"] == 71_211 + + +def test_wff_source_cells_and_fact_lineage_are_complete(): + cells = _cells() + facts = _facts() + assert len(cells) == 4910 + assert {cell.sheet_name for cell in cells} == {DATA_SHEET, INCOME_SHEET} + assert validate_source_cells(cells).valid + assert len(facts) == 330 + assert validate_facts(facts).valid + assert Counter(fact.entity.name for fact in facts) == { + "family": 329, + "person": 1, + } + assert {fact.provenance_class for fact in facts} == {"administrative"} + assert {fact.assertion for fact in facts} == {"observation"} + assert {fact.source.source_name for fact in facts} == {"ird"} + assert {fact.source.source_sha256 for fact in facts} == {WFF_SHA256} + assert {fact.source.url for fact in facts} == {WFF_SOURCE_URL} + assert {fact.measure.unit for fact in facts} == {"count", "nzd"} + assert {fact.measure.concept_relation for fact in facts} == {"source_label"} + cell_keys = {build_source_cell_key(cell) for cell in cells} + assert all(fact.source_cell_keys for fact in facts) + assert all(set(fact.source_cell_keys) <= cell_keys for fact in facts) + + # Every emitted value is one publisher cell with a declared unit scale; + # there is no interpolation, inferred residual, ratio, or reconciliation. + cells_by_address = {(cell.sheet_name, cell.address): cell for cell in cells} + records_by_id = {record.source_record_id: record for record in _records()} + for fact in facts: + record = records_by_id[fact.source_record_id] + selector = record.spec.selector + assert selector.end_address is None + assert record.spec.divisor_selector is None + raw_value = cells_by_address[(selector.sheet_name, selector.address)].raw_value + assert Decimal(str(fact.value)) == ( + Decimal(str(raw_value)) * Decimal(str(record.spec.value_scale)) + ) + + +def test_wff_nz_tax_year_and_country_are_explicit(): + for fact in _facts(): + assert (fact.period.type, fact.period.value) == ("tax_year", 2024) + assert (fact.geography.level, fact.geography.id) == ("country", "NZ") + coverage = fact.period_coverage + assert coverage.start_date == "2023-04-01" + assert coverage.end_date == "2024-03-31" + assert coverage.basis == "tax" + assert coverage.source_period_label == "2024 Tax Year / 2023-24 Tax Year" + assert coverage.accounting_basis is None + assert "not an estimate of all eligible families" in coverage.notes + + # A generic bundle's requested year must not relabel the fixed source year. + assert { + (fact.period.type, fact.period.value) + for fact in _package().build_facts(2023, cells=_cells()) + } == {("tax_year", 2024)} + + +@pytest.mark.parametrize( + ("component", "families", "entitlement"), + [ + ("ftc", 252_500, 2_273_000_000), + ("mftc", 2_600, 12_000_000), + ("iwtc", 150_500, 437_000_000), + ("bstc", 137_200, 320_000_000), + ("wff", 328_400, 3_043_000_000), + ], +) +def test_wff_national_credit_counts_and_entitlements(component, families, entitlement): + count = _fact("recipient_families", "all", f"{component}_recipient_families") + amount = _fact("aggregate_entitlements", "all", f"{component}_entitlement_nzd") + assert count.value == families + assert amount.value == entitlement + assert count.filters["wff_entitlement_status"] == "nonzero" + assert count.entity.role == amount.entity.role == "wff_entitled_family" + assert count.filters["wff_credit_component"] == ( + "any" if component == "wff" else component + ) + + +def test_wff_supported_children_and_family_sizes_remain_distinct(): + child_total = _fact("supported_children", "all", "wff_supported_children") + assert child_total.value == 656_500 + assert child_total.entity.name == "person" + counts = (136_600, 107_800, 50_900, 20_700, 7_700, 2_800, 1_100) + for child_count, expected in enumerate(counts, start=1): + suffix = ( + "1_child" + if child_count == 1 + else "7_plus_children" + if child_count == 7 + else f"{child_count}_children" + ) + fact = _fact( + "recipient_families_by_child_count", "all", f"wff_families_{suffix}" + ) + assert fact.value == expected + assert fact.entity.name == "family" + constraint = next( + c for c in fact.constraints if c.variable == "supported_child_count" + ) + assert constraint.value == child_count + assert constraint.unit == "children" + assert constraint.operator == (">=" if child_count == 7 else "==") + # Preserve distinct published counts; do not force one table to match another. + assert ( + sum(counts) + != _fact("recipient_families", "all", "wff_recipient_families").value + ) + + +def test_wff_complete_joint_income_table_preserves_source_labels(): + facts = [ + fact + for fact in _facts() + if fact.layout.record_set_id == f"{RECORD_PREFIX}.income_distribution" + ] + assert len(facts) == 39 * 8 + assert Counter(fact.layout.measure_id for fact in facts) == { + f"{component}_{measure}": 39 + for component in ("wff", "ftc", "iwtc", "bstc") + for measure in ("entitlement_nzd", "recipient_families") + } + expected_labels = {*range(5_000, 180_001, 5_000), "180,000+", "Unknown**", "All"} + assert { + fact.filters["family_scheme_income_band_source_label"] for fact in facts + } == expected_labels + for fact in facts: + constraints = [ + constraint + for constraint in fact.constraints + if constraint.variable == "family_scheme_income_band_source_label" + ] + assert len(constraints) == 1 + assert constraints[0].operator == "==" + assert ( + constraints[0].value + == fact.filters["family_scheme_income_band_source_label"] + ) + assert "numeric labels are not recoded" in fact.measure.concept_evidence_notes + assert fact.layout.table_record_kind == ( + "total" if fact.layout.groupby_value_id == "all_income_bands" else "detail" + ) + + +@pytest.mark.parametrize( + ("row", "counts", "amounts"), + [ + ( + "income_label_5000", + (2690, 2650, 810, 940), + (20_780_000, 16_750_000, 2_330_000, 1_700_000), + ), + ( + "income_label_180000_plus", + (16150, 210, 180, 15960), + (18_370_000, 340_000, 190_000, 17_840_000), + ), + ( + "income_unknown", + (3390, 2630, 1930, 1340), + (28_020_000, 20_790_000, 4_450_000, 2_700_000), + ), + ( + "all_income_bands", + (328400, 252500, 150500, 137200), + (3_043_000_000, 2_273_000_000, 437_000_000, 320_000_000), + ), + ], +) +def test_wff_income_table_exact_anchor_rows(row, counts, amounts): + for component, count, amount in zip( + ("wff", "ftc", "iwtc", "bstc"), counts, amounts + ): + assert ( + _fact("income_distribution", row, f"{component}_recipient_families").value + == count + ) + assert ( + _fact("income_distribution", row, f"{component}_entitlement_nzd").value + == amount + ) + + +def test_wff_income_rounding_adjustments_are_not_population_facts(): + cells = {cell.address: cell for cell in _cells() if cell.sheet_name == INCOME_SHEET} + assert cells["AO50"].raw_value == "Rounding adjustment" + assert cells["AO53"].raw_value == ( + "** Income is unknown for families that have not filed a Working for Families return." + ) + for column, adjustment in zip( + ("AP", "AQ", "AR", "AS", "AT", "AU", "AV", "AW"), + (-0.43, 0.82, 0.43, -0.2, 50, 100, 180, 50), + ): + assert cells[f"{column}50"].raw_value == pytest.approx(adjustment) + detail = sum(cells[f"{column}{row}"].raw_value for row in range(12, 50)) + # IRD reports detail minus total, not an additive population row. + assert detail - cells[f"{column}50"].raw_value == pytest.approx( + cells[f"{column}51"].raw_value + ) + assert all( + record.spec.selector.sheet_name != INCOME_SHEET + or not record.spec.selector.address.endswith("50") + for record in _records() + ) + + +@pytest.mark.parametrize( + ("sheet", "address", "message"), + [ + (DATA_SHEET, "A27", "row header"), + (DATA_SHEET, "B92", "column header"), + (INCOME_SHEET, "AO10", "income table tax-year label"), + (INCOME_SHEET, "AO52", "income table population footnote"), + ], +) +def test_wff_header_guards_reject_drift(sheet, address, message): + changed_cells = [ + replace(cell, raw_value="unexpected publisher layout") + if (cell.sheet_name, cell.address) == (sheet, address) + else cell + for cell in _cells() + ] + with pytest.raises(ValueError, match=message): + _package().build_facts(2024, cells=changed_cells) From 25f5aba08b8973e51005fb180e6250d4b1430c5a Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 29 Aug 2026 20:02:17 -0400 Subject: [PATCH 5/8] Record verified raw NZ WFF artifact in R2 (#176) --- .../working_for_families_statistics_sept_2025/manifest.yaml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/db/data/ird/working_for_families_statistics_sept_2025/manifest.yaml b/db/data/ird/working_for_families_statistics_sept_2025/manifest.yaml index 89c069a6..d73e416c 100644 --- a/db/data/ird/working_for_families_statistics_sept_2025/manifest.yaml +++ b/db/data/ird/working_for_families_statistics_sept_2025/manifest.yaml @@ -10,3 +10,9 @@ files: sha256: 95ae66f4d44f3f47ea3daa006328b22f061a163cf7e31b487342cde649390833 size_bytes: 71211 fetched_at: '2026-08-29T22:46:45+00:00' + storage: + r2: + provider: r2 + bucket: ledger-raw + key: raw/nz/ird/ird-working-for-families-statistics-sept-2025/2024/95ae66f4d44f3f47ea3daa006328b22f061a163cf7e31b487342cde649390833/working-for-families-statistics---sept-2025.xlsx + uri: r2://ledger-raw/raw/nz/ird/ird-working-for-families-statistics-sept-2025/2024/95ae66f4d44f3f47ea3daa006328b22f061a163cf7e31b487342cde649390833/working-for-families-statistics---sept-2025.xlsx From 1cc7ebec6207704f59d5ea23cad118450817d762 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 29 Aug 2026 20:02:17 -0400 Subject: [PATCH 6/8] Verify NZ WFF source acceptance and consumer provenance (#176) --- docs/pe-nz-source-checklist.md | 18 +++++++++++- tests/test_nz_targets.py | 51 ++++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+), 1 deletion(-) diff --git a/docs/pe-nz-source-checklist.md b/docs/pe-nz-source-checklist.md index 5ee95f95..84fe5c13 100644 --- a/docs/pe-nz-source-checklist.md +++ b/docs/pe-nz-source-checklist.md @@ -60,7 +60,7 @@ tests, and record a verified `raw/nz/...` R2 URI. | `stats_nz/census_2023_ethnicity_age_region` | [ ] | [ ] | [ ] | | | `ird/taxable_income_distribution_2025` | [ ] | [ ] | [ ] | | | `ird/wage_salary_distribution_2025` | [ ] | [ ] | [ ] | | -| `ird/working_for_families_statistics_sept_2025` | [x] | [x] | [ ] | TY2024: 330 administrative facts; count/entitlement, children, family size, and full published income table. | +| `ird/working_for_families_statistics_sept_2025` | [x] | [x] | [x] | TY2024: 330 administrative facts; count/entitlement, children, family size, and full published income table. | | `ird/student_loan_statistics_march_2026` | [ ] | [ ] | [ ] | | | `msd/benefit_fact_sheets_national_march_2026` | [ ] | [ ] | [ ] | | | `msd/benefit_fact_sheets_supplementary_march_2026` | [ ] | [ ] | [ ] | | @@ -68,3 +68,19 @@ tests, and record a verified `raw/nz/...` R2 URI. | `msd/annual_report_benefit_expenses_2025` | [ ] | [ ] | [ ] | | | `mbie/tenancy_bond_rents_tla_2026` | [ ] | [ ] | [ ] | | | `stats_nz/qes_average_earnings_march_2026` | [ ] | [ ] | [ ] | | + +### WFF source semantics + +The September 2025 IRD workbook contributes 330 facts for the 2024 tax year. +The national recipient totals describe administrative claims with non-zero +entitlement, not every eligible family or cash paid during the financial year. +The income table describes joint family income among WFF families, not the +individual taxable-income universe in the other IRD packages. Its numeric band +labels remain source labels until a consumer supplies an evidenced mapping. +Unknown income and the published total remain distinct source rows; the +publisher's rounding-adjustment row is parsed and tested but is not a population +fact. Independently published family and credit totals are not reconciled here. + +The workbook was uploaded to the manifest's immutable `raw/nz/ird/...` key and +downloaded again on 2026-08-29. Its SHA-256 remained +`95ae66f4d44f3f47ea3daa006328b22f061a163cf7e31b487342cde649390833`. diff --git a/tests/test_nz_targets.py b/tests/test_nz_targets.py index f9539fed..a50d7693 100644 --- a/tests/test_nz_targets.py +++ b/tests/test_nz_targets.py @@ -12,6 +12,11 @@ import pytest import yaml +from chronicle.bundle import build_bundle_coverage +from chronicle.consumer_contract import ( + consumer_fact_rows, + validate_consumer_fact_contract, +) from chronicle.core import validate_facts from chronicle.source_package import ( SOURCE_PACKAGE_ALIASES, @@ -19,6 +24,7 @@ validate_source_package, ) from chronicle.sources import build_source_cell_key, validate_source_cells +from chronicle.suite import build_source_suite REPO_ROOT = Path(__file__).resolve().parents[1] @@ -26,6 +32,7 @@ WFF_DIRECTORY = Path("ird/working_for_families_statistics_sept_2025") WFF_FILENAME = "working-for-families-statistics---sept-2025.xlsx" WFF_SHA256 = "95ae66f4d44f3f47ea3daa006328b22f061a163cf7e31b487342cde649390833" +WFF_R2_KEY = f"raw/nz/ird/{WFF_ALIAS}/2024/{WFF_SHA256}/{WFF_FILENAME}" WFF_SOURCE_URL = ( "https://www.ird.govt.nz/-/media/project/ir/home/documents/about-us/" "tax-statistics---current/social-policy/wff-stats/" @@ -91,6 +98,50 @@ def test_wff_official_artifact_is_pinned(): assert path.stat().st_size == artifact["size_bytes"] == 71_211 +def test_wff_r2_provenance_and_consumer_contract(): + data_dir = REPO_ROOT / "db" / "data" / WFF_DIRECTORY + manifest = yaml.safe_load((data_dir / "manifest.yaml").read_text()) + storage = manifest["files"][2024]["storage"]["r2"] + assert storage == { + "provider": "r2", + "bucket": "ledger-raw", + "key": WFF_R2_KEY, + "uri": f"r2://ledger-raw/{WFF_R2_KEY}", + } + assert {fact.source.raw_r2_uri for fact in _facts()} == {storage["uri"]} + assert validate_consumer_fact_contract(_facts()).valid + + +def test_wff_build_suite_passes_all_source_acceptance_gates(tmp_path): + report = build_source_suite(WFF_ALIAS, tmp_path / "suite", year=2024) + assert report.valid + assert report.agent_acceptance.valid + assert all(report.agent_acceptance.checks.values()) + assert not report.agent_acceptance.errors + assert report.source_records.resolved_count == 330 + assert report.source_records.lineage_coverage == 1 + assert report.source_cells.cell_count == 4910 + assert report.consumer_facts.fact_count == 330 + assert report.agent_acceptance.counts["raw_artifact_count"] == 1 + assert report.agent_acceptance.counts["raw_r2_link_count"] == 1 + + +def test_wff_bundle_coverage_delta_has_no_duplicate_keys(): + coverage = build_bundle_coverage(consumer_fact_rows(_facts())) + assert coverage["fact_count"] == 330 + assert coverage["counts"]["by_source"] == {"ird": 330} + assert coverage["counts"]["by_period"] == {"tax_year:2024": 330} + assert coverage["counts"]["by_geography"] == {"country:NZ": 330} + assert coverage["counts"]["by_entity"] == {"family": 329, "person": 1} + assert coverage["counts"]["by_source_table"] == { + "ird:Working for Families statistics - September 2025": 330 + } + assert coverage["duplicates"] == { + "aggregate_fact_keys": [], + "semantic_fact_keys": [], + } + + def test_wff_source_cells_and_fact_lineage_are_complete(): cells = _cells() facts = _facts() From 8c9781bd2da5c86ec285321b03a28d55c5f3b964 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 29 Aug 2026 20:46:48 -0400 Subject: [PATCH 7/8] Document the measured NZ source bundle delta (#176) WFF-only bundle: 330 facts, 329 family + 1 person, one source/package/country, tax_year 2024, zero aggregate or semantic duplicate keys. Default bundle expectations: 171855 + 330 = 172185 facts; 43 + 1 = 44 sources; 151 + 1 = 152 packages; 12536 + 1 = 12537 geographies; 107 + 329 = 436 family facts; 60466 + 1 = 60467 person facts; TY2024 40 + 330 = 370; 146 + 1 = 147 source tables. Period and duplicate counts are unchanged. Source-only build measured locally; all-country bundle remains a CI check. Describe the average-entitlement table as outside emitted scope, not as an exact algebraic duplicate of independently rounded count/amount tables. --- .../source_package.yaml | 6 +++--- tests/test_chronicle_bundle.py | 2 ++ 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/ird/working_for_families_statistics_sept_2025/source_package.yaml b/packages/ird/working_for_families_statistics_sept_2025/source_package.yaml index a116defe..fc78900e 100644 --- a/packages/ird/working_for_families_statistics_sept_2025/source_package.yaml +++ b/packages/ird/working_for_families_statistics_sept_2025/source_package.yaml @@ -3,9 +3,9 @@ # package keeps the non-derived 2024 WFF/IWTC statistics in the # workbook: recipient families and aggregate entitlement by current credit, # supported children and family size, and the full published family-income -# distribution. The publisher's average-entitlement table is algebraically -# derivable from the count and amount tables, so it is parsed but not emitted as -# duplicate facts. CTC/PTC are obsolete and blank in 2024. The income table's +# distribution. The publisher's average-entitlement table is parsed but is +# outside this package's emitted count-and-total-amount scope. CTC/PTC are +# obsolete and blank in 2024. The income table's # explicit "Rounding adjustment" row is not a population stratum; it is parsed, # excluded from facts, and pinned in package tests. Numeric income-band labels # are preserved as labels, not silently interpreted as interval boundaries. diff --git a/tests/test_chronicle_bundle.py b/tests/test_chronicle_bundle.py index d49b8b74..4429a60c 100644 --- a/tests/test_chronicle_bundle.py +++ b/tests/test_chronicle_bundle.py @@ -69,6 +69,8 @@ def test_build_bundle_writes_merged_consumer_contract(tmp_path): assert report.valid assert summary["valid"] + # NZ WFF adds 330 TY2024 facts: 329 families and 1 person, in one new + # publisher/package/country. Its measured source-only delta has no duplicates. assert summary["counts"] == { "aggregate_duplicate_key_count": 0, "entity_count": 12, From 98465d513c5814763859294832d73d570a0c28d1 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 29 Aug 2026 21:04:18 -0400 Subject: [PATCH 8/8] Qualify WFF administrative income definitions (#176) --- docs/pe-nz-source-checklist.md | 7 ++-- .../source_package.yaml | 32 +++++++++++++++++++ tests/test_nz_targets.py | 17 +++++++++- 3 files changed, 53 insertions(+), 3 deletions(-) diff --git a/docs/pe-nz-source-checklist.md b/docs/pe-nz-source-checklist.md index 84fe5c13..663eccdd 100644 --- a/docs/pe-nz-source-checklist.md +++ b/docs/pe-nz-source-checklist.md @@ -74,8 +74,11 @@ tests, and record a verified `raw/nz/...` R2 URI. The September 2025 IRD workbook contributes 330 facts for the 2024 tax year. The national recipient totals describe administrative claims with non-zero entitlement, not every eligible family or cash paid during the financial year. -The income table describes joint family income among WFF families, not the -individual taxable-income universe in the other IRD packages. Its numeric band +The income table uses an entitlement-time-weighted average of assessed family +scheme income. For Work and Income recipients who did not file a WFF return, +IRD uses individual tax-return income, which excludes partner income. These +workbook Explanatory notes qualify the published income bands; this is not the +individual taxable-income universe in the other IRD packages. Numeric band labels remain source labels until a consumer supplies an evidenced mapping. Unknown income and the published total remain distinct source rows; the publisher's rounding-adjustment row is parsed and tested but is not a population diff --git a/packages/ird/working_for_families_statistics_sept_2025/source_package.yaml b/packages/ird/working_for_families_statistics_sept_2025/source_package.yaml index fc78900e..01be2c2b 100644 --- a/packages/ird/working_for_families_statistics_sept_2025/source_package.yaml +++ b/packages/ird/working_for_families_statistics_sept_2025/source_package.yaml @@ -1154,6 +1154,10 @@ record_sets: Family-income labels are preserved exactly, including Unknown and All; numeric labels are not recoded to inferred interval boundaries. The total includes credit components not separately shown in this table. + The workbook Explanatory notes define assessed family scheme income as an + entitlement-time-weighted average. For Work and Income recipients who did + not file a WFF return, IRD uses individual tax-return income, which + excludes partner income. unit: nzd aggregation: sum value_scale: 1000000 @@ -1178,6 +1182,10 @@ record_sets: Family-income labels are preserved exactly, including Unknown and All; numeric labels are not recoded to inferred interval boundaries. The total includes credit components not separately shown in this table. + The workbook Explanatory notes define assessed family scheme income as an + entitlement-time-weighted average. For Work and Income recipients who did + not file a WFF return, IRD uses individual tax-return income, which + excludes partner income. unit: nzd aggregation: sum value_scale: 1000000 @@ -1202,6 +1210,10 @@ record_sets: Family-income labels are preserved exactly, including Unknown and All; numeric labels are not recoded to inferred interval boundaries. The total includes credit components not separately shown in this table. + The workbook Explanatory notes define assessed family scheme income as an + entitlement-time-weighted average. For Work and Income recipients who did + not file a WFF return, IRD uses individual tax-return income, which + excludes partner income. unit: nzd aggregation: sum value_scale: 1000000 @@ -1226,6 +1238,10 @@ record_sets: Family-income labels are preserved exactly, including Unknown and All; numeric labels are not recoded to inferred interval boundaries. The total includes credit components not separately shown in this table. + The workbook Explanatory notes define assessed family scheme income as an + entitlement-time-weighted average. For Work and Income recipients who did + not file a WFF return, IRD uses individual tax-return income, which + excludes partner income. unit: nzd aggregation: sum value_scale: 1000000 @@ -1250,6 +1266,10 @@ record_sets: Family-income labels are preserved exactly, including Unknown and All; numeric labels are not recoded to inferred interval boundaries. The total includes credit components not separately shown in this table. + The workbook Explanatory notes define assessed family scheme income as an + entitlement-time-weighted average. For Work and Income recipients who did + not file a WFF return, IRD uses individual tax-return income, which + excludes partner income. unit: count aggregation: sum value_scale: 1 @@ -1274,6 +1294,10 @@ record_sets: Family-income labels are preserved exactly, including Unknown and All; numeric labels are not recoded to inferred interval boundaries. The total includes credit components not separately shown in this table. + The workbook Explanatory notes define assessed family scheme income as an + entitlement-time-weighted average. For Work and Income recipients who did + not file a WFF return, IRD uses individual tax-return income, which + excludes partner income. unit: count aggregation: sum value_scale: 1 @@ -1298,6 +1322,10 @@ record_sets: Family-income labels are preserved exactly, including Unknown and All; numeric labels are not recoded to inferred interval boundaries. The total includes credit components not separately shown in this table. + The workbook Explanatory notes define assessed family scheme income as an + entitlement-time-weighted average. For Work and Income recipients who did + not file a WFF return, IRD uses individual tax-return income, which + excludes partner income. unit: count aggregation: sum value_scale: 1 @@ -1322,6 +1350,10 @@ record_sets: Family-income labels are preserved exactly, including Unknown and All; numeric labels are not recoded to inferred interval boundaries. The total includes credit components not separately shown in this table. + The workbook Explanatory notes define assessed family scheme income as an + entitlement-time-weighted average. For Work and Income recipients who did + not file a WFF return, IRD uses individual tax-return income, which + excludes partner income. unit: count aggregation: sum value_scale: 1 diff --git a/tests/test_nz_targets.py b/tests/test_nz_targets.py index a50d7693..c9f34f11 100644 --- a/tests/test_nz_targets.py +++ b/tests/test_nz_targets.py @@ -252,7 +252,7 @@ def test_wff_supported_children_and_family_sizes_remain_distinct(): ) -def test_wff_complete_joint_income_table_preserves_source_labels(): +def test_wff_complete_published_income_table_preserves_source_labels(): facts = [ fact for fact in _facts() @@ -286,6 +286,21 @@ def test_wff_complete_joint_income_table_preserves_source_labels(): ) +def test_wff_income_definition_preserves_publisher_qualifications(): + facts = [ + fact + for fact in _facts() + if fact.layout.record_set_id == f"{RECORD_PREFIX}.income_distribution" + ] + checklist = (REPO_ROOT / "docs" / "pe-nz-source-checklist.md").read_text() + for notes in [checklist, *(fact.measure.concept_evidence_notes for fact in facts)]: + normalized = " ".join(notes.lower().split()) + assert "entitlement-time-weighted" in normalized + assert "work and income" in normalized + assert "individual tax-return income" in normalized + assert "excludes partner income" in normalized + + @pytest.mark.parametrize( ("row", "counts", "amounts"), [